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);
}
scope.isFullscreen = false;
scope.formProps = {
isFullscreen: false,
option: {
formDefaults: {
startEmpty: true
@ -86,6 +89,10 @@ function JsonForm($compile, $templateCache, $mdColorPicker) {
},
onColorClick: function(event, key, 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(){
if (scope.schema && scope.model) {
var result = utils.validateBySchema(scope.schema, scope.model);

View File

@ -15,4 +15,6 @@
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: {
loadNodes: '=',
editCallbacks: '=',
enableSearch: '@?',
onNodeSelected: '&',
onNodesInserted: '&'
onNodesInserted: '&',
searchCallback: '&?'
},
controller: NavTreeController,
controllerAs: 'vm',
@ -55,17 +57,30 @@ function NavTreeController($scope, $element, types) {
});
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)
.jstree(
{
core: {
multiple: false,
check_callback: true,
themes: { name: 'proton', responsive: true },
data: vm.loadNodes
}
}
);
.jstree(config);
vm.treeElement.on("changed.jstree", function (e, data) {
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.onTidy = this.onTidy.bind(this);
this.onLoad = this.onLoad.bind(this);
this.onToggleFull = this.onToggleFull.bind(this);
var value = props.value ? props.value + '' : '';
this.state = {
isFull: false,
value: value,
focused: false
};
@ -76,9 +78,26 @@ class ThingsboardAceEditor extends React.Component {
}
onLoad(editor) {
this.aceEditor = 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() {
const styles = reactCSS({
@ -108,18 +127,23 @@ class ThingsboardAceEditor extends React.Component {
if (this.state.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 (
<div className="tb-container">
<div className={containerClass}>
<label className={labelClass}>{this.props.form.title}</label>
<div className="json-form-ace-editor">
<div className="title-panel">
<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={this.state.isFull ? 'Exit fullscreen' : 'Fullscreen'} onTouchTap={this.onToggleFull}/>
</div>
<AceEditor mode={this.props.mode}
height="150px"
width="300px"
height={this.state.isFull ? "100%" : "150px"}
width={this.state.isFull ? "100%" : "300px"}
theme="github"
onChange={this.onValueChanged}
onFocus={this.onFocus}
@ -132,10 +156,10 @@ class ThingsboardAceEditor extends React.Component {
enableBasicAutocompletion={true}
enableSnippets={true}
enableLiveAutocompletion={true}
style={this.props.form.style || {width: '100%'}}/>
style={style}/>
</div>
<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>
);
}

View File

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

View File

@ -131,7 +131,7 @@ class ThingsboardArray extends React.Component {
}
let forms = this.props.form.items.map(function(form, index){
var copy = this.copyWithIndex(form, i);
return this.props.builder(copy, this.props.model, index, this.props.onChange, this.props.onColorClick, this.props.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));
arrays.push(
<li key={keys[i]} className="list-group-item">

View File

@ -19,7 +19,7 @@ class ThingsboardFieldSet extends React.Component {
render() {
let forms = this.props.form.items.map(function(form, index){
return this.props.builder(form, this.props.model, index, this.props.onChange, this.props.onColorClick, this.props.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));
return (

View File

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

View File

@ -63,6 +63,7 @@ class ThingsboardSchemaForm extends React.Component {
this.onChange = this.onChange.bind(this);
this.onColorClick = this.onColorClick.bind(this);
this.onToggleFullscreen = this.onToggleFullscreen.bind(this);
this.hasConditions = false;
}
@ -78,7 +79,11 @@ class ThingsboardSchemaForm extends React.Component {
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;
let Field = this.mapper[type];
if(!Field) {
@ -91,7 +96,7 @@ class ThingsboardSchemaForm extends React.Component {
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() {
@ -101,11 +106,16 @@ class ThingsboardSchemaForm extends React.Component {
mapper = _.merge(this.mapper, this.props.mapper);
}
let forms = merged.map(function(form, index) {
return this.builder(form, this.props.model, index, this.onChange, this.onColorClick, mapper);
return this.builder(form, this.props.model, index, this.onChange, this.onColorClick, this.onToggleFullscreen, mapper);
}.bind(this));
let formClass = 'SchemaForm';
if (this.props.isFullscreen) {
formClass += ' SchemaFormFullscreen';
}
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-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 {
position: relative;
bottom: -5px;

View File

@ -54,6 +54,25 @@ function EntitiesHierarchyWidgetController($element, $scope, $q, $timeout, toast
vm.nodesMap = {};
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() {
if (vm.ctx && vm.ctx.defaultSubscription) {
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) {
if (vm.hierarchyId == hierarchyId) {
if (vm.subscription) {
@ -73,12 +98,10 @@ function EntitiesHierarchyWidgetController($element, $scope, $q, $timeout, toast
}
});
vm.onNodesInserted = onNodesInserted;
vm.onNodeSelected = onNodeSelected;
function initializeConfig() {
vm.ctx.widgetActions = [ vm.searchAction ];
var testNodeCtx = {
entity: {
id: {
@ -98,6 +121,7 @@ function EntitiesHierarchyWidgetController($element, $scope, $q, $timeout, toast
var nodeIconFunction = loadNodeCtxFunction(vm.settings.nodeIconFunction, 'nodeCtx', testNodeCtx);
var nodeTextFunction = loadNodeCtxFunction(vm.settings.nodeTextFunction, '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 testNodeCtx2 = angular.copy(testNodeCtx);
@ -109,6 +133,7 @@ function EntitiesHierarchyWidgetController($element, $scope, $q, $timeout, toast
vm.nodeIconFunction = nodeIconFunction || defaultNodeIconFunction;
vm.nodeTextFunction = nodeTextFunction || ((nodeCtx) => nodeCtx.entity.name);
vm.nodeDisabledFunction = nodeDisabledFunction || (() => false);
vm.nodeOpenedFunction = nodeOpenedFunction || defaultNodeOpenedFunction;
vm.nodeHasChildrenFunction = nodeHasChildrenFunction || (() => true);
vm.nodesSortFunction = nodesSortFunction || defaultSortFunction;
}
@ -129,10 +154,40 @@ function EntitiesHierarchyWidgetController($element, $scope, $q, $timeout, toast
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() {
vm.loadNodes = loadNodes;
}
function updateSearchNodes() {
if (vm.query.search != null) {
vm.nodeEditCallbacks.search(vm.query.search);
} else {
vm.nodeEditCallbacks.clearSearch();
}
}
function onNodesInserted(nodes/*, parent*/) {
if (nodes) {
nodes.forEach((nodeId) => {
@ -222,6 +277,7 @@ function EntitiesHierarchyWidgetController($element, $scope, $q, $timeout, toast
function prepareNodeText(node) {
var nodeIcon = prepareNodeIcon(node.data.nodeCtx);
var nodeText = vm.nodeTextFunction(node.data.nodeCtx);
node.data.searchText = nodeText ? nodeText.replace(/<[^>]+>/g, '').toLowerCase() : "";
return nodeIcon + nodeText;
}
@ -298,7 +354,8 @@ function EntitiesHierarchyWidgetController($element, $scope, $q, $timeout, toast
nodeCtx: nodeCtx
};
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.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) {
var result = nodeCtx1.entity.id.entityType.localeCompare(nodeCtx2.entity.id.entityType);
if (result === 0) {

View File

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

View File

@ -17,12 +17,34 @@
-->
<div class="tb-absolute-fill tb-entities-hierarchy" 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">
<tb-nav-tree
load-nodes="vm.loadNodes"
on-node-selected="vm.onNodeSelected(node, event)"
on-nodes-inserted="vm.onNodesInserted(nodes, parent)"
edit-callbacks="vm.nodeEditCallbacks"
enable-search="true"
search-callback="vm.searchCallback(searchText, node)"
></tb-nav-tree>
</div>
</div>