Merge branch 'mircopz-feature/new-multiple-input-widget'
This commit is contained in:
commit
66f74e961e
@ -196,6 +196,22 @@
|
|||||||
"dataKeySettingsSchema": "{}\n",
|
"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\":{}}"
|
"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": "<tb-multiple-input-widget \n form-id=\"formId\"\n ctx=\"ctx\">\n</tb-multiple-input-widget>",
|
||||||
|
"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\":{}}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -24,6 +24,7 @@ import thingsboardEntitiesTableWidget from '../widget/lib/entities-table-widget'
|
|||||||
import thingsboardEntitiesHierarchyWidget from '../widget/lib/entities-hierarchy-widget';
|
import thingsboardEntitiesHierarchyWidget from '../widget/lib/entities-hierarchy-widget';
|
||||||
import thingsboardExtensionsTableWidget from '../widget/lib/extensions-table-widget';
|
import thingsboardExtensionsTableWidget from '../widget/lib/extensions-table-widget';
|
||||||
import thingsboardDateRangeNavigatorWidget from '../widget/lib/date-range-navigator/date-range-navigator';
|
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';
|
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,
|
export default angular.module('thingsboard.api.widget', ['oc.lazyLoad', thingsboardLedLight,
|
||||||
thingsboardTimeseriesTableWidget, thingsboardAlarmsTableWidget, thingsboardEntitiesTableWidget,
|
thingsboardTimeseriesTableWidget, thingsboardAlarmsTableWidget, thingsboardEntitiesTableWidget,
|
||||||
thingsboardEntitiesHierarchyWidget, thingsboardExtensionsTableWidget, thingsboardDateRangeNavigatorWidget,
|
thingsboardEntitiesHierarchyWidget, thingsboardExtensionsTableWidget, thingsboardDateRangeNavigatorWidget,
|
||||||
thingsboardRpcWidgets, thingsboardTypes, thingsboardUtils, TripAnimationWidget])
|
thingsboardMultipleInputWidget, thingsboardRpcWidgets, thingsboardTypes, thingsboardUtils, TripAnimationWidget])
|
||||||
.factory('widgetService', WidgetService)
|
.factory('widgetService', WidgetService)
|
||||||
.name;
|
.name;
|
||||||
|
|
||||||
|
|||||||
@ -22,13 +22,17 @@ import ReactSchemaForm from './react/json-form-react.jsx';
|
|||||||
import jsonFormTemplate from './json-form.tpl.html';
|
import jsonFormTemplate from './json-form.tpl.html';
|
||||||
import { utils } from 'react-schema-form';
|
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', [])
|
export default angular.module('thingsboard.directives.jsonForm', [])
|
||||||
.directive('tbJsonForm', JsonForm)
|
.directive('tbJsonForm', JsonForm)
|
||||||
|
.controller('MaterialIconsDialogController', MaterialIconsDialogController)
|
||||||
.value('ReactSchemaForm', ReactSchemaForm)
|
.value('ReactSchemaForm', ReactSchemaForm)
|
||||||
.name;
|
.name;
|
||||||
|
|
||||||
/*@ngInject*/
|
/*@ngInject*/
|
||||||
function JsonForm($compile, $templateCache, $mdColorPicker) {
|
function JsonForm($compile, $templateCache, $mdColorPicker, $mdDialog, $document) {
|
||||||
|
|
||||||
var linker = function (scope, element) {
|
var linker = function (scope, element) {
|
||||||
|
|
||||||
@ -90,6 +94,9 @@ function JsonForm($compile, $templateCache, $mdColorPicker) {
|
|||||||
onColorClick: function(event, key, val) {
|
onColorClick: function(event, key, val) {
|
||||||
scope.showColorPicker(event, val);
|
scope.showColorPicker(event, val);
|
||||||
},
|
},
|
||||||
|
onIconClick: function(event) {
|
||||||
|
scope.openIconDialog(event);
|
||||||
|
},
|
||||||
onToggleFullscreen: function() {
|
onToggleFullscreen: function() {
|
||||||
scope.isFullscreen = !scope.isFullscreen;
|
scope.isFullscreen = !scope.isFullscreen;
|
||||||
scope.formProps.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.onFullscreenChanged = function() {}
|
||||||
|
|
||||||
scope.validate = function(){
|
scope.validate = function(){
|
||||||
|
|||||||
@ -131,7 +131,7 @@ class ThingsboardArray extends React.Component {
|
|||||||
}
|
}
|
||||||
let forms = this.props.form.items.map(function(form, index){
|
let forms = this.props.form.items.map(function(form, index){
|
||||||
var copy = this.copyWithIndex(form, i);
|
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));
|
}.bind(this));
|
||||||
arrays.push(
|
arrays.push(
|
||||||
<li key={keys[i]} className="list-group-item">
|
<li key={keys[i]} className="list-group-item">
|
||||||
|
|||||||
@ -19,7 +19,7 @@ class ThingsboardFieldSet extends React.Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
let forms = this.props.form.items.map(function(form, index){
|
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));
|
}.bind(this));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
134
ui/src/app/components/react/json-form-icon.jsx
Normal file
134
ui/src/app/components/react/json-form-icon.jsx
Normal file
@ -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 (
|
||||||
|
<div style={ styles.container }>
|
||||||
|
<div id="icon-container" style={ styles.iconContainer }>
|
||||||
|
<IconButton iconClassName="material-icons" style={ styles.icon }>
|
||||||
|
{pickedIcon}
|
||||||
|
</IconButton>
|
||||||
|
<TextField
|
||||||
|
className={fieldClass}
|
||||||
|
floatingLabelText={this.props.form.title}
|
||||||
|
hintText={this.props.form.placeholder}
|
||||||
|
errorText={this.props.error}
|
||||||
|
value={this.state.icon}
|
||||||
|
disabled={this.props.form.readonly}
|
||||||
|
style={ styles.iconText } />
|
||||||
|
</div>
|
||||||
|
<IconButton iconClassName="material-icons" tooltip="Clear" onTouchTap={this.onClear}>clear</IconButton>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ThingsboardBaseComponent(ThingsboardIcon);
|
||||||
@ -52,6 +52,7 @@ ReactSchemaForm.propTypes = {
|
|||||||
option: React.PropTypes.object,
|
option: React.PropTypes.object,
|
||||||
onModelChange: React.PropTypes.func,
|
onModelChange: React.PropTypes.func,
|
||||||
onColorClick: React.PropTypes.func,
|
onColorClick: React.PropTypes.func,
|
||||||
|
onIconClick: React.PropTypes.func,
|
||||||
onToggleFullscreen: React.PropTypes.func
|
onToggleFullscreen: React.PropTypes.func
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -32,6 +32,7 @@ import ThingsboardImage from './json-form-image.jsx';
|
|||||||
import ThingsboardCheckbox from './json-form-checkbox.jsx';
|
import ThingsboardCheckbox from './json-form-checkbox.jsx';
|
||||||
import Help from 'react-schema-form/lib/Help';
|
import Help from 'react-schema-form/lib/Help';
|
||||||
import ThingsboardFieldSet from './json-form-fieldset.jsx';
|
import ThingsboardFieldSet from './json-form-fieldset.jsx';
|
||||||
|
import ThingsboardIcon from './json-form-icon.jsx';
|
||||||
|
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
@ -58,11 +59,13 @@ class ThingsboardSchemaForm extends React.Component {
|
|||||||
'css': ThingsboardCss,
|
'css': ThingsboardCss,
|
||||||
'color': ThingsboardColor,
|
'color': ThingsboardColor,
|
||||||
'rc-select': ThingsboardRcSelect,
|
'rc-select': ThingsboardRcSelect,
|
||||||
'fieldset': ThingsboardFieldSet
|
'fieldset': ThingsboardFieldSet,
|
||||||
|
'icon': ThingsboardIcon
|
||||||
};
|
};
|
||||||
|
|
||||||
this.onChange = this.onChange.bind(this);
|
this.onChange = this.onChange.bind(this);
|
||||||
this.onColorClick = this.onColorClick.bind(this);
|
this.onColorClick = this.onColorClick.bind(this);
|
||||||
|
this.onIconClick = this.onIconClick.bind(this);
|
||||||
this.onToggleFullscreen = this.onToggleFullscreen.bind(this);
|
this.onToggleFullscreen = this.onToggleFullscreen.bind(this);
|
||||||
this.hasConditions = false;
|
this.hasConditions = false;
|
||||||
}
|
}
|
||||||
@ -79,12 +82,16 @@ class ThingsboardSchemaForm extends React.Component {
|
|||||||
this.props.onColorClick(event, key, val);
|
this.props.onColorClick(event, key, val);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onIconClick(event) {
|
||||||
|
this.props.onIconClick(event);
|
||||||
|
}
|
||||||
|
|
||||||
onToggleFullscreen() {
|
onToggleFullscreen() {
|
||||||
this.props.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;
|
var type = form.type;
|
||||||
let Field = this.mapper[type];
|
let Field = this.mapper[type];
|
||||||
if(!Field) {
|
if(!Field) {
|
||||||
@ -97,7 +104,7 @@ class ThingsboardSchemaForm extends React.Component {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return <Field model={model} form={form} key={index} onChange={onChange} onColorClick={onColorClick} onToggleFullscreen={onToggleFullscreen} mapper={mapper} builder={this.builder}/>
|
return <Field model={model} form={form} key={index} onChange={onChange} onColorClick={onColorClick} onIconClick={onIconClick} onToggleFullscreen={onToggleFullscreen} mapper={mapper} builder={this.builder}/>
|
||||||
}
|
}
|
||||||
|
|
||||||
createSchema(theForm) {
|
createSchema(theForm) {
|
||||||
@ -107,7 +114,7 @@ class ThingsboardSchemaForm extends React.Component {
|
|||||||
mapper = _.merge(this.mapper, this.props.mapper);
|
mapper = _.merge(this.mapper, this.props.mapper);
|
||||||
}
|
}
|
||||||
let forms = merged.map(function(form, index) {
|
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));
|
}.bind(this));
|
||||||
|
|
||||||
let formClass = 'SchemaForm';
|
let formClass = 'SchemaForm';
|
||||||
|
|||||||
@ -49,7 +49,8 @@
|
|||||||
"import": "Import",
|
"import": "Import",
|
||||||
"export": "Export",
|
"export": "Export",
|
||||||
"share-via": "Share via {{provider}}",
|
"share-via": "Share via {{provider}}",
|
||||||
"continue": "Continue"
|
"continue": "Continue",
|
||||||
|
"discard-changes": "Discard Changes"
|
||||||
},
|
},
|
||||||
"aggregation": {
|
"aggregation": {
|
||||||
"aggregation": "Aggregation",
|
"aggregation": "Aggregation",
|
||||||
|
|||||||
@ -49,6 +49,7 @@
|
|||||||
"import": "Importar",
|
"import": "Importar",
|
||||||
"export": "Exportar",
|
"export": "Exportar",
|
||||||
"share-via": "Compartir vía {{provider}}",
|
"share-via": "Compartir vía {{provider}}",
|
||||||
|
"discard-changes": "Cancelar los cambios",
|
||||||
"continue": "Continuar"
|
"continue": "Continuar"
|
||||||
},
|
},
|
||||||
"aggregation": {
|
"aggregation": {
|
||||||
|
|||||||
@ -48,7 +48,8 @@
|
|||||||
"undo": "Annuler",
|
"undo": "Annuler",
|
||||||
"update": "mise à jour",
|
"update": "mise à jour",
|
||||||
"view": "Afficher",
|
"view": "Afficher",
|
||||||
"yes": "Oui"
|
"yes": "Oui",
|
||||||
|
"discard-changes": "Annuler les modifications"
|
||||||
},
|
},
|
||||||
"admin": {
|
"admin": {
|
||||||
"base-url": "URL de base",
|
"base-url": "URL de base",
|
||||||
|
|||||||
@ -48,7 +48,8 @@
|
|||||||
"paste-reference": "Incolla riferimento",
|
"paste-reference": "Incolla riferimento",
|
||||||
"import": "Importa",
|
"import": "Importa",
|
||||||
"export": "Esporta",
|
"export": "Esporta",
|
||||||
"share-via": "Condividi con {{provider}}"
|
"share-via": "Condividi con {{provider}}",
|
||||||
|
"discard-changes": "Annulla le modifiche"
|
||||||
},
|
},
|
||||||
"aggregation": {
|
"aggregation": {
|
||||||
"aggregation": "Aggregazione",
|
"aggregation": "Aggregazione",
|
||||||
|
|||||||
288
ui/src/app/widget/lib/multiple-input-widget.js
Normal file
288
ui/src/app/widget/lib/multiple-input-widget.js
Normal file
@ -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];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
48
ui/src/app/widget/lib/multiple-input-widget.scss
Normal file
48
ui/src/app/widget/lib/multiple-input-widget.scss
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
68
ui/src/app/widget/lib/multiple-input-widget.tpl.html
Normal file
68
ui/src/app/widget/lib/multiple-input-widget.tpl.html
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
<!--
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
-->
|
||||||
|
<form class="tb-multiple-input" name="multipleInputForm" ng-submit="vm.postData($event)" novalidate>
|
||||||
|
<div style="padding: 0 8px; margin: auto 0;">
|
||||||
|
<div ng-show="vm.entityDetected" layout="row" flex ng-repeat="row in vm.rows" ng-init="rowIndex=$index">
|
||||||
|
<div layout="column" flex ng-repeat="key in row.data track by $index" ng-init="keyIndex=$index">
|
||||||
|
<md-tooltip class="tb-tooltip-multiline" ng-if="key.settings.tooltipMessage && key.settings.tooltipMessage.length" md-direction="left">
|
||||||
|
<span ng-bind-html="key.settings.tooltipMessage"></span>
|
||||||
|
</md-tooltip>
|
||||||
|
<md-input-container class="md-block" ng-style="vm.cellStyle(key, rowIndex, $first, $last)">
|
||||||
|
<label ng-style="vm.textColor(key)">{{key.label}}</label>
|
||||||
|
<md-icon ng-style="vm.textColor(key)" class="material-icons" ng-if="key.settings.icon">
|
||||||
|
{{key.settings.icon}}
|
||||||
|
</md-icon>
|
||||||
|
<input name="value{{rowIndex}}{{keyIndex}}"
|
||||||
|
ng-style="vm.textColor(key)"
|
||||||
|
ng-disabled="key.settings.readOnly"
|
||||||
|
ng-model="key.currentValue"
|
||||||
|
min="{{key.settings.min}}"
|
||||||
|
max="{{key.settings.max}}"
|
||||||
|
ng-required="key.settings.required"
|
||||||
|
type="{{key.settings.inputType}}"
|
||||||
|
step="{{key.settings.step}}"
|
||||||
|
md-select-on-focus
|
||||||
|
ng-change="vm.inputChanged()">
|
||||||
|
<div ng-messages="multipleInputForm['value' + rowIndex + keyIndex].$error">
|
||||||
|
<div ng-message="min">Value must be greater than {{key.settings.min}}</div>
|
||||||
|
<div ng-message="max">Value must be lower than {{key.settings.max}}</div>
|
||||||
|
<div ng-message="required">This field is required</div>
|
||||||
|
</div>
|
||||||
|
</md-input-container>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="text-align: center; font-size: 18px; color: #a0a0a0;" ng-hide="vm.entityDetected" ng-bind="vm.message"
|
||||||
|
></div>
|
||||||
|
<div style="text-align: center; font-size: 18px; color: #a0a0a0;" ng-show="vm.entityDetected && !vm.dataKeyDetected">
|
||||||
|
No attribute is selected
|
||||||
|
</div>
|
||||||
|
<div style="text-align: center; font-size: 18px; color: #a0a0a0;" ng-show="vm.entityDetected && !vm.isValidParameter">
|
||||||
|
Timeseries parameter cannot be used in this widget
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="md-padding" layout="row" layout-align="end center" ng-show="vm.entityDetected && vm.dataKeyDetected">
|
||||||
|
<md-button class="md-primary" ng-click="vm.discardAll()" style="max-height: 50px;margin-right:20px;" ng-disabled="!vm.hasAnyChange">
|
||||||
|
{{ 'action.undo' | translate }}
|
||||||
|
</md-button>
|
||||||
|
<md-button class="md-raised md-primary" type="submit" value="Submit" style="max-height: 50px;margin-right:20px;"
|
||||||
|
ng-disabled="!vm.hasAnyChange || multipleInputForm.$invalid" ng-click="vm.isFocused = false">
|
||||||
|
{{ 'action.save' | translate }}
|
||||||
|
</md-button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
Loading…
x
Reference in New Issue
Block a user