Add search to Entities Hierarchy widget. Improve widget advanced forms: add fullscreen button for area fields.

This commit is contained in:
Igor Kulikov 2019-03-15 16:49:14 +02:00
parent 0d933cf065
commit f671d81ce4
14 changed files with 221 additions and 32 deletions

File diff suppressed because one or more lines are too long

View File

@ -71,7 +71,10 @@ function JsonForm($compile, $templateCache, $mdColorPicker) {
$compile(element.contents())(childScope); $compile(element.contents())(childScope);
} }
scope.isFullscreen = false;
scope.formProps = { scope.formProps = {
isFullscreen: false,
option: { option: {
formDefaults: { formDefaults: {
startEmpty: true startEmpty: true
@ -86,6 +89,10 @@ function JsonForm($compile, $templateCache, $mdColorPicker) {
}, },
onColorClick: function(event, key, val) { onColorClick: function(event, key, val) {
scope.showColorPicker(event, val); scope.showColorPicker(event, val);
},
onToggleFullscreen: function() {
scope.isFullscreen = !scope.isFullscreen;
scope.formProps.isFullscreen = scope.isFullscreen;
} }
}; };
@ -116,6 +123,8 @@ function JsonForm($compile, $templateCache, $mdColorPicker) {
}); });
} }
scope.onFullscreenChanged = function() {}
scope.validate = function(){ scope.validate = function(){
if (scope.schema && scope.model) { if (scope.schema && scope.model) {
var result = utils.validateBySchema(scope.schema, scope.model); var result = utils.validateBySchema(scope.schema, scope.model);

View File

@ -15,4 +15,6 @@
limitations under the License. limitations under the License.
--> -->
<react-component name="ReactSchemaForm" props="formProps" watch-depth="value"></react-component> <div style="background: #fff;" tb-expand-fullscreen="isFullscreen" hide-expand-button="true" fullscreen-zindex="100" on-fullscreen-changed="onFullscreenChanged()">
<react-component name="ReactSchemaForm" props="formProps" watch-depth="value"></react-component>
</div>

View File

@ -33,8 +33,10 @@ function NavTree() {
bindToController: { bindToController: {
loadNodes: '=', loadNodes: '=',
editCallbacks: '=', editCallbacks: '=',
enableSearch: '@?',
onNodeSelected: '&', onNodeSelected: '&',
onNodesInserted: '&' onNodesInserted: '&',
searchCallback: '&?'
}, },
controller: NavTreeController, controller: NavTreeController,
controllerAs: 'vm', controllerAs: 'vm',
@ -55,17 +57,30 @@ function NavTreeController($scope, $element, types) {
}); });
function initTree() { function initTree() {
var config = {
core: {
multiple: false,
check_callback: true,
themes: { name: 'proton', responsive: true },
data: vm.loadNodes
}
};
if (vm.enableSearch) {
config.plugins = ["search"];
config.search = {
case_sensitive: false,
show_only_matches: true,
show_only_matches_children: false,
search_leaves_only: false
};
if (vm.searchCallback) {
config.search.search_callback = (searchText, node) => vm.searchCallback({searchText: searchText, node: node});
}
}
vm.treeElement = angular.element('.tb-nav-tree-container', $element) vm.treeElement = angular.element('.tb-nav-tree-container', $element)
.jstree( .jstree(config);
{
core: {
multiple: false,
check_callback: true,
themes: { name: 'proton', responsive: true },
data: vm.loadNodes
}
}
);
vm.treeElement.on("changed.jstree", function (e, data) { vm.treeElement.on("changed.jstree", function (e, data) {
if (vm.onNodeSelected) { if (vm.onNodeSelected) {
@ -180,6 +195,12 @@ function NavTreeController($scope, $element, types) {
} }
} }
}; };
vm.editCallbacks.search = (searchText) => {
vm.treeElement.jstree('search', searchText);
};
vm.editCallbacks.clearSearch = () => {
vm.treeElement.jstree('clear_search');
};
} }
} }
} }

View File

@ -34,8 +34,10 @@ class ThingsboardAceEditor extends React.Component {
this.onFocus = this.onFocus.bind(this); this.onFocus = this.onFocus.bind(this);
this.onTidy = this.onTidy.bind(this); this.onTidy = this.onTidy.bind(this);
this.onLoad = this.onLoad.bind(this); this.onLoad = this.onLoad.bind(this);
this.onToggleFull = this.onToggleFull.bind(this);
var value = props.value ? props.value + '' : ''; var value = props.value ? props.value + '' : '';
this.state = { this.state = {
isFull: false,
value: value, value: value,
focused: false focused: false
}; };
@ -76,9 +78,26 @@ class ThingsboardAceEditor extends React.Component {
} }
onLoad(editor) { onLoad(editor) {
this.aceEditor = editor;
fixAceEditor(editor); fixAceEditor(editor);
} }
onToggleFull() {
this.setState({ isFull: !this.state.isFull });
this.props.onToggleFullscreen();
this.updateAceEditorSize = true;
}
componentDidUpdate() {
if (this.updateAceEditorSize) {
if (this.aceEditor) {
this.aceEditor.resize();
this.aceEditor.renderer.updateFull();
}
this.updateAceEditorSize = false;
}
}
render() { render() {
const styles = reactCSS({ const styles = reactCSS({
@ -108,18 +127,23 @@ class ThingsboardAceEditor extends React.Component {
if (this.state.focused) { if (this.state.focused) {
labelClass += " tb-focused"; labelClass += " tb-focused";
} }
var containerClass = "tb-container";
var style = this.props.form.style || {width: '100%'};
if (this.state.isFull) {
containerClass += " fullscreen-form-field";
}
return ( return (
<div className="tb-container"> <div className={containerClass}>
<label className={labelClass}>{this.props.form.title}</label> <label className={labelClass}>{this.props.form.title}</label>
<div className="json-form-ace-editor"> <div className="json-form-ace-editor">
<div className="title-panel"> <div className="title-panel">
<label>{this.props.mode}</label> <label>{this.props.mode}</label>
<FlatButton style={ styles.tidyButtonStyle } className="tidy-button" label={'Tidy'} onTouchTap={this.onTidy}/> <FlatButton style={ styles.tidyButtonStyle } className="tidy-button" label={'Tidy'} onTouchTap={this.onTidy}/>
<FlatButton style={ styles.tidyButtonStyle } className="tidy-button" label={this.state.isFull ? 'Exit fullscreen' : 'Fullscreen'} onTouchTap={this.onToggleFull}/>
</div> </div>
<AceEditor mode={this.props.mode} <AceEditor mode={this.props.mode}
height="150px" height={this.state.isFull ? "100%" : "150px"}
width="300px" width={this.state.isFull ? "100%" : "300px"}
theme="github" theme="github"
onChange={this.onValueChanged} onChange={this.onValueChanged}
onFocus={this.onFocus} onFocus={this.onFocus}
@ -132,10 +156,10 @@ class ThingsboardAceEditor extends React.Component {
enableBasicAutocompletion={true} enableBasicAutocompletion={true}
enableSnippets={true} enableSnippets={true}
enableLiveAutocompletion={true} enableLiveAutocompletion={true}
style={this.props.form.style || {width: '100%'}}/> style={style}/>
</div> </div>
<div className="json-form-error" <div className="json-form-error"
style={{opacity: this.props.valid ? '0' : '1'}}>{this.props.error}</div> style={{opacity: this.props.valid ? '0' : '1'}}>{this.props.error}</div>
</div> </div>
); );
} }

View File

@ -13,6 +13,13 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
.fullscreen-form-field {
.json-form-ace-editor {
height: calc(100% - 60px);
}
}
.json-form-ace-editor { .json-form-ace-editor {
position: relative; position: relative;
height: 100%; height: 100%;

View File

@ -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.mapper, this.props.builder); return this.props.builder(copy, this.props.model, index, this.props.onChange, this.props.onColorClick, 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">

View File

@ -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.mapper, this.props.builder); return this.props.builder(form, this.props.model, index, this.props.onChange, this.props.onColorClick, this.props.onToggleFullscreen, this.props.mapper, this.props.builder);
}.bind(this)); }.bind(this));
return ( return (

View File

@ -50,7 +50,8 @@ ReactSchemaForm.propTypes = {
model: React.PropTypes.object, model: React.PropTypes.object,
option: React.PropTypes.object, option: React.PropTypes.object,
onModelChange: React.PropTypes.func, onModelChange: React.PropTypes.func,
onColorClick: React.PropTypes.func onColorClick: React.PropTypes.func,
onToggleFullscreen: React.PropTypes.func
} }
ReactSchemaForm.defaultProps = { ReactSchemaForm.defaultProps = {

View File

@ -63,6 +63,7 @@ class ThingsboardSchemaForm extends React.Component {
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.onToggleFullscreen = this.onToggleFullscreen.bind(this);
this.hasConditions = false; this.hasConditions = false;
} }
@ -78,7 +79,11 @@ class ThingsboardSchemaForm extends React.Component {
this.props.onColorClick(event, key, val); this.props.onColorClick(event, key, val);
} }
builder(form, model, index, onChange, onColorClick, mapper) { onToggleFullscreen() {
this.props.onToggleFullscreen();
}
builder(form, model, index, onChange, onColorClick, onToggleFullscreen, mapper) {
var type = form.type; var type = form.type;
let Field = this.mapper[type]; let Field = this.mapper[type];
if(!Field) { if(!Field) {
@ -91,7 +96,7 @@ class ThingsboardSchemaForm extends React.Component {
return null; return null;
} }
} }
return <Field model={model} form={form} key={index} onChange={onChange} onColorClick={onColorClick} mapper={mapper} builder={this.builder}/> return <Field model={model} form={form} key={index} onChange={onChange} onColorClick={onColorClick} onToggleFullscreen={onToggleFullscreen} mapper={mapper} builder={this.builder}/>
} }
render() { render() {
@ -101,11 +106,16 @@ 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, mapper); return this.builder(form, this.props.model, index, this.onChange, this.onColorClick, this.onToggleFullscreen, mapper);
}.bind(this)); }.bind(this));
let formClass = 'SchemaForm';
if (this.props.isFullscreen) {
formClass += ' SchemaFormFullscreen';
}
return ( return (
<div style={{width: '100%'}} className='SchemaForm'>{forms}</div> <div style={{width: '100%'}} className={formClass}>{forms}</div>
); );
} }
} }

View File

@ -21,6 +21,24 @@ $swift-ease-out-timing-function: cubic-bezier(.25, .8, .25, 1) !default;
$input-label-float-offset: 6px !default; $input-label-float-offset: 6px !default;
$input-label-float-scale: .75 !default; $input-label-float-scale: .75 !default;
.SchemaForm {
&.SchemaFormFullscreen {
position: relative;
width: 100%;
height: 100%;
> div:not(.fullscreen-form-field) {
display: none;
}
> div.fullscreen-form-field {
position: relative;
width: 100%;
height: 100%;
}
}
}
.json-form-error { .json-form-error {
position: relative; position: relative;
bottom: -5px; bottom: -5px;

View File

@ -54,6 +54,25 @@ function EntitiesHierarchyWidgetController($element, $scope, $q, $timeout, toast
vm.nodesMap = {}; vm.nodesMap = {};
vm.pendingUpdateNodeTasks = {}; vm.pendingUpdateNodeTasks = {};
vm.query = {
search: null
};
vm.searchAction = {
name: 'action.search',
show: true,
onAction: function() {
vm.enterFilterMode();
},
icon: 'search'
};
vm.onNodesInserted = onNodesInserted;
vm.onNodeSelected = onNodeSelected;
vm.enterFilterMode = enterFilterMode;
vm.exitFilterMode = exitFilterMode;
vm.searchCallback = searchCallback;
$scope.$watch('vm.ctx', function() { $scope.$watch('vm.ctx', function() {
if (vm.ctx && vm.ctx.defaultSubscription) { if (vm.ctx && vm.ctx.defaultSubscription) {
vm.settings = vm.ctx.settings; vm.settings = vm.ctx.settings;
@ -65,6 +84,12 @@ function EntitiesHierarchyWidgetController($element, $scope, $q, $timeout, toast
} }
}); });
$scope.$watch("vm.query.search", function(newVal, prevVal) {
if (!angular.equals(newVal, prevVal) && vm.query.search != null) {
updateSearchNodes();
}
});
$scope.$on('entities-hierarchy-data-updated', function(event, hierarchyId) { $scope.$on('entities-hierarchy-data-updated', function(event, hierarchyId) {
if (vm.hierarchyId == hierarchyId) { if (vm.hierarchyId == hierarchyId) {
if (vm.subscription) { if (vm.subscription) {
@ -73,12 +98,10 @@ function EntitiesHierarchyWidgetController($element, $scope, $q, $timeout, toast
} }
}); });
vm.onNodesInserted = onNodesInserted;
vm.onNodeSelected = onNodeSelected;
function initializeConfig() { function initializeConfig() {
vm.ctx.widgetActions = [ vm.searchAction ];
var testNodeCtx = { var testNodeCtx = {
entity: { entity: {
id: { id: {
@ -98,6 +121,7 @@ function EntitiesHierarchyWidgetController($element, $scope, $q, $timeout, toast
var nodeIconFunction = loadNodeCtxFunction(vm.settings.nodeIconFunction, 'nodeCtx', testNodeCtx); var nodeIconFunction = loadNodeCtxFunction(vm.settings.nodeIconFunction, 'nodeCtx', testNodeCtx);
var nodeTextFunction = loadNodeCtxFunction(vm.settings.nodeTextFunction, 'nodeCtx', testNodeCtx); var nodeTextFunction = loadNodeCtxFunction(vm.settings.nodeTextFunction, 'nodeCtx', testNodeCtx);
var nodeDisabledFunction = loadNodeCtxFunction(vm.settings.nodeDisabledFunction, 'nodeCtx', testNodeCtx); var nodeDisabledFunction = loadNodeCtxFunction(vm.settings.nodeDisabledFunction, 'nodeCtx', testNodeCtx);
var nodeOpenedFunction = loadNodeCtxFunction(vm.settings.nodeOpenedFunction, 'nodeCtx', testNodeCtx);
var nodeHasChildrenFunction = loadNodeCtxFunction(vm.settings.nodeHasChildrenFunction, 'nodeCtx', testNodeCtx); var nodeHasChildrenFunction = loadNodeCtxFunction(vm.settings.nodeHasChildrenFunction, 'nodeCtx', testNodeCtx);
var testNodeCtx2 = angular.copy(testNodeCtx); var testNodeCtx2 = angular.copy(testNodeCtx);
@ -109,6 +133,7 @@ function EntitiesHierarchyWidgetController($element, $scope, $q, $timeout, toast
vm.nodeIconFunction = nodeIconFunction || defaultNodeIconFunction; vm.nodeIconFunction = nodeIconFunction || defaultNodeIconFunction;
vm.nodeTextFunction = nodeTextFunction || ((nodeCtx) => nodeCtx.entity.name); vm.nodeTextFunction = nodeTextFunction || ((nodeCtx) => nodeCtx.entity.name);
vm.nodeDisabledFunction = nodeDisabledFunction || (() => false); vm.nodeDisabledFunction = nodeDisabledFunction || (() => false);
vm.nodeOpenedFunction = nodeOpenedFunction || defaultNodeOpenedFunction;
vm.nodeHasChildrenFunction = nodeHasChildrenFunction || (() => true); vm.nodeHasChildrenFunction = nodeHasChildrenFunction || (() => true);
vm.nodesSortFunction = nodesSortFunction || defaultSortFunction; vm.nodesSortFunction = nodesSortFunction || defaultSortFunction;
} }
@ -129,10 +154,40 @@ function EntitiesHierarchyWidgetController($element, $scope, $q, $timeout, toast
return nodeCtxFunction; return nodeCtxFunction;
} }
function enterFilterMode () {
vm.query.search = '';
vm.ctx.hideTitlePanel = true;
$timeout(()=>{
angular.element(vm.ctx.$container).find('.searchInput').focus();
})
}
function exitFilterMode () {
vm.query.search = null;
updateSearchNodes();
vm.ctx.hideTitlePanel = false;
}
function searchCallback (searchText, node) {
var theNode = vm.nodesMap[node.id];
if (theNode && theNode.data.searchText) {
return theNode.data.searchText.includes(searchText.toLowerCase());
}
return false;
}
function updateDatasources() { function updateDatasources() {
vm.loadNodes = loadNodes; vm.loadNodes = loadNodes;
} }
function updateSearchNodes() {
if (vm.query.search != null) {
vm.nodeEditCallbacks.search(vm.query.search);
} else {
vm.nodeEditCallbacks.clearSearch();
}
}
function onNodesInserted(nodes/*, parent*/) { function onNodesInserted(nodes/*, parent*/) {
if (nodes) { if (nodes) {
nodes.forEach((nodeId) => { nodes.forEach((nodeId) => {
@ -222,6 +277,7 @@ function EntitiesHierarchyWidgetController($element, $scope, $q, $timeout, toast
function prepareNodeText(node) { function prepareNodeText(node) {
var nodeIcon = prepareNodeIcon(node.data.nodeCtx); var nodeIcon = prepareNodeIcon(node.data.nodeCtx);
var nodeText = vm.nodeTextFunction(node.data.nodeCtx); var nodeText = vm.nodeTextFunction(node.data.nodeCtx);
node.data.searchText = nodeText ? nodeText.replace(/<[^>]+>/g, '').toLowerCase() : "";
return nodeIcon + nodeText; return nodeIcon + nodeText;
} }
@ -298,7 +354,8 @@ function EntitiesHierarchyWidgetController($element, $scope, $q, $timeout, toast
nodeCtx: nodeCtx nodeCtx: nodeCtx
}; };
node.state = { node.state = {
disabled: vm.nodeDisabledFunction(node.data.nodeCtx) disabled: vm.nodeDisabledFunction(node.data.nodeCtx),
opened: vm.nodeOpenedFunction(node.data.nodeCtx)
}; };
node.text = prepareNodeText(node); node.text = prepareNodeText(node);
node.children = vm.nodeHasChildrenFunction(node.data.nodeCtx); node.children = vm.nodeHasChildrenFunction(node.data.nodeCtx);
@ -459,6 +516,10 @@ function EntitiesHierarchyWidgetController($element, $scope, $q, $timeout, toast
}; };
} }
function defaultNodeOpenedFunction(nodeCtx) {
return nodeCtx.level <= 4;
}
function defaultSortFunction(nodeCtx1, nodeCtx2) { function defaultSortFunction(nodeCtx1, nodeCtx2) {
var result = nodeCtx1.entity.id.entityType.localeCompare(nodeCtx2.entity.id.entityType); var result = nodeCtx1.entity.id.entityType.localeCompare(nodeCtx2.entity.id.entityType);
if (result === 0) { if (result === 0) {

View File

@ -14,7 +14,21 @@
* limitations under the License. * limitations under the License.
*/ */
.tb-has-timewindow {
.tb-entities-hierarchy {
md-toolbar {
min-height: 60px;
max-height: 60px;
}
}
}
.tb-entities-hierarchy { .tb-entities-hierarchy {
md-toolbar {
min-height: 39px;
max-height: 39px;
}
.tb-entities-nav-tree-panel { .tb-entities-nav-tree-panel {
overflow-x: auto; overflow-x: auto;
overflow-y: auto; overflow-y: auto;

View File

@ -17,12 +17,34 @@
--> -->
<div class="tb-absolute-fill tb-entities-hierarchy" layout="column"> <div class="tb-absolute-fill tb-entities-hierarchy" layout="column">
<div ng-show="vm.showData" flex class="tb-absolute-fill" layout="column"> <div ng-show="vm.showData" flex class="tb-absolute-fill" layout="column">
<md-toolbar class="md-table-toolbar md-default" ng-show="vm.query.search != null">
<div class="md-toolbar-tools">
<md-button class="md-icon-button" aria-label="{{ 'action.search' | translate }}">
<md-icon aria-label="{{ 'action.search' | translate }}" class="material-icons">search</md-icon>
<md-tooltip md-direction="{{vm.ctx.dashboard.isWidgetExpanded ? 'bottom' : 'top'}}">
{{'entity.search' | translate}}
</md-tooltip>
</md-button>
<md-input-container flex>
<label>&nbsp;</label>
<input ng-model="vm.query.search" class="searchInput" placeholder="{{'entity.search' | translate}}"/>
</md-input-container>
<md-button class="md-icon-button" aria-label="Close" ng-click="vm.exitFilterMode()">
<md-icon aria-label="Close" class="material-icons">close</md-icon>
<md-tooltip md-direction="{{vm.ctx.dashboard.isWidgetExpanded ? 'bottom' : 'top'}}">
{{ 'action.close' | translate }}
</md-tooltip>
</md-button>
</div>
</md-toolbar>
<div flex class="tb-entities-nav-tree-panel"> <div flex class="tb-entities-nav-tree-panel">
<tb-nav-tree <tb-nav-tree
load-nodes="vm.loadNodes" load-nodes="vm.loadNodes"
on-node-selected="vm.onNodeSelected(node, event)" on-node-selected="vm.onNodeSelected(node, event)"
on-nodes-inserted="vm.onNodesInserted(nodes, parent)" on-nodes-inserted="vm.onNodesInserted(nodes, parent)"
edit-callbacks="vm.nodeEditCallbacks" edit-callbacks="vm.nodeEditCallbacks"
enable-search="true"
search-callback="vm.searchCallback(searchText, node)"
></tb-nav-tree> ></tb-nav-tree>
</div> </div>
</div> </div>