Rule Chain UI: Copy/Paste support.

This commit is contained in:
Igor Kulikov 2018-04-11 22:12:52 +03:00
parent 737f2cc92a
commit b13d666f1b
5 changed files with 225 additions and 5 deletions

View File

@ -1208,6 +1208,7 @@ export default angular.module('thingsboard.locale', [])
"delete-selected-objects": "Delete selected nodes and connections", "delete-selected-objects": "Delete selected nodes and connections",
"delete-selected": "Delete selected", "delete-selected": "Delete selected",
"select-all": "Select all", "select-all": "Select all",
"copy-selected": "Copy selected",
"deselect-all": "Deselect all", "deselect-all": "Deselect all",
"rulenode-details": "Rule node details", "rulenode-details": "Rule node details",
"debug-mode": "Debug mode", "debug-mode": "Debug mode",

View File

@ -29,7 +29,7 @@ import addRuleNodeLinkTemplate from './add-link.tpl.html';
/*@ngInject*/ /*@ngInject*/
export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $timeout, $mdExpansionPanel, $window, $document, $mdDialog, export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $timeout, $mdExpansionPanel, $window, $document, $mdDialog,
$filter, $translate, hotkeys, types, ruleChainService, Modelfactory, flowchartConstants, $filter, $translate, hotkeys, types, ruleChainService, itembuffer, Modelfactory, flowchartConstants,
ruleChain, ruleChainMetaData, ruleNodeComponents) { ruleChain, ruleChainMetaData, ruleNodeComponents) {
var vm = this; var vm = this;
@ -140,6 +140,22 @@ export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $time
subtitle: $translate.instant('rulechain.rulechain') subtitle: $translate.instant('rulechain.rulechain')
}; };
contextInfo.items = []; contextInfo.items = [];
contextInfo.items.push(
{
action: function ($event) {
pasteRuleNodes($event);
},
enabled: itembuffer.hasRuleNodes(),
value: "action.paste",
icon: "content_paste",
shortcut: "M-V"
}
);
contextInfo.items.push(
{
divider: true
}
);
if (objectsSelected()) { if (objectsSelected()) {
contextInfo.items.push( contextInfo.items.push(
{ {
@ -152,6 +168,17 @@ export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $time
shortcut: "Esc" shortcut: "Esc"
} }
); );
contextInfo.items.push(
{
action: function (event) {
copyRuleNodes(event);
},
enabled: true,
value: "rulenode.copy-selected",
icon: "content_copy",
shortcut: "M-C"
}
);
contextInfo.items.push( contextInfo.items.push(
{ {
action: function () { action: function () {
@ -176,6 +203,11 @@ export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $time
} }
); );
} }
contextInfo.items.push(
{
divider: true
}
);
contextInfo.items.push( contextInfo.items.push(
{ {
action: function () { action: function () {
@ -220,6 +252,16 @@ export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $time
icon: "menu" icon: "menu"
} }
); );
contextInfo.items.push(
{
action: function (event) {
copyNode(event, node);
},
enabled: true,
value: "action.copy",
icon: "content_copy"
}
);
contextInfo.items.push( contextInfo.items.push(
{ {
action: function () { action: function () {
@ -526,7 +568,7 @@ export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $time
if (destNode.component.type == types.ruleNodeType.RULE_CHAIN.value) { if (destNode.component.type == types.ruleNodeType.RULE_CHAIN.value) {
deferred.reject(); deferred.reject();
} else { } else {
var res = $filter('filter')(vm.ruleChainModel.edges, {source: vm.inputConnectorId}); var res = $filter('filter')(vm.ruleChainModel.edges, {source: vm.inputConnectorId}, true);
if (res && res.length) { if (res && res.length) {
vm.modelservice.edges.delete(res[0]); vm.modelservice.edges.delete(res[0]);
} }
@ -582,6 +624,112 @@ export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $time
} }
} }
function copyNode(event, node) {
var offset = angular.element(vm.canvasControl.modelservice.getCanvasHtmlElement()).offset();
var x = Math.round(event.clientX - offset.left);
var y = Math.round(event.clientY - offset.top);
itembuffer.copyRuleNodes(x, y, [node], []);
}
function copyRuleNodes(event) {
var offset = angular.element(vm.canvasControl.modelservice.getCanvasHtmlElement()).offset();
var x = Math.round(event.clientX - offset.left);
var y = Math.round(event.clientY - offset.top);
var nodes = vm.modelservice.nodes.getSelectedNodes();
var edges = vm.modelservice.edges.getSelectedEdges();
var connections = [];
for (var i=0;i<edges.length;i++) {
var edge = edges[i];
var sourceNode = vm.modelservice.nodes.getNodeByConnectorId(edge.source);
var destNode = vm.modelservice.nodes.getNodeByConnectorId(edge.destination);
var isInputSource = sourceNode.component.type == types.ruleNodeType.INPUT.value;
var fromIndex = nodes.indexOf(sourceNode);
var toIndex = nodes.indexOf(destNode);
if ( (isInputSource || fromIndex > -1) && toIndex > -1 ) {
var connection = {
isInputSource: isInputSource,
fromIndex: fromIndex,
toIndex: toIndex,
label: edge.label
};
connections.push(connection);
}
}
itembuffer.copyRuleNodes(x, y, nodes, connections);
}
function pasteRuleNodes(event) {
var offset = angular.element(vm.canvasControl.modelservice.getCanvasHtmlElement()).offset();
var x = Math.round(event.clientX - offset.left);
var y = Math.round(event.clientY - offset.top);
var ruleNodes = itembuffer.pasteRuleNodes(x, y, event);
if (ruleNodes) {
vm.modelservice.deselectAll();
var nodes = [];
for (var i=0;i<ruleNodes.nodes.length;i++) {
var node = ruleNodes.nodes[i];
node.id = 'rule-chain-node-' + vm.nextNodeID++;
var component = node.component;
if (component.configurationDescriptor.nodeDefinition.inEnabled) {
node.connectors.push(
{
type: flowchartConstants.leftConnectorType,
id: vm.nextConnectorID++
}
);
}
if (component.configurationDescriptor.nodeDefinition.outEnabled) {
node.connectors.push(
{
type: flowchartConstants.rightConnectorType,
id: vm.nextConnectorID++
}
);
}
nodes.push(node);
vm.ruleChainModel.nodes.push(node);
vm.modelservice.nodes.select(node);
}
for (i=0;i<ruleNodes.connections.length;i++) {
var connection = ruleNodes.connections[i];
var sourceNode = nodes[connection.fromIndex];
var destNode = nodes[connection.toIndex];
if ( (connection.isInputSource || sourceNode) && destNode ) {
var source, destination;
if (connection.isInputSource) {
source = vm.inputConnectorId;
} else {
var sourceConnectors = vm.modelservice.nodes.getConnectorsByType(sourceNode, flowchartConstants.rightConnectorType);
if (sourceConnectors && sourceConnectors.length) {
source = sourceConnectors[0].id;
}
}
var destConnectors = vm.modelservice.nodes.getConnectorsByType(destNode, flowchartConstants.leftConnectorType);
if (destConnectors && destConnectors.length) {
destination = destConnectors[0].id;
}
if (source && destination) {
var edge = {
source: source,
destination: destination,
label: connection.label
};
vm.ruleChainModel.edges.push(edge);
vm.modelservice.edges.select(edge);
}
}
}
if (vm.canvasControl.adjustCanvasSize) {
vm.canvasControl.adjustCanvasSize();
}
updateRuleNodesHighlight();
validate();
}
}
loadRuleChainLibrary(ruleNodeComponents, true); loadRuleChainLibrary(ruleNodeComponents, true);
$scope.$watch('vm.ruleNodeSearch', $scope.$watch('vm.ruleNodeSearch',

View File

@ -108,11 +108,14 @@
#tb-rule-chain-context-menu { #tb-rule-chain-context-menu {
padding-top: 0px; padding-top: 0px;
border-radius: 8px; border-radius: 8px;
max-height: 404px;
.tb-context-menu-header { .tb-context-menu-header {
padding: 8px 5px 5px; padding: 8px 5px 5px;
font-size: 14px; font-size: 14px;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
height: 30px;
min-height: 30px;
&.tb-rulechain { &.tb-rulechain {
background-color: #aac7e4; background-color: #aac7e4;
} }

View File

@ -122,7 +122,8 @@
</div> </div>
</div> </div>
<div ng-repeat="item in vm.contextInfo.items"> <div ng-repeat="item in vm.contextInfo.items">
<md-menu-item> <md-divider ng-if="item.divider"></md-divider>
<md-menu-item ng-if="!item.divider">
<md-button ng-disabled="!item.enabled" ng-click="item.action(vm.contextMenuEvent)"> <md-button ng-disabled="!item.enabled" ng-click="item.action(vm.contextMenuEvent)">
<md-icon ng-if="item.icon" md-menu-align-target aria-label="{{ item.value | translate }}" class="material-icons">{{item.icon}}</md-icon> <md-icon ng-if="item.icon" md-menu-align-target aria-label="{{ item.value | translate }}" class="material-icons">{{item.icon}}</md-icon>
<span translate>{{item.value}}</span> <span translate>{{item.value}}</span>

View File

@ -24,10 +24,11 @@ export default angular.module('thingsboard.itembuffer', [angularStorage])
.name; .name;
/*@ngInject*/ /*@ngInject*/
function ItemBuffer($q, bufferStore, types, utils, dashboardUtils) { function ItemBuffer($q, bufferStore, types, utils, dashboardUtils, ruleChainService) {
const WIDGET_ITEM = "widget_item"; const WIDGET_ITEM = "widget_item";
const WIDGET_REFERENCE = "widget_reference"; const WIDGET_REFERENCE = "widget_reference";
const RULE_NODES = "rule_nodes";
var service = { var service = {
prepareWidgetItem: prepareWidgetItem, prepareWidgetItem: prepareWidgetItem,
@ -37,7 +38,10 @@ function ItemBuffer($q, bufferStore, types, utils, dashboardUtils) {
canPasteWidgetReference: canPasteWidgetReference, canPasteWidgetReference: canPasteWidgetReference,
pasteWidget: pasteWidget, pasteWidget: pasteWidget,
pasteWidgetReference: pasteWidgetReference, pasteWidgetReference: pasteWidgetReference,
addWidgetToDashboard: addWidgetToDashboard addWidgetToDashboard: addWidgetToDashboard,
copyRuleNodes: copyRuleNodes,
hasRuleNodes: hasRuleNodes,
pasteRuleNodes: pasteRuleNodes
} }
return service; return service;
@ -151,6 +155,69 @@ function ItemBuffer($q, bufferStore, types, utils, dashboardUtils) {
}; };
} }
function copyRuleNodes(x, y, nodes, connections) {
var ruleNodes = {
nodes: [],
connections: [],
originX: x,
originY: y
};
for (var i=0;i<nodes.length;i++) {
var origNode = nodes[i];
var node = {
additionalInfo: origNode.additionalInfo,
configuration: origNode.configuration,
debugMode: origNode.debugMode,
x: origNode.x,
y: origNode.y,
name: origNode.name,
componentClazz: origNode.component.clazz,
};
if (origNode.targetRuleChainId) {
node.targetRuleChainId = origNode.targetRuleChainId;
}
if (origNode.error) {
node.error = origNode.error;
}
ruleNodes.nodes.push(node);
}
for (i=0;i<connections.length;i++) {
var connection = connections[i];
ruleNodes.connections.push(connection);
}
bufferStore.set(RULE_NODES, angular.toJson(ruleNodes));
}
function hasRuleNodes() {
return bufferStore.get(RULE_NODES);
}
function pasteRuleNodes(x, y) {
var ruleNodesJson = bufferStore.get(RULE_NODES);
if (ruleNodesJson) {
var ruleNodes = angular.fromJson(ruleNodesJson);
var deltaX = x - ruleNodes.originX;
var deltaY = y - ruleNodes.originY;
for (var i=0;i<ruleNodes.nodes.length;i++) {
var node = ruleNodes.nodes[i];
var component = ruleChainService.getRuleNodeComponentByClazz(node.componentClazz);
if (component) {
delete node.componentClazz;
node.component = component;
node.nodeClass = types.ruleNodeType[component.type].nodeClass;
node.icon = types.ruleNodeType[component.type].icon;
node.connectors = [];
node.x = Math.round(node.x + deltaX);
node.y = Math.round(node.y + deltaY);
} else {
return null;
}
}
return ruleNodes;
}
return null;
}
function copyWidget(dashboard, sourceState, sourceLayout, widget) { function copyWidget(dashboard, sourceState, sourceLayout, widget) {
var widgetItem = prepareWidgetItem(dashboard, sourceState, sourceLayout, widget); var widgetItem = prepareWidgetItem(dashboard, sourceState, sourceLayout, widget);
bufferStore.set(WIDGET_ITEM, angular.toJson(widgetItem)); bufferStore.set(WIDGET_ITEM, angular.toJson(widgetItem));