Commit b13d666f1b2bfa8eaab3e7a325f18f0bac7ba51e

Authored by Igor Kulikov
1 parent 737f2cc9

Rule Chain UI: Copy/Paste support.

... ... @@ -1208,6 +1208,7 @@ export default angular.module('thingsboard.locale', [])
1208 1208 "delete-selected-objects": "Delete selected nodes and connections",
1209 1209 "delete-selected": "Delete selected",
1210 1210 "select-all": "Select all",
  1211 + "copy-selected": "Copy selected",
1211 1212 "deselect-all": "Deselect all",
1212 1213 "rulenode-details": "Rule node details",
1213 1214 "debug-mode": "Debug mode",
... ...
... ... @@ -29,7 +29,7 @@ import addRuleNodeLinkTemplate from './add-link.tpl.html';
29 29
30 30 /*@ngInject*/
31 31 export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $timeout, $mdExpansionPanel, $window, $document, $mdDialog,
32   - $filter, $translate, hotkeys, types, ruleChainService, Modelfactory, flowchartConstants,
  32 + $filter, $translate, hotkeys, types, ruleChainService, itembuffer, Modelfactory, flowchartConstants,
33 33 ruleChain, ruleChainMetaData, ruleNodeComponents) {
34 34
35 35 var vm = this;
... ... @@ -140,6 +140,22 @@ export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $time
140 140 subtitle: $translate.instant('rulechain.rulechain')
141 141 };
142 142 contextInfo.items = [];
  143 + contextInfo.items.push(
  144 + {
  145 + action: function ($event) {
  146 + pasteRuleNodes($event);
  147 + },
  148 + enabled: itembuffer.hasRuleNodes(),
  149 + value: "action.paste",
  150 + icon: "content_paste",
  151 + shortcut: "M-V"
  152 + }
  153 + );
  154 + contextInfo.items.push(
  155 + {
  156 + divider: true
  157 + }
  158 + );
143 159 if (objectsSelected()) {
144 160 contextInfo.items.push(
145 161 {
... ... @@ -154,6 +170,17 @@ export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $time
154 170 );
155 171 contextInfo.items.push(
156 172 {
  173 + action: function (event) {
  174 + copyRuleNodes(event);
  175 + },
  176 + enabled: true,
  177 + value: "rulenode.copy-selected",
  178 + icon: "content_copy",
  179 + shortcut: "M-C"
  180 + }
  181 + );
  182 + contextInfo.items.push(
  183 + {
157 184 action: function () {
158 185 vm.modelservice.deleteSelected();
159 186 },
... ... @@ -178,6 +205,11 @@ export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $time
178 205 }
179 206 contextInfo.items.push(
180 207 {
  208 + divider: true
  209 + }
  210 + );
  211 + contextInfo.items.push(
  212 + {
181 213 action: function () {
182 214 vm.saveRuleChain();
183 215 },
... ... @@ -222,6 +254,16 @@ export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $time
222 254 );
223 255 contextInfo.items.push(
224 256 {
  257 + action: function (event) {
  258 + copyNode(event, node);
  259 + },
  260 + enabled: true,
  261 + value: "action.copy",
  262 + icon: "content_copy"
  263 + }
  264 + );
  265 + contextInfo.items.push(
  266 + {
225 267 action: function () {
226 268 vm.canvasControl.modelservice.nodes.delete(node);
227 269 },
... ... @@ -526,7 +568,7 @@ export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $time
526 568 if (destNode.component.type == types.ruleNodeType.RULE_CHAIN.value) {
527 569 deferred.reject();
528 570 } else {
529   - var res = $filter('filter')(vm.ruleChainModel.edges, {source: vm.inputConnectorId});
  571 + var res = $filter('filter')(vm.ruleChainModel.edges, {source: vm.inputConnectorId}, true);
530 572 if (res && res.length) {
531 573 vm.modelservice.edges.delete(res[0]);
532 574 }
... ... @@ -582,6 +624,112 @@ export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $time
582 624 }
583 625 }
584 626
  627 + function copyNode(event, node) {
  628 + var offset = angular.element(vm.canvasControl.modelservice.getCanvasHtmlElement()).offset();
  629 + var x = Math.round(event.clientX - offset.left);
  630 + var y = Math.round(event.clientY - offset.top);
  631 + itembuffer.copyRuleNodes(x, y, [node], []);
  632 + }
  633 +
  634 + function copyRuleNodes(event) {
  635 + var offset = angular.element(vm.canvasControl.modelservice.getCanvasHtmlElement()).offset();
  636 + var x = Math.round(event.clientX - offset.left);
  637 + var y = Math.round(event.clientY - offset.top);
  638 + var nodes = vm.modelservice.nodes.getSelectedNodes();
  639 + var edges = vm.modelservice.edges.getSelectedEdges();
  640 + var connections = [];
  641 + for (var i=0;i<edges.length;i++) {
  642 + var edge = edges[i];
  643 + var sourceNode = vm.modelservice.nodes.getNodeByConnectorId(edge.source);
  644 + var destNode = vm.modelservice.nodes.getNodeByConnectorId(edge.destination);
  645 + var isInputSource = sourceNode.component.type == types.ruleNodeType.INPUT.value;
  646 + var fromIndex = nodes.indexOf(sourceNode);
  647 + var toIndex = nodes.indexOf(destNode);
  648 + if ( (isInputSource || fromIndex > -1) && toIndex > -1 ) {
  649 + var connection = {
  650 + isInputSource: isInputSource,
  651 + fromIndex: fromIndex,
  652 + toIndex: toIndex,
  653 + label: edge.label
  654 + };
  655 + connections.push(connection);
  656 + }
  657 + }
  658 + itembuffer.copyRuleNodes(x, y, nodes, connections);
  659 + }
  660 +
  661 + function pasteRuleNodes(event) {
  662 + var offset = angular.element(vm.canvasControl.modelservice.getCanvasHtmlElement()).offset();
  663 + var x = Math.round(event.clientX - offset.left);
  664 + var y = Math.round(event.clientY - offset.top);
  665 + var ruleNodes = itembuffer.pasteRuleNodes(x, y, event);
  666 + if (ruleNodes) {
  667 + vm.modelservice.deselectAll();
  668 + var nodes = [];
  669 + for (var i=0;i<ruleNodes.nodes.length;i++) {
  670 + var node = ruleNodes.nodes[i];
  671 + node.id = 'rule-chain-node-' + vm.nextNodeID++;
  672 + var component = node.component;
  673 + if (component.configurationDescriptor.nodeDefinition.inEnabled) {
  674 + node.connectors.push(
  675 + {
  676 + type: flowchartConstants.leftConnectorType,
  677 + id: vm.nextConnectorID++
  678 + }
  679 + );
  680 + }
  681 + if (component.configurationDescriptor.nodeDefinition.outEnabled) {
  682 + node.connectors.push(
  683 + {
  684 + type: flowchartConstants.rightConnectorType,
  685 + id: vm.nextConnectorID++
  686 + }
  687 + );
  688 + }
  689 + nodes.push(node);
  690 + vm.ruleChainModel.nodes.push(node);
  691 + vm.modelservice.nodes.select(node);
  692 + }
  693 + for (i=0;i<ruleNodes.connections.length;i++) {
  694 + var connection = ruleNodes.connections[i];
  695 + var sourceNode = nodes[connection.fromIndex];
  696 + var destNode = nodes[connection.toIndex];
  697 + if ( (connection.isInputSource || sourceNode) && destNode ) {
  698 + var source, destination;
  699 + if (connection.isInputSource) {
  700 + source = vm.inputConnectorId;
  701 + } else {
  702 + var sourceConnectors = vm.modelservice.nodes.getConnectorsByType(sourceNode, flowchartConstants.rightConnectorType);
  703 + if (sourceConnectors && sourceConnectors.length) {
  704 + source = sourceConnectors[0].id;
  705 + }
  706 + }
  707 + var destConnectors = vm.modelservice.nodes.getConnectorsByType(destNode, flowchartConstants.leftConnectorType);
  708 + if (destConnectors && destConnectors.length) {
  709 + destination = destConnectors[0].id;
  710 + }
  711 + if (source && destination) {
  712 + var edge = {
  713 + source: source,
  714 + destination: destination,
  715 + label: connection.label
  716 + };
  717 + vm.ruleChainModel.edges.push(edge);
  718 + vm.modelservice.edges.select(edge);
  719 + }
  720 + }
  721 + }
  722 +
  723 + if (vm.canvasControl.adjustCanvasSize) {
  724 + vm.canvasControl.adjustCanvasSize();
  725 + }
  726 +
  727 + updateRuleNodesHighlight();
  728 +
  729 + validate();
  730 + }
  731 + }
  732 +
585 733 loadRuleChainLibrary(ruleNodeComponents, true);
586 734
587 735 $scope.$watch('vm.ruleNodeSearch',
... ...
... ... @@ -108,11 +108,14 @@
108 108 #tb-rule-chain-context-menu {
109 109 padding-top: 0px;
110 110 border-radius: 8px;
  111 + max-height: 404px;
111 112 .tb-context-menu-header {
112 113 padding: 8px 5px 5px;
113 114 font-size: 14px;
114 115 display: flex;
115 116 flex-direction: row;
  117 + height: 30px;
  118 + min-height: 30px;
116 119 &.tb-rulechain {
117 120 background-color: #aac7e4;
118 121 }
... ...
... ... @@ -122,7 +122,8 @@
122 122 </div>
123 123 </div>
124 124 <div ng-repeat="item in vm.contextInfo.items">
125   - <md-menu-item>
  125 + <md-divider ng-if="item.divider"></md-divider>
  126 + <md-menu-item ng-if="!item.divider">
126 127 <md-button ng-disabled="!item.enabled" ng-click="item.action(vm.contextMenuEvent)">
127 128 <md-icon ng-if="item.icon" md-menu-align-target aria-label="{{ item.value | translate }}" class="material-icons">{{item.icon}}</md-icon>
128 129 <span translate>{{item.value}}</span>
... ...
... ... @@ -24,10 +24,11 @@ export default angular.module('thingsboard.itembuffer', [angularStorage])
24 24 .name;
25 25
26 26 /*@ngInject*/
27   -function ItemBuffer($q, bufferStore, types, utils, dashboardUtils) {
  27 +function ItemBuffer($q, bufferStore, types, utils, dashboardUtils, ruleChainService) {
28 28
29 29 const WIDGET_ITEM = "widget_item";
30 30 const WIDGET_REFERENCE = "widget_reference";
  31 + const RULE_NODES = "rule_nodes";
31 32
32 33 var service = {
33 34 prepareWidgetItem: prepareWidgetItem,
... ... @@ -37,7 +38,10 @@ function ItemBuffer($q, bufferStore, types, utils, dashboardUtils) {
37 38 canPasteWidgetReference: canPasteWidgetReference,
38 39 pasteWidget: pasteWidget,
39 40 pasteWidgetReference: pasteWidgetReference,
40   - addWidgetToDashboard: addWidgetToDashboard
  41 + addWidgetToDashboard: addWidgetToDashboard,
  42 + copyRuleNodes: copyRuleNodes,
  43 + hasRuleNodes: hasRuleNodes,
  44 + pasteRuleNodes: pasteRuleNodes
41 45 }
42 46
43 47 return service;
... ... @@ -151,6 +155,69 @@ function ItemBuffer($q, bufferStore, types, utils, dashboardUtils) {
151 155 };
152 156 }
153 157
  158 + function copyRuleNodes(x, y, nodes, connections) {
  159 + var ruleNodes = {
  160 + nodes: [],
  161 + connections: [],
  162 + originX: x,
  163 + originY: y
  164 + };
  165 + for (var i=0;i<nodes.length;i++) {
  166 + var origNode = nodes[i];
  167 + var node = {
  168 + additionalInfo: origNode.additionalInfo,
  169 + configuration: origNode.configuration,
  170 + debugMode: origNode.debugMode,
  171 + x: origNode.x,
  172 + y: origNode.y,
  173 + name: origNode.name,
  174 + componentClazz: origNode.component.clazz,
  175 + };
  176 + if (origNode.targetRuleChainId) {
  177 + node.targetRuleChainId = origNode.targetRuleChainId;
  178 + }
  179 + if (origNode.error) {
  180 + node.error = origNode.error;
  181 + }
  182 + ruleNodes.nodes.push(node);
  183 + }
  184 + for (i=0;i<connections.length;i++) {
  185 + var connection = connections[i];
  186 + ruleNodes.connections.push(connection);
  187 + }
  188 + bufferStore.set(RULE_NODES, angular.toJson(ruleNodes));
  189 + }
  190 +
  191 + function hasRuleNodes() {
  192 + return bufferStore.get(RULE_NODES);
  193 + }
  194 +
  195 + function pasteRuleNodes(x, y) {
  196 + var ruleNodesJson = bufferStore.get(RULE_NODES);
  197 + if (ruleNodesJson) {
  198 + var ruleNodes = angular.fromJson(ruleNodesJson);
  199 + var deltaX = x - ruleNodes.originX;
  200 + var deltaY = y - ruleNodes.originY;
  201 + for (var i=0;i<ruleNodes.nodes.length;i++) {
  202 + var node = ruleNodes.nodes[i];
  203 + var component = ruleChainService.getRuleNodeComponentByClazz(node.componentClazz);
  204 + if (component) {
  205 + delete node.componentClazz;
  206 + node.component = component;
  207 + node.nodeClass = types.ruleNodeType[component.type].nodeClass;
  208 + node.icon = types.ruleNodeType[component.type].icon;
  209 + node.connectors = [];
  210 + node.x = Math.round(node.x + deltaX);
  211 + node.y = Math.round(node.y + deltaY);
  212 + } else {
  213 + return null;
  214 + }
  215 + }
  216 + return ruleNodes;
  217 + }
  218 + return null;
  219 + }
  220 +
154 221 function copyWidget(dashboard, sourceState, sourceLayout, widget) {
155 222 var widgetItem = prepareWidgetItem(dashboard, sourceState, sourceLayout, widget);
156 223 bufferStore.set(WIDGET_ITEM, angular.toJson(widgetItem));
... ...