Add search to Entities Hierarchy widget. Improve widget advanced forms: add fullscreen button for area fields.
This commit is contained in:
parent
0d933cf065
commit
f671d81ce4
File diff suppressed because one or more lines are too long
@ -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);
|
||||
|
||||
@ -15,4 +15,6 @@
|
||||
limitations under the License.
|
||||
|
||||
-->
|
||||
<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>
|
||||
|
||||
@ -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() {
|
||||
vm.treeElement = angular.element('.tb-nav-tree-container', $element)
|
||||
.jstree(
|
||||
{
|
||||
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(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');
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,7 +156,7 @@ 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>
|
||||
|
||||
@ -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%;
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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> </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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user