thingsboard-memoizeit

Details

diff --git a/ui/src/app/components/confirm-on-exit.directive.js b/ui/src/app/components/confirm-on-exit.directive.js
index fe9a9bd..e04e110 100644
--- a/ui/src/app/components/confirm-on-exit.directive.js
+++ b/ui/src/app/components/confirm-on-exit.directive.js
@@ -18,17 +18,17 @@ export default angular.module('thingsboard.directives.confirmOnExit', [])
     .name;
 
 /*@ngInject*/
-function ConfirmOnExit($state, $mdDialog, $window, $filter, userService) {
+function ConfirmOnExit($state, $mdDialog, $window, $filter, $parse, userService) {
     return {
-        link: function ($scope) {
-
+        link: function ($scope, $element, $attributes) {
+            $scope.confirmForm = $scope.$eval($attributes.confirmForm);
             $window.onbeforeunload = function () {
-                if (userService.isAuthenticated() && (($scope.confirmForm && $scope.confirmForm.$dirty) || $scope.isDirty)) {
+                if (userService.isAuthenticated() && (($scope.confirmForm && $scope.confirmForm.$dirty) || $scope.$eval($attributes.isDirty))) {
                     return $filter('translate')('confirm-on-exit.message');
                 }
             }
             $scope.$on('$stateChangeStart', function (event, next, current, params) {
-                if (userService.isAuthenticated() && (($scope.confirmForm && $scope.confirmForm.$dirty) || $scope.isDirty)) {
+                if (userService.isAuthenticated() && (($scope.confirmForm && $scope.confirmForm.$dirty) || $scope.$eval($attributes.isDirty))) {
                     event.preventDefault();
                     var confirm = $mdDialog.confirm()
                         .title($filter('translate')('confirm-on-exit.title'))
@@ -40,7 +40,9 @@ function ConfirmOnExit($state, $mdDialog, $window, $filter, userService) {
                         if ($scope.confirmForm) {
                             $scope.confirmForm.$setPristine();
                         } else {
-                            $scope.isDirty = false;
+                            var remoteSetter = $parse($attributes.isDirty).assign;
+                            remoteSetter($scope, false);
+                            //$scope.isDirty = false;
                         }
                         $state.go(next.name, params);
                     }, function () {
@@ -48,9 +50,6 @@ function ConfirmOnExit($state, $mdDialog, $window, $filter, userService) {
                 }
             });
         },
-        scope: {
-            confirmForm: '=',
-            isDirty: '='
-        }
+        scope: false
     };
 }
\ No newline at end of file
diff --git a/ui/src/app/locale/locale.constant.js b/ui/src/app/locale/locale.constant.js
index cca2a11..5dce787 100644
--- a/ui/src/app/locale/locale.constant.js
+++ b/ui/src/app/locale/locale.constant.js
@@ -1177,6 +1177,9 @@ export default angular.module('thingsboard.locale', [])
                     "type": "Type",
                     "description": "Description",
                     "delete": "Delete rule node",
+                    "select-all": "Select all nodes and connections",
+                    "deselect-all": "Deselect all nodes and connections",
+                    "delete-selected-objects": "Delete selected nodes and connections",
                     "rulenode-details": "Rule node details",
                     "debug-mode": "Debug mode",
                     "configuration": "Configuration",
diff --git a/ui/src/app/rulechain/rulechain.controller.js b/ui/src/app/rulechain/rulechain.controller.js
index b792f13..4eba5b2 100644
--- a/ui/src/app/rulechain/rulechain.controller.js
+++ b/ui/src/app/rulechain/rulechain.controller.js
@@ -27,15 +27,10 @@ import addRuleNodeLinkTemplate from './add-link.tpl.html';
 
 /* eslint-enable import/no-unresolved, import/default */
 
-
-const deleteKeyCode = 46;
-const ctrlKeyCode = 17;
-const aKeyCode = 65;
-const escKeyCode = 27;
-
 /*@ngInject*/
 export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, $timeout, $mdExpansionPanel, $document, $mdDialog,
-                                    $filter, $translate, types, ruleChainService, Modelfactory, flowchartConstants, ruleChain, ruleChainMetaData) {
+                                    $filter, $translate, hotkeys, types, ruleChainService, Modelfactory, flowchartConstants,
+                                    ruleChain, ruleChainMetaData, ruleNodeComponents) {
 
     var vm = this;
 
@@ -76,39 +71,62 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
 
     vm.modelservice = Modelfactory(vm.ruleChainModel, vm.selectedObjects);
 
-    vm.ctrlDown = false;
-
     vm.saveRuleChain = saveRuleChain;
     vm.revertRuleChain = revertRuleChain;
 
-    vm.keyDown = function (evt) {
-        if (evt.keyCode === ctrlKeyCode) {
-            vm.ctrlDown = true;
-            evt.stopPropagation();
-            evt.preventDefault();
-        }
-    };
-
-    vm.keyUp = function (evt) {
-
-        if (evt.keyCode === deleteKeyCode) {
-            vm.modelservice.deleteSelected();
-        }
-
-        if (evt.keyCode == aKeyCode && vm.ctrlDown) {
-            vm.modelservice.selectAll();
-        }
+    vm.objectsSelected = objectsSelected;
+    vm.deleteSelected = deleteSelected;
 
-        if (evt.keyCode == escKeyCode) {
-            vm.modelservice.deselectAll();
-        }
+    initHotKeys();
 
-        if (evt.keyCode === ctrlKeyCode) {
-            vm.ctrlDown = false;
-            evt.stopPropagation();
-            evt.preventDefault();
-        }
-    };
+    function initHotKeys() {
+        hotkeys.bindTo($scope)
+            .add({
+                combo: 'ctrl+a',
+                description: $translate.instant('rulenode.select-all'),
+                allowIn: ['INPUT', 'SELECT', 'TEXTAREA'],
+                callback: function (event) {
+                    event.preventDefault();
+                    vm.modelservice.selectAll();
+                }
+            })
+            .add({
+                combo: 'esc',
+                description: $translate.instant('rulenode.deselect-all'),
+                allowIn: ['INPUT', 'SELECT', 'TEXTAREA'],
+                callback: function (event) {
+                    event.preventDefault();
+                    vm.modelservice.deselectAll();
+                }
+            })
+            .add({
+                combo: 'ctrl+s',
+                description: $translate.instant('action.apply'),
+                allowIn: ['INPUT', 'SELECT', 'TEXTAREA'],
+                callback: function (event) {
+                    event.preventDefault();
+                    vm.saveRuleChain();
+                }
+            })
+            .add({
+                combo: 'ctrl+z',
+                description: $translate.instant('action.decline-changes'),
+                allowIn: ['INPUT', 'SELECT', 'TEXTAREA'],
+                callback: function (event) {
+                    event.preventDefault();
+                    vm.revertRuleChain();
+                }
+            })
+            .add({
+                combo: 'del',
+                description: $translate.instant('rulenode.delete-selected-objects'),
+                allowIn: ['INPUT', 'SELECT', 'TEXTAREA'],
+                callback: function (event) {
+                    event.preventDefault();
+                    vm.modelservice.deleteSelected();
+                }
+            })
+    }
 
     vm.onEditRuleNodeClosed = function() {
         vm.editingRuleNode = null;
@@ -286,44 +304,40 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
     loadRuleChainLibrary();
 
     function loadRuleChainLibrary() {
-        ruleChainService.getRuleNodeComponents().then(
-            (ruleNodeComponents) => {
-                for (var i=0;i<ruleNodeComponents.length;i++) {
-                    var ruleNodeComponent = ruleNodeComponents[i];
-                    var componentType = ruleNodeComponent.type;
-                    var model = vm.ruleNodeTypesModel[componentType].model;
-                    var node = {
-                        id: model.nodes.length,
-                        component: ruleNodeComponent,
-                        name: '',
-                        nodeClass: vm.types.ruleNodeType[componentType].nodeClass,
-                        icon: vm.types.ruleNodeType[componentType].icon,
-                        x: 30,
-                        y: 10+50*model.nodes.length,
-                        connectors: []
-                    };
-                    if (ruleNodeComponent.configurationDescriptor.nodeDefinition.inEnabled) {
-                        node.connectors.push(
-                            {
-                                type: flowchartConstants.leftConnectorType,
-                                id: model.nodes.length * 2
-                            }
-                        );
+        for (var i=0;i<ruleNodeComponents.length;i++) {
+            var ruleNodeComponent = ruleNodeComponents[i];
+            var componentType = ruleNodeComponent.type;
+            var model = vm.ruleNodeTypesModel[componentType].model;
+            var node = {
+                id: model.nodes.length,
+                component: ruleNodeComponent,
+                name: '',
+                nodeClass: vm.types.ruleNodeType[componentType].nodeClass,
+                icon: vm.types.ruleNodeType[componentType].icon,
+                x: 30,
+                y: 10+50*model.nodes.length,
+                connectors: []
+            };
+            if (ruleNodeComponent.configurationDescriptor.nodeDefinition.inEnabled) {
+                node.connectors.push(
+                    {
+                        type: flowchartConstants.leftConnectorType,
+                        id: model.nodes.length * 2
                     }
-                    if (ruleNodeComponent.configurationDescriptor.nodeDefinition.outEnabled) {
-                        node.connectors.push(
-                            {
-                                type: flowchartConstants.rightConnectorType,
-                                id: model.nodes.length * 2 + 1
-                            }
-                        );
+                );
+            }
+            if (ruleNodeComponent.configurationDescriptor.nodeDefinition.outEnabled) {
+                node.connectors.push(
+                    {
+                        type: flowchartConstants.rightConnectorType,
+                        id: model.nodes.length * 2 + 1
                     }
-                    model.nodes.push(node);
-                }
-                vm.ruleChainLibraryLoaded = true;
-                prepareRuleChain();
+                );
             }
-        );
+            model.nodes.push(node);
+        }
+        vm.ruleChainLibraryLoaded = true;
+        prepareRuleChain();
     }
 
     function prepareRuleChain() {
@@ -632,6 +646,14 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
         });
     }
 
+    function objectsSelected() {
+        return vm.modelservice.nodes.getSelectedNodes().length > 0 ||
+            vm.modelservice.edges.getSelectedEdges().length > 0
+    }
+
+    function deleteSelected() {
+        vm.modelservice.deleteSelected();
+    }
 }
 
 /*@ngInject*/
diff --git a/ui/src/app/rulechain/rulechain.routes.js b/ui/src/app/rulechain/rulechain.routes.js
index 808661a..f9578ef 100644
--- a/ui/src/app/rulechain/rulechain.routes.js
+++ b/ui/src/app/rulechain/rulechain.routes.js
@@ -68,6 +68,11 @@ export default function RuleChainRoutes($stateProvider, NodeTemplatePathProvider
                 /*@ngInject*/
                     function($stateParams, ruleChainService) {
                         return ruleChainService.getRuleChainMetaData($stateParams.ruleChainId);
+                    },
+                ruleNodeComponents:
+                /*@ngInject*/
+                    function($stateParams, ruleChainService) {
+                        return ruleChainService.getRuleNodeComponents();
                     }
             },
             data: {
diff --git a/ui/src/app/rulechain/rulechain.scss b/ui/src/app/rulechain/rulechain.scss
index 26a5225..c9b2ced 100644
--- a/ui/src/app/rulechain/rulechain.scss
+++ b/ui/src/app/rulechain/rulechain.scss
@@ -75,6 +75,7 @@
   padding: 5px 10px;
   border-radius: 5px;
   background-color: #F15B26;
+  pointer-events: none;
   color: #333;
   border: solid 1px #777;
   font-size: 12px;
@@ -121,10 +122,6 @@
 .fc-node {
   z-index: 1;
   outline: none;
-  &.fc-hover, &.fc-selected {
-    -webkit-filter: brightness(70%);
-    filter: brightness(70%);
-  }
   &.fc-dragging {
     z-index: 10;
   }
@@ -132,6 +129,26 @@
     padding: 0 15px;
     text-align: center;
   }
+  .fc-node-overlay {
+    position: absolute;
+    pointer-events: none;
+    left: 0;
+    top: 0;
+    right: 0;
+    bottom: 0;
+    background-color: #000;
+    opacity: 0;
+  }
+  &.fc-hover {
+    .fc-node-overlay {
+      opacity: 0.25;
+    }
+  }
+  &.fc-selected {
+    .fc-node-overlay {
+      opacity: 0.25;
+    }
+  }
 }
 
 .fc-leftConnectors, .fc-rightConnectors {
@@ -170,17 +187,33 @@
   margin: 10px;
   border-radius: 5px;
   background-color: #ccc;
+  pointer-events: all;
 }
 
 .fc-connector.fc-hover {
   background-color: #000;
 }
 
+.fc-arrow-marker {
+  polygon {
+    stroke: gray;
+    fill: gray;
+  }
+}
+
+.fc-arrow-marker-selected {
+  polygon {
+    stroke: red;
+    fill: red;
+  }
+}
+
 .fc-edge {
   outline: none;
   stroke: gray;
   stroke-width: 4;
   fill: transparent;
+  transition: stroke-width .2s;
   &.fc-selected {
     stroke: red;
     stroke-width: 4;
@@ -229,24 +262,53 @@
   cursor: pointer;
 }
 
+.fc-noselect {
+  -webkit-touch-callout: none; /* iOS Safari */
+  -webkit-user-select: none; /* Safari */
+  -khtml-user-select: none; /* Konqueror HTML */
+  -moz-user-select: none; /* Firefox */
+  -ms-user-select: none; /* Internet Explorer/Edge */
+  user-select: none; /* Non-prefixed version, currently
+                                  supported by Chrome and Opera */
+}
+
 .fc-edge-label {
   position: absolute;
-  user-select: none;
-  pointer-events: none;
+  transition: transform .2s;
   opacity: 0.8;
+  &.ng-leave {
+    transition: 0s none;
+  }
+  &.fc-hover {
+    transform: scale(1.25);
+  }
+  &.fc-selected {
+    .fc-edge-label-text {
+      span {
+        border: solid red;
+        color: red;
+      }
+    }
+  }
+  .fc-nodedelete {
+    right: -13px;
+    top: -30px;
+  }
+  &:focus {
+    outline: 0;
+  }
 }
 
 .fc-edge-label-text {
   position: absolute;
-  left: 50%;
-  -webkit-transform: translateX(-50%);
-  transform: translateX(-50%);
+  -webkit-transform: translate(-50%, -50%);
+  transform: translate(-50%, -50%);
   white-space: nowrap;
   text-align: center;
   font-size: 14px;
   font-weight: 600;
-  top: 5px;
   span {
+    cursor: default;
     border: solid 2px #003a79;
     border-radius: 10px;
     color: #003a79;
@@ -255,6 +317,13 @@
   }
 }
 
+.fc-select-rectangle {
+  border: 2px dashed #5262ff;
+  position: absolute;
+  background: rgba(20,125,255,0.1);
+  z-index: 2;
+}
+
 @keyframes dash {
   from {
     stroke-dashoffset: 500;
diff --git a/ui/src/app/rulechain/rulechain.tpl.html b/ui/src/app/rulechain/rulechain.tpl.html
index 9f1141e..d23f920 100644
--- a/ui/src/app/rulechain/rulechain.tpl.html
+++ b/ui/src/app/rulechain/rulechain.tpl.html
@@ -16,8 +16,10 @@
 
 -->
 
-<md-content flex tb-expand-fullscreen
-            expand-tooltip-direction="bottom" layout="column" class="tb-rulechain">
+<md-content flex tb-expand-fullscreen tb-confirm-on-exit is-dirty="vm.isDirty"
+            expand-tooltip-direction="bottom" layout="column" class="tb-rulechain"
+            ng-keydown="vm.keyDown($event)"
+            ng-keyup="vm.keyUp($event)">
     <section class="tb-rulechain-container" flex layout="column">
         <div class="tb-rulechain-layout" flex layout="row">
             <div class="tb-rulechain-library">
@@ -50,8 +52,6 @@
             </div>
             <div flex class="tb-rulechain-graph">
                 <fc-canvas id="tb-rulchain-canvas"
-                           ng-keydown="vm.keyDown($event)"
-                           ng-keyup="vm.keyUp($event)"
                            model="vm.ruleChainModel"
                            selected-objects="vm.selectedObjects"
                            edge-style="curved"
@@ -112,6 +112,13 @@
         </tb-details-sidenav>
     </section>
     <section layout="row" layout-wrap class="tb-footer-buttons md-fab" layout-align="start end">
+        <md-button ng-disabled="$root.loading" ng-show="vm.objectsSelected()" class="tb-btn-footer md-accent md-hue-2 md-fab"
+                   ng-click="vm.deleteSelected()" aria-label="{{ 'action.delete' | translate }}">
+            <md-tooltip md-direction="top">
+                {{ 'rulenode.delete-selected-objects' | translate }}
+            </md-tooltip>
+            <ng-md-icon icon="delete"></ng-md-icon>
+        </md-button>
         <md-button ng-disabled="$root.loading || !vm.isDirty"
                    class="tb-btn-footer md-accent md-hue-2 md-fab"
                    aria-label="{{ 'action.apply' | translate }}"
diff --git a/ui/src/app/rulechain/rulenode.tpl.html b/ui/src/app/rulechain/rulenode.tpl.html
index ffc8a0f..55ee3d3 100644
--- a/ui/src/app/rulechain/rulenode.tpl.html
+++ b/ui/src/app/rulechain/rulenode.tpl.html
@@ -22,6 +22,7 @@
         ng-mousedown="callbacks.mouseDown($event, node)"
         ng-mouseenter="callbacks.mouseEnter($event, node)"
         ng-mouseleave="callbacks.mouseLeave($event, node)">
+    <div class="{{flowchartConstants.nodeOverlayClass}}"></div>
     <div class="tb-rule-node {{node.nodeClass}}">
         <md-icon aria-label="node-type-icon" flex="15"
                  class="material-icons">{{node.icon}}</md-icon>