Commit f081b656d076f43afef1b43bdb393c1b31b943ab

Authored by Igor Kulikov
1 parent 1f58c1b0

Multiple labels support.

@@ -32,6 +32,7 @@ function RuleChainService($http, $q, $filter, $ocLazyLoad, $translate, types, co @@ -32,6 +32,7 @@ function RuleChainService($http, $q, $filter, $ocLazyLoad, $translate, types, co
32 getRuleNodeComponents: getRuleNodeComponents, 32 getRuleNodeComponents: getRuleNodeComponents,
33 getRuleNodeComponentByClazz: getRuleNodeComponentByClazz, 33 getRuleNodeComponentByClazz: getRuleNodeComponentByClazz,
34 getRuleNodeSupportedLinks: getRuleNodeSupportedLinks, 34 getRuleNodeSupportedLinks: getRuleNodeSupportedLinks,
  35 + ruleNodeAllowCustomLinks: ruleNodeAllowCustomLinks,
35 resolveTargetRuleChains: resolveTargetRuleChains, 36 resolveTargetRuleChains: resolveTargetRuleChains,
36 testScript: testScript, 37 testScript: testScript,
37 getLatestRuleNodeDebugInput: getLatestRuleNodeDebugInput 38 getLatestRuleNodeDebugInput: getLatestRuleNodeDebugInput
@@ -127,21 +128,21 @@ function RuleChainService($http, $q, $filter, $ocLazyLoad, $translate, types, co @@ -127,21 +128,21 @@ function RuleChainService($http, $q, $filter, $ocLazyLoad, $translate, types, co
127 128
128 function getRuleNodeSupportedLinks(component) { 129 function getRuleNodeSupportedLinks(component) {
129 var relationTypes = component.configurationDescriptor.nodeDefinition.relationTypes; 130 var relationTypes = component.configurationDescriptor.nodeDefinition.relationTypes;
130 - var customRelations = component.configurationDescriptor.nodeDefinition.customRelations;  
131 - var linkLabels = []; 131 + var linkLabels = {};
132 for (var i=0;i<relationTypes.length;i++) { 132 for (var i=0;i<relationTypes.length;i++) {
133 - linkLabels.push({  
134 - name: relationTypes[i], custom: false  
135 - });  
136 - }  
137 - if (customRelations) {  
138 - linkLabels.push(  
139 - { name: 'Custom', custom: true }  
140 - ); 133 + var label = relationTypes[i];
  134 + linkLabels[label] = {
  135 + name: label,
  136 + value: label
  137 + };
141 } 138 }
142 return linkLabels; 139 return linkLabels;
143 } 140 }
144 141
  142 + function ruleNodeAllowCustomLinks(component) {
  143 + return component.configurationDescriptor.nodeDefinition.customRelations;
  144 + }
  145 +
145 function getRuleNodeComponents() { 146 function getRuleNodeComponents() {
146 var deferred = $q.defer(); 147 var deferred = $q.defer();
147 if (ruleNodeComponents) { 148 if (ruleNodeComponents) {
@@ -226,7 +227,10 @@ function RuleChainService($http, $q, $filter, $ocLazyLoad, $translate, types, co @@ -226,7 +227,10 @@ function RuleChainService($http, $q, $filter, $ocLazyLoad, $translate, types, co
226 if (res && res.length) { 227 if (res && res.length) {
227 return res[0]; 228 return res[0];
228 } 229 }
229 - return null; 230 + var unknownComponent = angular.copy(types.unknownNodeComponent);
  231 + unknownComponent.clazz = clazz;
  232 + unknownComponent.configurationDescriptor.nodeDefinition.details = "Unknown Rule Node class: " + clazz;
  233 + return unknownComponent;
230 } 234 }
231 235
232 function resolveTargetRuleChains(ruleChainConnections) { 236 function resolveTargetRuleChains(ruleChainConnections) {
@@ -510,6 +510,22 @@ export default angular.module('thingsboard.types', []) @@ -510,6 +510,22 @@ export default angular.module('thingsboard.types', [])
510 } 510 }
511 } 511 }
512 }, 512 },
  513 + unknownNodeComponent: {
  514 + type: 'UNKNOWN',
  515 + name: 'unknown',
  516 + clazz: 'tb.internal.Unknown',
  517 + configurationDescriptor: {
  518 + nodeDefinition: {
  519 + description: "",
  520 + details: "",
  521 + inEnabled: true,
  522 + outEnabled: true,
  523 + relationTypes: [],
  524 + customRelations: false,
  525 + defaultConfiguration: {}
  526 + }
  527 + }
  528 + },
513 inputNodeComponent: { 529 inputNodeComponent: {
514 type: 'INPUT', 530 type: 'INPUT',
515 name: 'Input', 531 name: 'Input',
@@ -565,6 +581,13 @@ export default angular.module('thingsboard.types', []) @@ -565,6 +581,13 @@ export default angular.module('thingsboard.types', [])
565 nodeClass: "tb-input-type", 581 nodeClass: "tb-input-type",
566 icon: "input", 582 icon: "input",
567 special: true 583 special: true
  584 + },
  585 + UNKNOWN: {
  586 + value: "UNKNOWN",
  587 + name: "rulenode.type-unknown",
  588 + details: "rulenode.type-unknown-details",
  589 + nodeClass: "tb-unknown-type",
  590 + icon: "help_outline"
568 } 591 }
569 }, 592 },
570 valueType: { 593 valueType: {
@@ -16,8 +16,8 @@ @@ -16,8 +16,8 @@
16 16
17 --> 17 -->
18 <div flex layout="column" style="margin-top: -10px;"> 18 <div flex layout="column" style="margin-top: -10px;">
19 - <div flex>{{vm.item.additionalInfo.description}}</div>  
20 - <div flex style="text-transform: uppercase; padding-bottom: 10px;">{{vm.item.type}}</div>  
21 - <div class="tb-small" ng-show="vm.isAssignedToCustomer()">{{'device.assignedToCustomer' | translate}} '{{vm.item.assignedCustomer.title}}'</div>  
22 - <div class="tb-small" ng-show="vm.isPublic()">{{'device.public' | translate}}</div> 19 + <div style="text-transform: uppercase; padding-bottom: 5px;">{{vm.item.type}}</div>
  20 + <div class="tb-card-description">{{vm.item.additionalInfo.description}}</div>
  21 + <div style="padding-top: 5px;" class="tb-small" ng-show="vm.isAssignedToCustomer()">{{'device.assignedToCustomer' | translate}} '{{vm.item.assignedCustomer.title}}'</div>
  22 + <div style="padding-top: 5px;" class="tb-small" ng-show="vm.isPublic()">{{'device.public' | translate}}</div>
23 </div> 23 </div>
@@ -1157,6 +1157,11 @@ export default angular.module('thingsboard.locale', []) @@ -1157,6 +1157,11 @@ export default angular.module('thingsboard.locale', [])
1157 "link-label-required": "Link label is required.", 1157 "link-label-required": "Link label is required.",
1158 "custom-link-label": "Custom link label", 1158 "custom-link-label": "Custom link label",
1159 "custom-link-label-required": "Custom link label is required.", 1159 "custom-link-label-required": "Custom link label is required.",
  1160 + "link-labels": "Link labels",
  1161 + "link-labels-required": "Link labels is required.",
  1162 + "no-link-labels-found": "No link labels found",
  1163 + "no-link-label-matching": "'{{label}}' not found.",
  1164 + "create-new-link-label": "Create a new one!",
1160 "type-filter": "Filter", 1165 "type-filter": "Filter",
1161 "type-filter-details": "Filter incoming messages with configured conditions", 1166 "type-filter-details": "Filter incoming messages with configured conditions",
1162 "type-enrichment": "Enrichment", 1167 "type-enrichment": "Enrichment",
@@ -1171,6 +1176,8 @@ export default angular.module('thingsboard.locale', []) @@ -1171,6 +1176,8 @@ export default angular.module('thingsboard.locale', [])
1171 "type-rule-chain-details": "Forwards incoming messages to specified Rule Chain", 1176 "type-rule-chain-details": "Forwards incoming messages to specified Rule Chain",
1172 "type-input": "Input", 1177 "type-input": "Input",
1173 "type-input-details": "Logical input of Rule Chain, forwards incoming messages to next related Rule Node", 1178 "type-input-details": "Logical input of Rule Chain, forwards incoming messages to next related Rule Node",
  1179 + "type-unknown": "Unknown",
  1180 + "type-unknown-details": "Unresolved Rule Node",
1174 "directive-is-not-loaded": "Defined configuration directive '{{directiveName}}' is not available.", 1181 "directive-is-not-loaded": "Defined configuration directive '{{directiveName}}' is not available.",
1175 "ui-resources-load-error": "Failed to load configuration ui resources.", 1182 "ui-resources-load-error": "Failed to load configuration ui resources.",
1176 "invalid-target-rulechain": "Unable to resolve target rule chain!", 1183 "invalid-target-rulechain": "Unable to resolve target rule chain!",
@@ -31,7 +31,7 @@ @@ -31,7 +31,7 @@
31 <span style="min-height: 5px;" flex="" ng-show="!$root.loading"></span> 31 <span style="min-height: 5px;" flex="" ng-show="!$root.loading"></span>
32 <md-dialog-content> 32 <md-dialog-content>
33 <div class="md-dialog-content"> 33 <div class="md-dialog-content">
34 - <tb-rule-node-link link="vm.link" labels="vm.labels" is-edit="true" the-form="theForm"></tb-rule-node-link> 34 + <tb-rule-node-link ng-model="vm.link" allowed-labels="vm.labels" is-edit="true" allow-custom="vm.allowCustomLabels"></tb-rule-node-link>
35 </div> 35 </div>
36 </md-dialog-content> 36 </md-dialog-content>
37 <md-dialog-actions layout="row"> 37 <md-dialog-actions layout="row">
@@ -17,7 +17,46 @@ @@ -17,7 +17,46 @@
17 --> 17 -->
18 <md-content class="md-padding tb-link" layout="column"> 18 <md-content class="md-padding tb-link" layout="column">
19 <fieldset ng-disabled="$root.loading || !isEdit || isReadOnly"> 19 <fieldset ng-disabled="$root.loading || !isEdit || isReadOnly">
20 - <md-input-container class="md-block"> 20 + <label translate class="tb-title no-padding" ng-class="{'tb-required': required}">rulenode.link-labels</label>
  21 + <md-chips id="link_label_chips"
  22 + ng-required="true"
  23 + readonly="$root.loading || !isEdit || isReadOnly"
  24 + ng-model="labels" md-autocomplete-snap
  25 + md-transform-chip="transformLinkLabelChip($chip)"
  26 + md-require-match="!allowCustom">
  27 + <md-autocomplete
  28 + id="link_label"
  29 + md-no-cache="true"
  30 + md-selected-item="selectedLabel"
  31 + md-search-text="labelSearchText"
  32 + md-items="item in labelsSearch(labelSearchText)"
  33 + md-item-text="item.name"
  34 + md-min-length="0"
  35 + placeholder="{{'rulenode.link-label' | translate }}"
  36 + md-menu-class="tb-link-label-autocomplete">
  37 + <span md-highlight-text="labelSearchText" md-highlight-flags="^i">{{item}}</span>
  38 + <md-not-found>
  39 + <div class="tb-not-found">
  40 + <div class="tb-no-entries" ng-if="!labelSearchText || !labelSearchText.length">
  41 + <span translate>rulenode.no-link-labels-found</span>
  42 + </div>
  43 + <div ng-if="labelSearchText && labelSearchText.length">
  44 + <span translate translate-values='{ label: "{{labelSearchText | truncate:true:6:&apos;...&apos;}}" }'>rulenode.no-link-label-matching</span>
  45 + <span ng-if="allowCustom">
  46 + <a translate ng-click="createLinkLabel($event, '#link_label_chips')">rulenode.create-new-link-label</a>
  47 + </span>
  48 + </div>
  49 + </div>
  50 + </md-not-found>
  51 + </md-autocomplete>
  52 + <md-chip-template>
  53 + <span>{{$chip.name}}</span>
  54 + </md-chip-template>
  55 + </md-chips>
  56 + <div class="tb-error-messages" ng-messages="ngModelCtrl.$error" role="alert">
  57 + <div translate ng-message="linkLabels" class="tb-error-message">rulenode.link-labels-required</div>
  58 + </div>
  59 + <!--md-input-container class="md-block">
21 <label translate>rulenode.link-label</label> 60 <label translate>rulenode.link-label</label>
22 <md-select ng-model="selectedLabel" ng-change="selectedLabelChanged()"> 61 <md-select ng-model="selectedLabel" ng-change="selectedLabelChanged()">
23 <md-option ng-repeat="label in labels" ng-value="label"> 62 <md-option ng-repeat="label in labels" ng-value="label">
@@ -34,6 +73,6 @@ @@ -34,6 +73,6 @@
34 <div ng-messages="theForm.customLinkLabel.$error"> 73 <div ng-messages="theForm.customLinkLabel.$error">
35 <div translate ng-message="required">rulenode.custom-link-label-required</div> 74 <div translate ng-message="required">rulenode.custom-link-label-required</div>
36 </div> 75 </div>
37 - </md-input-container> 76 + </md-input-container-->
38 </fieldset> 77 </fieldset>
39 </md-content> 78 </md-content>
@@ -14,6 +14,8 @@ @@ -14,6 +14,8 @@
14 * limitations under the License. 14 * limitations under the License.
15 */ 15 */
16 16
  17 +import './link.scss';
  18 +
17 /* eslint-disable import/no-unresolved, import/default */ 19 /* eslint-disable import/no-unresolved, import/default */
18 20
19 import linkFieldsetTemplate from './link-fieldset.tpl.html'; 21 import linkFieldsetTemplate from './link-fieldset.tpl.html';
@@ -22,13 +24,18 @@ import linkFieldsetTemplate from './link-fieldset.tpl.html'; @@ -22,13 +24,18 @@ import linkFieldsetTemplate from './link-fieldset.tpl.html';
22 24
23 /*@ngInject*/ 25 /*@ngInject*/
24 export default function LinkDirective($compile, $templateCache, $filter) { 26 export default function LinkDirective($compile, $templateCache, $filter) {
25 - var linker = function (scope, element) { 27 + var linker = function (scope, element, attrs, ngModelCtrl) {
26 var template = $templateCache.get(linkFieldsetTemplate); 28 var template = $templateCache.get(linkFieldsetTemplate);
27 element.html(template); 29 element.html(template);
28 30
29 scope.selectedLabel = null; 31 scope.selectedLabel = null;
  32 + scope.labelSearchText = null;
  33 +
  34 + scope.ngModelCtrl = ngModelCtrl;
  35 +
  36 + var labelsList = [];
30 37
31 - scope.$watch('link', function() { 38 + /*scope.$watch('link', function() {
32 scope.selectedLabel = null; 39 scope.selectedLabel = null;
33 if (scope.link && scope.labels) { 40 if (scope.link && scope.labels) {
34 if (scope.link.label) { 41 if (scope.link.label) {
@@ -53,19 +60,100 @@ export default function LinkDirective($compile, $templateCache, $filter) { @@ -53,19 +60,100 @@ export default function LinkDirective($compile, $templateCache, $filter) {
53 scope.link.label = ""; 60 scope.link.label = "";
54 } 61 }
55 } 62 }
  63 + };*/
  64 +
  65 + scope.transformLinkLabelChip = function (chip) {
  66 + var res = $filter('filter')(labelsList, {name: chip}, true);
  67 + var result;
  68 + if (res && res.length) {
  69 + result = angular.copy(res[0]);
  70 + } else {
  71 + result = {
  72 + name: chip,
  73 + value: chip
  74 + };
  75 + }
  76 + return result;
  77 + };
  78 +
  79 + scope.labelsSearch = function (searchText) {
  80 + var labels = searchText ? $filter('filter')(labelsList, {name: searchText}) : labelsList;
  81 + return labels.map((label) => label.name);
  82 + };
  83 +
  84 + scope.createLinkLabel = function (event, chipsId) {
  85 + var chipsChild = angular.element(chipsId, element)[0].firstElementChild;
  86 + var el = angular.element(chipsChild);
  87 + var chipBuffer = el.scope().$mdChipsCtrl.getChipBuffer();
  88 + event.preventDefault();
  89 + event.stopPropagation();
  90 + el.scope().$mdChipsCtrl.appendChip(chipBuffer.trim());
  91 + el.scope().$mdChipsCtrl.resetChipBuffer();
56 }; 92 };
57 93
  94 +
  95 + ngModelCtrl.$render = function () {
  96 + labelsList.length = 0;
  97 + for (var label in scope.allowedLabels) {
  98 + var linkLabel = {
  99 + name: scope.allowedLabels[label].name,
  100 + value: scope.allowedLabels[label].value
  101 + };
  102 + labelsList.push(linkLabel);
  103 + }
  104 +
  105 + var link = ngModelCtrl.$viewValue;
  106 + var labels = [];
  107 + if (link && link.labels) {
  108 + for (var i = 0; i < link.labels.length; i++) {
  109 + label = link.labels[i];
  110 + if (scope.allowedLabels[label]) {
  111 + labels.push(angular.copy(scope.allowedLabels[label]));
  112 + } else {
  113 + labels.push({
  114 + name: label,
  115 + value: label
  116 + });
  117 + }
  118 + }
  119 + }
  120 + scope.labels = labels;
  121 + scope.$watch('labels', function (newVal, prevVal) {
  122 + if (!angular.equals(newVal, prevVal)) {
  123 + updateLabels();
  124 + }
  125 + }, true);
  126 + };
  127 +
  128 + function updateLabels() {
  129 + if (ngModelCtrl.$viewValue) {
  130 + var labels = [];
  131 + for (var i = 0; i < scope.labels.length; i++) {
  132 + labels.push(scope.labels[i].value);
  133 + }
  134 + ngModelCtrl.$viewValue.labels = labels;
  135 + ngModelCtrl.$viewValue.label = labels.join(' / ');
  136 + updateValidity();
  137 + }
  138 + }
  139 +
  140 + function updateValidity() {
  141 + var valid = ngModelCtrl.$viewValue.labels &&
  142 + ngModelCtrl.$viewValue.labels.length ? true : false;
  143 + ngModelCtrl.$setValidity('linkLabels', valid);
  144 + }
  145 +
58 $compile(element.contents())(scope); 146 $compile(element.contents())(scope);
59 } 147 }
60 return { 148 return {
61 restrict: "E", 149 restrict: "E",
  150 + require: "^ngModel",
62 link: linker, 151 link: linker,
63 scope: { 152 scope: {
64 - link: '=',  
65 - labels: '=', 153 + allowedLabels: '=',
  154 + allowCustom: '=',
66 isEdit: '=', 155 isEdit: '=',
67 - isReadOnly: '=',  
68 - theForm: '=' 156 + isReadOnly: '='
69 } 157 }
70 }; 158 };
71 } 159 }
  1 +/**
  2 + * Copyright © 2016-2018 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +.tb-link-label-autocomplete {
  18 + .tb-not-found {
  19 + display: block;
  20 + line-height: 1.5;
  21 + height: 48px;
  22 + .tb-no-entries {
  23 + line-height: 48px;
  24 + }
  25 + }
  26 + li {
  27 + height: auto !important;
  28 + white-space: normal !important;
  29 + }
  30 +}
@@ -669,11 +669,15 @@ export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $time @@ -669,11 +669,15 @@ export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $time
669 } 669 }
670 } else { 670 } else {
671 if (edge.label) { 671 if (edge.label) {
  672 + if (!edge.labels) {
  673 + edge.labels = edge.label.split(' / ');
  674 + }
672 deferred.resolve(edge); 675 deferred.resolve(edge);
673 } else { 676 } else {
674 var labels = ruleChainService.getRuleNodeSupportedLinks(sourceNode.component); 677 var labels = ruleChainService.getRuleNodeSupportedLinks(sourceNode.component);
  678 + var allowCustomLabels = ruleChainService.ruleNodeAllowCustomLinks(sourceNode.component);
675 vm.enableHotKeys = false; 679 vm.enableHotKeys = false;
676 - addRuleNodeLink(event, edge, labels).then( 680 + addRuleNodeLink(event, edge, labels, allowCustomLabels).then(
677 (link) => { 681 (link) => {
678 deferred.resolve(link); 682 deferred.resolve(link);
679 vm.enableHotKeys = true; 683 vm.enableHotKeys = true;
@@ -713,6 +717,7 @@ export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $time @@ -713,6 +717,7 @@ export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $time
713 vm.isEditingRuleNode = false; 717 vm.isEditingRuleNode = false;
714 vm.editingRuleNode = null; 718 vm.editingRuleNode = null;
715 vm.editingRuleNodeLinkLabels = ruleChainService.getRuleNodeSupportedLinks(sourceNode.component); 719 vm.editingRuleNodeLinkLabels = ruleChainService.getRuleNodeSupportedLinks(sourceNode.component);
  720 + vm.editingRuleNodeAllowCustomLabels = ruleChainService.ruleNodeAllowCustomLinks(sourceNode.component);
716 vm.isEditingRuleNodeLink = true; 721 vm.isEditingRuleNodeLink = true;
717 vm.editingRuleNodeLinkIndex = vm.ruleChainModel.edges.indexOf(edge); 722 vm.editingRuleNodeLinkIndex = vm.ruleChainModel.edges.indexOf(edge);
718 vm.editingRuleNodeLink = angular.copy(edge); 723 vm.editingRuleNodeLink = angular.copy(edge);
@@ -744,7 +749,8 @@ export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $time @@ -744,7 +749,8 @@ export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $time
744 isInputSource: isInputSource, 749 isInputSource: isInputSource,
745 fromIndex: fromIndex, 750 fromIndex: fromIndex,
746 toIndex: toIndex, 751 toIndex: toIndex,
747 - label: edge.label 752 + label: edge.label,
  753 + labels: edge.labels
748 }; 754 };
749 connections.push(connection); 755 connections.push(connection);
750 } 756 }
@@ -816,7 +822,8 @@ export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $time @@ -816,7 +822,8 @@ export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $time
816 var edge = { 822 var edge = {
817 source: source, 823 source: source,
818 destination: destination, 824 destination: destination,
819 - label: connection.label 825 + label: connection.label,
  826 + labels: connection.labels
820 }; 827 };
821 vm.ruleChainModel.edges.push(edge); 828 vm.ruleChainModel.edges.push(edge);
822 vm.modelservice.edges.select(edge); 829 vm.modelservice.edges.select(edge);
@@ -1024,6 +1031,7 @@ export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $time @@ -1024,6 +1031,7 @@ export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $time
1024 } 1031 }
1025 1032
1026 if (vm.ruleChainMetaData.connections) { 1033 if (vm.ruleChainMetaData.connections) {
  1034 + var edgeMap = {};
1027 for (i = 0; i < vm.ruleChainMetaData.connections.length; i++) { 1035 for (i = 0; i < vm.ruleChainMetaData.connections.length; i++) {
1028 var connection = vm.ruleChainMetaData.connections[i]; 1036 var connection = vm.ruleChainMetaData.connections[i];
1029 var sourceNode = nodes[connection.fromIndex]; 1037 var sourceNode = nodes[connection.fromIndex];
@@ -1032,12 +1040,23 @@ export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $time @@ -1032,12 +1040,23 @@ export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $time
1032 var sourceConnectors = vm.modelservice.nodes.getConnectorsByType(sourceNode, flowchartConstants.rightConnectorType); 1040 var sourceConnectors = vm.modelservice.nodes.getConnectorsByType(sourceNode, flowchartConstants.rightConnectorType);
1033 var destConnectors = vm.modelservice.nodes.getConnectorsByType(destNode, flowchartConstants.leftConnectorType); 1041 var destConnectors = vm.modelservice.nodes.getConnectorsByType(destNode, flowchartConstants.leftConnectorType);
1034 if (sourceConnectors && sourceConnectors.length && destConnectors && destConnectors.length) { 1042 if (sourceConnectors && sourceConnectors.length && destConnectors && destConnectors.length) {
1035 - edge = {  
1036 - source: sourceConnectors[0].id,  
1037 - destination: destConnectors[0].id,  
1038 - label: connection.type  
1039 - };  
1040 - vm.ruleChainModel.edges.push(edge); 1043 + var sourceId = sourceConnectors[0].id;
  1044 + var destId = destConnectors[0].id;
  1045 + var edgeKey = sourceId + '_' + destId;
  1046 + edge = edgeMap[edgeKey];
  1047 + if (!edge) {
  1048 + edge = {
  1049 + source: sourceId,
  1050 + destination: destId,
  1051 + label: connection.type,
  1052 + labels: [connection.type]
  1053 + };
  1054 + edgeMap[edgeKey] = edge;
  1055 + vm.ruleChainModel.edges.push(edge);
  1056 + } else {
  1057 + edge.label += ' / ' +connection.type;
  1058 + edge.labels.push(connection.type);
  1059 + }
1041 } 1060 }
1042 } 1061 }
1043 } 1062 }
@@ -1045,6 +1064,7 @@ export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $time @@ -1045,6 +1064,7 @@ export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $time
1045 1064
1046 if (vm.ruleChainMetaData.ruleChainConnections) { 1065 if (vm.ruleChainMetaData.ruleChainConnections) {
1047 var ruleChainNodesMap = {}; 1066 var ruleChainNodesMap = {};
  1067 + var ruleChainEdgeMap = {};
1048 for (i = 0; i < vm.ruleChainMetaData.ruleChainConnections.length; i++) { 1068 for (i = 0; i < vm.ruleChainMetaData.ruleChainConnections.length; i++) {
1049 var ruleChainConnection = vm.ruleChainMetaData.ruleChainConnections[i]; 1069 var ruleChainConnection = vm.ruleChainMetaData.ruleChainConnections[i];
1050 var ruleChain = ruleChainsMap[ruleChainConnection.targetRuleChainId.id]; 1070 var ruleChain = ruleChainsMap[ruleChainConnection.targetRuleChainId.id];
@@ -1081,12 +1101,23 @@ export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $time @@ -1081,12 +1101,23 @@ export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $time
1081 if (sourceNode) { 1101 if (sourceNode) {
1082 connectors = vm.modelservice.nodes.getConnectorsByType(sourceNode, flowchartConstants.rightConnectorType); 1102 connectors = vm.modelservice.nodes.getConnectorsByType(sourceNode, flowchartConstants.rightConnectorType);
1083 if (connectors && connectors.length) { 1103 if (connectors && connectors.length) {
1084 - var ruleChainEdge = {  
1085 - source: connectors[0].id,  
1086 - destination: ruleChainNode.connectors[0].id,  
1087 - label: ruleChainConnection.type  
1088 - };  
1089 - vm.ruleChainModel.edges.push(ruleChainEdge); 1104 + sourceId = connectors[0].id;
  1105 + destId = ruleChainNode.connectors[0].id;
  1106 + edgeKey = sourceId + '_' + destId;
  1107 + var ruleChainEdge = ruleChainEdgeMap[edgeKey];
  1108 + if (!ruleChainEdge) {
  1109 + ruleChainEdge = {
  1110 + source: sourceId,
  1111 + destination: destId,
  1112 + label: ruleChainConnection.type,
  1113 + labels: [ruleChainConnection.type]
  1114 + };
  1115 + ruleChainEdgeMap[edgeKey] = ruleChainEdge;
  1116 + vm.ruleChainModel.edges.push(ruleChainEdge);
  1117 + } else {
  1118 + ruleChainEdge.label += ' / ' +ruleChainConnection.type;
  1119 + ruleChainEdge.labels.push(ruleChainConnection.type);
  1120 + }
1090 } 1121 }
1091 } 1122 }
1092 } 1123 }
@@ -1199,8 +1230,7 @@ export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $time @@ -1199,8 +1230,7 @@ export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $time
1199 var ruleChainConnection = { 1230 var ruleChainConnection = {
1200 fromIndex: fromIndex, 1231 fromIndex: fromIndex,
1201 targetRuleChainId: {entityType: vm.types.entityType.rulechain, id: destNode.targetRuleChainId}, 1232 targetRuleChainId: {entityType: vm.types.entityType.rulechain, id: destNode.targetRuleChainId},
1202 - additionalInfo: destNode.additionalInfo,  
1203 - type: edge.label 1233 + additionalInfo: destNode.additionalInfo
1204 }; 1234 };
1205 if (!ruleChainConnection.additionalInfo) { 1235 if (!ruleChainConnection.additionalInfo) {
1206 ruleChainConnection.additionalInfo = {}; 1236 ruleChainConnection.additionalInfo = {};
@@ -1208,15 +1238,22 @@ export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $time @@ -1208,15 +1238,22 @@ export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $time
1208 ruleChainConnection.additionalInfo.layoutX = Math.round(destNode.x); 1238 ruleChainConnection.additionalInfo.layoutX = Math.round(destNode.x);
1209 ruleChainConnection.additionalInfo.layoutY = Math.round(destNode.y); 1239 ruleChainConnection.additionalInfo.layoutY = Math.round(destNode.y);
1210 ruleChainConnection.additionalInfo.ruleChainNodeId = destNode.id; 1240 ruleChainConnection.additionalInfo.ruleChainNodeId = destNode.id;
1211 - ruleChainMetaData.ruleChainConnections.push(ruleChainConnection); 1241 + for (var rcIndex=0;rcIndex<edge.labels.length;rcIndex++) {
  1242 + var newRuleChainConnection = angular.copy(ruleChainConnection);
  1243 + newRuleChainConnection.type = edge.labels[rcIndex];
  1244 + ruleChainMetaData.ruleChainConnections.push(newRuleChainConnection);
  1245 + }
1212 } else { 1246 } else {
1213 var toIndex = nodes.indexOf(destNode); 1247 var toIndex = nodes.indexOf(destNode);
1214 var nodeConnection = { 1248 var nodeConnection = {
1215 fromIndex: fromIndex, 1249 fromIndex: fromIndex,
1216 - toIndex: toIndex,  
1217 - type: edge.label 1250 + toIndex: toIndex
1218 }; 1251 };
1219 - ruleChainMetaData.connections.push(nodeConnection); 1252 + for (var cIndex=0;cIndex<edge.labels.length;cIndex++) {
  1253 + var newNodeConnection = angular.copy(nodeConnection);
  1254 + newNodeConnection.type = edge.labels[cIndex];
  1255 + ruleChainMetaData.connections.push(newNodeConnection);
  1256 + }
1220 } 1257 }
1221 } 1258 }
1222 } 1259 }
@@ -1285,13 +1322,13 @@ export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $time @@ -1285,13 +1322,13 @@ export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $time
1285 }); 1322 });
1286 } 1323 }
1287 1324
1288 - function addRuleNodeLink($event, link, labels) { 1325 + function addRuleNodeLink($event, link, labels, allowCustomLabels) {
1289 return $mdDialog.show({ 1326 return $mdDialog.show({
1290 controller: 'AddRuleNodeLinkController', 1327 controller: 'AddRuleNodeLinkController',
1291 controllerAs: 'vm', 1328 controllerAs: 'vm',
1292 templateUrl: addRuleNodeLinkTemplate, 1329 templateUrl: addRuleNodeLinkTemplate,
1293 parent: angular.element($document[0].body), 1330 parent: angular.element($document[0].body),
1294 - locals: {link: link, labels: labels}, 1331 + locals: {link: link, labels: labels, allowCustomLabels: allowCustomLabels},
1295 fullscreen: true, 1332 fullscreen: true,
1296 targetEvent: $event 1333 targetEvent: $event
1297 }); 1334 });
@@ -1335,13 +1372,14 @@ export function AddRuleNodeController($scope, $mdDialog, ruleNode, ruleChainId, @@ -1335,13 +1372,14 @@ export function AddRuleNodeController($scope, $mdDialog, ruleNode, ruleChainId,
1335 } 1372 }
1336 1373
1337 /*@ngInject*/ 1374 /*@ngInject*/
1338 -export function AddRuleNodeLinkController($scope, $mdDialog, link, labels, helpLinks) { 1375 +export function AddRuleNodeLinkController($scope, $mdDialog, link, labels, allowCustomLabels, helpLinks) {
1339 1376
1340 var vm = this; 1377 var vm = this;
1341 1378
1342 vm.helpLinks = helpLinks; 1379 vm.helpLinks = helpLinks;
1343 vm.link = link; 1380 vm.link = link;
1344 vm.labels = labels; 1381 vm.labels = labels;
  1382 + vm.allowCustomLabels = allowCustomLabels;
1345 1383
1346 vm.add = add; 1384 vm.add = add;
1347 vm.cancel = cancel; 1385 vm.cancel = cancel;
@@ -170,6 +170,9 @@ @@ -170,6 +170,9 @@
170 &.tb-rule-chain-type { 170 &.tb-rule-chain-type {
171 background-color: #d6c4f1; 171 background-color: #d6c4f1;
172 } 172 }
  173 + &.tb-unknown-type {
  174 + background-color: #f16c29;
  175 + }
173 } 176 }
174 177
175 .tb-rule-node { 178 .tb-rule-node {
@@ -202,6 +205,7 @@ @@ -202,6 +205,7 @@
202 background-color: #a3eaa9; 205 background-color: #a3eaa9;
203 user-select: none; 206 user-select: none;
204 } 207 }
  208 +
205 md-icon { 209 md-icon {
206 font-size: 20px; 210 font-size: 20px;
207 width: 20px; 211 width: 20px;
@@ -207,11 +207,11 @@ @@ -207,11 +207,11 @@
207 </details-buttons> 207 </details-buttons>
208 <form name="vm.ruleNodeLinkForm" ng-if="vm.isEditingRuleNodeLink"> 208 <form name="vm.ruleNodeLinkForm" ng-if="vm.isEditingRuleNodeLink">
209 <tb-rule-node-link 209 <tb-rule-node-link
210 - link="vm.editingRuleNodeLink"  
211 - labels="vm.editingRuleNodeLinkLabels" 210 + ng-model="vm.editingRuleNodeLink"
  211 + allowed-labels="vm.editingRuleNodeLinkLabels"
  212 + allow-custom="vm.editingRuleNodeAllowCustomLabels"
212 is-edit="true" 213 is-edit="true"
213 - is-read-only="false"  
214 - the-form="vm.ruleNodeLinkForm"> 214 + is-read-only="false">
215 </tb-rule-node-link> 215 </tb-rule-node-link>
216 </form> 216 </form>
217 </tb-details-sidenav> 217 </tb-details-sidenav>
@@ -16,6 +16,7 @@ @@ -16,6 +16,7 @@
16 @import "~compass-sass-mixins/lib/compass"; 16 @import "~compass-sass-mixins/lib/compass";
17 @import "constants"; 17 @import "constants";
18 @import "animations"; 18 @import "animations";
  19 +@import "mixins";
19 @import "fonts"; 20 @import "fonts";
20 21
21 /*************** 22 /***************
@@ -437,6 +438,12 @@ pre.tb-highlight { @@ -437,6 +438,12 @@ pre.tb-highlight {
437 } 438 }
438 } 439 }
439 440
  441 +.tb-card-description {
  442 + color: rgba(0,0,0,0.54);
  443 + font-size: 13px;
  444 + @include line-clamp(2, 1.1);
  445 +}
  446 +
440 /*********************** 447 /***********************
441 * Flow 448 * Flow
442 ***********************/ 449 ***********************/
@@ -31,4 +31,29 @@ @@ -31,4 +31,29 @@
31 &:-ms-input-placeholder { 31 &:-ms-input-placeholder {
32 @content; 32 @content;
33 } 33 }
34 -}  
  34 +}
  35 +
  36 +@mixin line-clamp($numLines: 1, $lineHeight: 1.412) {
  37 + overflow: hidden;
  38 + position: relative;
  39 + line-height: $lineHeight;
  40 + text-align: justify;
  41 + margin-right: -1em;
  42 + padding-right: 2em;
  43 + max-height: ($numLines*$lineHeight)+em;
  44 + &:before {
  45 + content: '...';
  46 + position: absolute;
  47 + right: 1em;
  48 + bottom: 0;
  49 + }
  50 + &:after {
  51 + content: '';
  52 + position: absolute;
  53 + right: 1em;
  54 + width: 1em;
  55 + height: 1em;
  56 + margin-top: 0.2em;
  57 + background: white;
  58 + }
  59 +}