Commit 2fdb372c893c842afdc5884d14a20a6e120b9d13

Authored by Igor Kulikov
1 parent 755b6d36

TB-64: Implement widget actions.

@@ -171,6 +171,48 @@ export default class Subscription { @@ -171,6 +171,48 @@ export default class Subscription {
171 return deferred.promise; 171 return deferred.promise;
172 } 172 }
173 173
  174 + getFirstEntityInfo() {
  175 + var entityId;
  176 + var entityName;
  177 + if (this.type === this.ctx.types.widgetType.rpc.value) {
  178 + if (this.targetDeviceId) {
  179 + entityId = {
  180 + entityType: this.ctx.entityType.device,
  181 + id: this.targetDeviceId
  182 + }
  183 + entityName = this.targetDeviceName;
  184 + }
  185 + } else if (this.type == this.ctx.types.widgetType.alarm.value) {
  186 + if (this.alarmSource && this.alarmSource.entityType && this.alarmSource.entityId) {
  187 + entityId = {
  188 + entityType: this.alarmSource.entityType,
  189 + id: this.alarmSource.entityId
  190 + }
  191 + entityName = this.alarmSource.entityName;
  192 + }
  193 + } else {
  194 + for (var i=0;i<this.datasources.length;i++) {
  195 + var datasource = this.datasources[i];
  196 + if (datasource && datasource.entityType && datasource.entityId) {
  197 + entityId = {
  198 + entityType: datasource.entityType,
  199 + id: datasource.entityId
  200 + }
  201 + entityName = datasource.entityName;
  202 + break;
  203 + }
  204 + }
  205 + }
  206 + if (entityId) {
  207 + return {
  208 + entityId: entityId,
  209 + entityName: entityName
  210 + };
  211 + } else {
  212 + return null;
  213 + }
  214 + }
  215 +
174 initAlarmSubscription() { 216 initAlarmSubscription() {
175 var deferred = this.ctx.$q.defer(); 217 var deferred = this.ctx.$q.defer();
176 if (!this.ctx.aliasController) { 218 if (!this.ctx.aliasController) {
@@ -342,6 +384,7 @@ export default class Subscription { @@ -342,6 +384,7 @@ export default class Subscription {
342 function success(aliasInfo) { 384 function success(aliasInfo) {
343 if (aliasInfo.currentEntity && aliasInfo.currentEntity.entityType == subscription.ctx.types.entityType.device) { 385 if (aliasInfo.currentEntity && aliasInfo.currentEntity.entityType == subscription.ctx.types.entityType.device) {
344 subscription.targetDeviceId = aliasInfo.currentEntity.id; 386 subscription.targetDeviceId = aliasInfo.currentEntity.id;
  387 + subscription.targetDeviceName = aliasInfo.currentEntity.name;
345 if (subscription.targetDeviceId) { 388 if (subscription.targetDeviceId) {
346 subscription.rpcEnabled = true; 389 subscription.rpcEnabled = true;
347 } else { 390 } else {
@@ -626,7 +626,7 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, $tr @@ -626,7 +626,7 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, $tr
626 } 626 }
627 for (var actionSourceId in types.widgetActionSources) { 627 for (var actionSourceId in types.widgetActionSources) {
628 result.actionSources[actionSourceId] = angular.copy(types.widgetActionSources[actionSourceId]); 628 result.actionSources[actionSourceId] = angular.copy(types.widgetActionSources[actionSourceId]);
629 - result.actionSources[actionSourceId].name = $translate.instant(result.actionSources[actionSourceId].name); 629 + result.actionSources[actionSourceId].name = $translate.instant(result.actionSources[actionSourceId].name) + '';
630 } 630 }
631 631
632 return result; 632 return result;
@@ -400,8 +400,9 @@ export default angular.module('thingsboard.types', []) @@ -400,8 +400,9 @@ export default angular.module('thingsboard.types', [])
400 } 400 }
401 }, 401 },
402 widgetActionSources: { 402 widgetActionSources: {
403 - 'headerButton': { 403 + headerButton: {
404 name: 'widget-action.header-button', 404 name: 'widget-action.header-button',
  405 + value: 'headerButton',
405 multiple: true 406 multiple: true
406 } 407 }
407 }, 408 },
@@ -38,7 +38,11 @@ function Utils($mdColorPalette, $rootScope, $window, $translate, $q, $timeout, t @@ -38,7 +38,11 @@ function Utils($mdColorPalette, $rootScope, $window, $translate, $q, $timeout, t
38 materialColors = [], 38 materialColors = [],
39 materialIcons = []; 39 materialIcons = [];
40 40
41 - var commonUsedMaterialIcons = [ 'more_horiz', 'close', 'play_arrow' ]; 41 + var commonMaterialIcons = [ 'more_horiz', 'more_vert', 'open_in_new', 'visibility', 'play_arrow', 'arrow_back', 'arrow_downward',
  42 + 'arrow_forward', 'arrow_upwards', 'close', 'refresh', 'menu', 'show_chart', 'multiline_chart', 'pie_chart', 'insert_chart', 'people',
  43 + 'person', 'domain', 'devices_other', 'now_widgets', 'dashboards', 'map', 'pin_drop', 'my_location', 'extension', 'search',
  44 + 'settings', 'notifications', 'notifications_active', 'info', 'info_outline', 'warning', 'list', 'file_download', 'import_export',
  45 + 'share', 'add', 'edit', 'done' ];
42 46
43 predefinedFunctions['Sin'] = "return Math.round(1000*Math.sin(time/5000));"; 47 predefinedFunctions['Sin'] = "return Math.round(1000*Math.sin(time/5000));";
44 predefinedFunctions['Cos'] = "return Math.round(1000*Math.cos(time/5000));"; 48 predefinedFunctions['Cos'] = "return Math.round(1000*Math.cos(time/5000));";
@@ -148,7 +152,8 @@ function Utils($mdColorPalette, $rootScope, $window, $translate, $q, $timeout, t @@ -148,7 +152,8 @@ function Utils($mdColorPalette, $rootScope, $window, $translate, $q, $timeout, t
148 validateDatasources: validateDatasources, 152 validateDatasources: validateDatasources,
149 createKey: createKey, 153 createKey: createKey,
150 createLabelFromDatasource: createLabelFromDatasource, 154 createLabelFromDatasource: createLabelFromDatasource,
151 - insertVariable: insertVariable 155 + insertVariable: insertVariable,
  156 + customTranslation: customTranslation
152 } 157 }
153 158
154 return service; 159 return service;
@@ -188,7 +193,7 @@ function Utils($mdColorPalette, $rootScope, $window, $translate, $q, $timeout, t @@ -188,7 +193,7 @@ function Utils($mdColorPalette, $rootScope, $window, $translate, $q, $timeout, t
188 } 193 }
189 194
190 function getCommonMaterialIcons() { 195 function getCommonMaterialIcons() {
191 - return commonUsedMaterialIcons; 196 + return commonMaterialIcons;
192 } 197 }
193 198
194 function genMaterialColor(str) { 199 function genMaterialColor(str) {
@@ -469,4 +474,16 @@ function Utils($mdColorPalette, $rootScope, $window, $translate, $q, $timeout, t @@ -469,4 +474,16 @@ function Utils($mdColorPalette, $rootScope, $window, $translate, $q, $timeout, t
469 return result; 474 return result;
470 } 475 }
471 476
  477 + function customTranslation(translationValue, defaultValue) {
  478 + var result = '';
  479 + var translationId = types.translate.customTranslationsPrefix + translationValue;
  480 + var translation = $translate.instant(translationId);
  481 + if (translation != translationId) {
  482 + result = translation + '';
  483 + } else {
  484 + result = defaultValue;
  485 + }
  486 + return result;
  487 + }
  488 +
472 } 489 }
@@ -87,23 +87,32 @@ function DashboardAutocomplete($compile, $templateCache, $q, dashboardService, u @@ -87,23 +87,32 @@ function DashboardAutocomplete($compile, $templateCache, $q, dashboardService, u
87 dashboardService.getDashboardInfo(ngModelCtrl.$viewValue).then( 87 dashboardService.getDashboardInfo(ngModelCtrl.$viewValue).then(
88 function success(dashboard) { 88 function success(dashboard) {
89 scope.dashboard = dashboard; 89 scope.dashboard = dashboard;
  90 + startWatchers();
90 }, 91 },
91 function fail() { 92 function fail() {
92 scope.dashboard = null; 93 scope.dashboard = null;
  94 + scope.updateView();
  95 + startWatchers();
93 } 96 }
94 ); 97 );
95 } else { 98 } else {
96 scope.dashboard = null; 99 scope.dashboard = null;
  100 + startWatchers();
97 } 101 }
98 } 102 }
99 103
100 - scope.$watch('dashboard', function () {  
101 - scope.updateView();  
102 - });  
103 -  
104 - scope.$watch('disabled', function () {  
105 - scope.updateView();  
106 - }); 104 + function startWatchers() {
  105 + scope.$watch('dashboard', function (newVal, prevVal) {
  106 + if (!angular.equals(newVal, prevVal)) {
  107 + scope.updateView();
  108 + }
  109 + });
  110 + scope.$watch('disabled', function (newVal, prevVal) {
  111 + if (!angular.equals(newVal, prevVal)) {
  112 + scope.updateView();
  113 + }
  114 + });
  115 + }
107 116
108 if (scope.selectFirstDashboard) { 117 if (scope.selectFirstDashboard) {
109 var pageLink = {limit: 1, textSearch: ''}; 118 var pageLink = {limit: 1, textSearch: ''};
@@ -111,6 +120,7 @@ function DashboardAutocomplete($compile, $templateCache, $q, dashboardService, u @@ -111,6 +120,7 @@ function DashboardAutocomplete($compile, $templateCache, $q, dashboardService, u
111 var dashboards = result.data; 120 var dashboards = result.data;
112 if (dashboards.length > 0) { 121 if (dashboards.length > 0) {
113 scope.dashboard = dashboards[0]; 122 scope.dashboard = dashboards[0];
  123 + scope.updateView();
114 } 124 }
115 }, function fail() { 125 }, function fail() {
116 }); 126 });
@@ -187,6 +187,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $ @@ -187,6 +187,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
187 vm.showWidgetActions = showWidgetActions; 187 vm.showWidgetActions = showWidgetActions;
188 vm.widgetTitleStyle = widgetTitleStyle; 188 vm.widgetTitleStyle = widgetTitleStyle;
189 vm.widgetTitle = widgetTitle; 189 vm.widgetTitle = widgetTitle;
  190 + vm.customWidgetHeaderActions = customWidgetHeaderActions;
190 vm.widgetActions = widgetActions; 191 vm.widgetActions = widgetActions;
191 vm.dropWidgetShadow = dropWidgetShadow; 192 vm.dropWidgetShadow = dropWidgetShadow;
192 vm.enableWidgetFullscreen = enableWidgetFullscreen; 193 vm.enableWidgetFullscreen = enableWidgetFullscreen;
@@ -875,6 +876,15 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $ @@ -875,6 +876,15 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
875 } 876 }
876 } 877 }
877 878
  879 + function customWidgetHeaderActions(widget) {
  880 + var ctx = widgetContext(widget);
  881 + if (ctx && ctx.customHeaderActions && ctx.customHeaderActions.length) {
  882 + return ctx.customHeaderActions;
  883 + } else {
  884 + return [];
  885 + }
  886 + }
  887 +
878 function widgetActions(widget) { 888 function widgetActions(widget) {
879 var ctx = widgetContext(widget); 889 var ctx = widgetContext(widget);
880 if (ctx && ctx.widgetActions && ctx.widgetActions.length) { 890 if (ctx && ctx.widgetActions && ctx.widgetActions.length) {
@@ -52,6 +52,16 @@ @@ -52,6 +52,16 @@
52 <tb-timewindow aggregation="{{vm.hasAggregation(widget)}}" ng-if="vm.hasTimewindow(widget)" ng-model="widget.config.timewindow"></tb-timewindow> 52 <tb-timewindow aggregation="{{vm.hasAggregation(widget)}}" ng-if="vm.hasTimewindow(widget)" ng-model="widget.config.timewindow"></tb-timewindow>
53 </div> 53 </div>
54 <div class="tb-widget-actions" layout="row" layout-align="start center" ng-show="vm.showWidgetActions(widget)" tb-mousedown="$event.stopPropagation()"> 54 <div class="tb-widget-actions" layout="row" layout-align="start center" ng-show="vm.showWidgetActions(widget)" tb-mousedown="$event.stopPropagation()">
  55 + <md-button ng-repeat="action in vm.customWidgetHeaderActions(widget)"
  56 + aria-label="{{action.displayName}}"
  57 + ng-show="!vm.isEdit"
  58 + ng-click="action.onAction($event)"
  59 + class="md-icon-button">
  60 + <md-tooltip md-direction="top">
  61 + {{action.displayName}}
  62 + </md-tooltip>
  63 + <ng-md-icon size="20" icon="{{action.icon}}"></ng-md-icon>
  64 + </md-button>
55 <md-button ng-repeat="action in vm.widgetActions(widget)" 65 <md-button ng-repeat="action in vm.widgetActions(widget)"
56 aria-label="{{ action.name | translate }}" 66 aria-label="{{ action.name | translate }}"
57 ng-show="!vm.isEdit && action.show" 67 ng-show="!vm.isEdit && action.show"
@@ -14,6 +14,8 @@ @@ -14,6 +14,8 @@
14 * limitations under the License. 14 * limitations under the License.
15 */ 15 */
16 16
  17 +import './material-icon-select.scss';
  18 +
17 import MaterialIconsDialogController from './material-icons-dialog.controller'; 19 import MaterialIconsDialogController from './material-icons-dialog.controller';
18 20
19 /* eslint-disable import/no-unresolved, import/default */ 21 /* eslint-disable import/no-unresolved, import/default */
  1 +/**
  2 + * Copyright © 2016-2017 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-material-icon-select {
  18 + md-icon {
  19 + padding: 4px;
  20 + margin: 8px 4px 4px;
  21 + cursor: pointer;
  22 + border: solid 1px rgba(0,0,0,0.27);
  23 + }
  24 + md-input-container {
  25 + margin-bottom: 0px;
  26 + }
  27 +}
@@ -15,12 +15,10 @@ @@ -15,12 +15,10 @@
15 limitations under the License. 15 limitations under the License.
16 16
17 --> 17 -->
18 -<div layout="row"> 18 +<div class="tb-material-icon-select" layout="row">
19 <md-icon class="material-icons" ng-click="openIconDialog($event)">{{icon}}</md-icon> 19 <md-icon class="material-icons" ng-click="openIconDialog($event)">{{icon}}</md-icon>
20 <md-input-container flex> 20 <md-input-container flex>
21 - <md-input-container class="md-block">  
22 - <label translate>icon.icon</label>  
23 - <input ng-click="openIconDialog($event)" ng-model="icon">  
24 - </md-input-container> 21 + <label translate>icon.icon</label>
  22 + <input ng-mousedown="openIconDialog($event)" ng-model="icon">
25 </md-input-container> 23 </md-input-container>
26 </div> 24 </div>
@@ -42,7 +42,8 @@ function ManageWidgetActions() { @@ -42,7 +42,8 @@ function ManageWidgetActions() {
42 scope: true, 42 scope: true,
43 bindToController: { 43 bindToController: {
44 actionSources: '=', 44 actionSources: '=',
45 - widgetActions: '=' 45 + widgetActions: '=',
  46 + fetchDashboardStates: '&',
46 }, 47 },
47 controller: ManageWidgetActionsController, 48 controller: ManageWidgetActionsController,
48 controllerAs: 'vm', 49 controllerAs: 'vm',
@@ -55,7 +56,7 @@ function ManageWidgetActions() { @@ -55,7 +56,7 @@ function ManageWidgetActions() {
55 56
56 /*@ngInject*/ 57 /*@ngInject*/
57 function ManageWidgetActionsController($rootScope, $scope, $document, $mdDialog, $q, $filter, 58 function ManageWidgetActionsController($rootScope, $scope, $document, $mdDialog, $q, $filter,
58 - $translate, $timeout, types) { 59 + $translate, $timeout, utils, types) {
59 60
60 let vm = this; 61 let vm = this;
61 62
@@ -165,12 +166,30 @@ function ManageWidgetActionsController($rootScope, $scope, $document, $mdDialog, @@ -165,12 +166,30 @@ function ManageWidgetActionsController($rootScope, $scope, $document, $mdDialog,
165 if (!isAdd) { 166 if (!isAdd) {
166 prevActionId = action.id; 167 prevActionId = action.id;
167 } 168 }
  169 + var availableActionSources = {};
  170 + for (var id in vm.actionSources) {
  171 + var actionSource = vm.actionSources[id];
  172 + if (actionSource.multiple) {
  173 + availableActionSources[id] = actionSource;
  174 + } else {
  175 + if (!isAdd && action.actionSourceId == id) {
  176 + availableActionSources[id] = actionSource;
  177 + } else {
  178 + var result = $filter('filter')(vm.allActions, {actionSourceId: id});
  179 + if (!result || !result.length) {
  180 + availableActionSources[id] = actionSource;
  181 + }
  182 + }
  183 + }
  184 + }
168 $mdDialog.show({ 185 $mdDialog.show({
169 controller: 'WidgetActionDialogController', 186 controller: 'WidgetActionDialogController',
170 controllerAs: 'vm', 187 controllerAs: 'vm',
171 templateUrl: widgetActionDialogTemplate, 188 templateUrl: widgetActionDialogTemplate,
172 parent: angular.element($document[0].body), 189 parent: angular.element($document[0].body),
173 - locals: {isAdd: isAdd, actionSources: vm.actionSources, action: angular.copy(action)}, 190 + locals: {isAdd: isAdd, fetchDashboardStates: vm.fetchDashboardStates,
  191 + actionSources: availableActionSources, widgetActions: vm.widgetActions,
  192 + action: angular.copy(action)},
174 skipHide: true, 193 skipHide: true,
175 fullscreen: true, 194 fullscreen: true,
176 targetEvent: $event 195 targetEvent: $event
@@ -189,7 +208,8 @@ function ManageWidgetActionsController($rootScope, $scope, $document, $mdDialog, @@ -189,7 +208,8 @@ function ManageWidgetActionsController($rootScope, $scope, $document, $mdDialog,
189 } 208 }
190 209
191 function saveAction(action, prevActionId) { 210 function saveAction(action, prevActionId) {
192 - action.actionSourceName = vm.actionSources[action.actionSourceId].name; 211 + var actionSourceName = vm.actionSources[action.actionSourceId].name;
  212 + action.actionSourceName = utils.customTranslation(actionSourceName, actionSourceName);
193 action.typeName = $translate.instant(types.widgetActionTypes[action.type].name); 213 action.typeName = $translate.instant(types.widgetActionTypes[action.type].name);
194 var actionSourceId = action.actionSourceId; 214 var actionSourceId = action.actionSourceId;
195 var widgetAction = angular.copy(action); 215 var widgetAction = angular.copy(action);
@@ -227,15 +247,10 @@ function ManageWidgetActionsController($rootScope, $scope, $document, $mdDialog, @@ -227,15 +247,10 @@ function ManageWidgetActionsController($rootScope, $scope, $document, $mdDialog,
227 var actionSourceActions = vm.widgetActions[actionSourceId]; 247 var actionSourceActions = vm.widgetActions[actionSourceId];
228 for (var i=0;i<actionSourceActions.length;i++) { 248 for (var i=0;i<actionSourceActions.length;i++) {
229 var actionSourceAction = actionSourceActions[i]; 249 var actionSourceAction = actionSourceActions[i];
230 - var action = {  
231 - id: actionSourceAction.id,  
232 - actionSourceId: actionSourceId,  
233 - actionSourceName: actionSource.name,  
234 - name: actionSourceAction.name,  
235 - icon: actionSourceAction.icon,  
236 - type: actionSourceAction.type,  
237 - typeName: $translate.instant(types.widgetActionTypes[actionSourceAction.type].name)  
238 - }; 250 + var action = angular.copy(actionSourceAction);
  251 + action.actionSourceId = actionSourceId;
  252 + action.actionSourceName = utils.customTranslation(actionSource.name, actionSource.name);
  253 + action.typeName = $translate.instant(types.widgetActionTypes[actionSourceAction.type].name);
239 vm.allActions.push(action); 254 vm.allActions.push(action);
240 } 255 }
241 } 256 }
@@ -15,14 +15,21 @@ @@ -15,14 +15,21 @@
15 */ 15 */
16 16
17 /*@ngInject*/ 17 /*@ngInject*/
18 -export default function WidgetActionDialogController($scope, $mdDialog, types, utils, isAdd, actionSources, action) { 18 +export default function WidgetActionDialogController($scope, $mdDialog, $filter, $q, dashboardService, dashboardUtils, types, utils,
  19 + isAdd, fetchDashboardStates, actionSources, widgetActions, action) {
19 20
20 var vm = this; 21 var vm = this;
21 22
22 vm.types = types; 23 vm.types = types;
23 24
24 vm.isAdd = isAdd; 25 vm.isAdd = isAdd;
  26 + vm.fetchDashboardStates = fetchDashboardStates;
25 vm.actionSources = actionSources; 27 vm.actionSources = actionSources;
  28 + vm.widgetActions = widgetActions;
  29 +
  30 + vm.targetDashboardStateSearchText = '';
  31 +
  32 + vm.selectedDashboardStateIds = [];
26 33
27 if (vm.isAdd) { 34 if (vm.isAdd) {
28 vm.action = { 35 vm.action = {
@@ -32,15 +39,131 @@ export default function WidgetActionDialogController($scope, $mdDialog, types, u @@ -32,15 +39,131 @@ export default function WidgetActionDialogController($scope, $mdDialog, types, u
32 vm.action = action; 39 vm.action = action;
33 } 40 }
34 41
  42 + vm.actionSourceName = actionSourceName;
  43 +
  44 + vm.targetDashboardStateSearchTextChanged = function() {
  45 + }
  46 +
  47 + vm.dashboardStateSearch = dashboardStateSearch;
35 vm.cancel = cancel; 48 vm.cancel = cancel;
36 vm.save = save; 49 vm.save = save;
37 50
  51 + $scope.$watch("vm.action.name", function(newVal, prevVal) {
  52 + if (!angular.equals(newVal, prevVal) && vm.action.name != null) {
  53 + checkActionName();
  54 + }
  55 + });
  56 +
  57 + $scope.$watch("vm.action.actionSourceId", function(newVal, prevVal) {
  58 + if (!angular.equals(newVal, prevVal) && vm.action.actionSourceId != null) {
  59 + checkActionName();
  60 + }
  61 + });
  62 +
  63 + $scope.$watch("vm.action.targetDashboardId", function() {
  64 + vm.selectedDashboardStateIds = [];
  65 + if (vm.action.targetDashboardId) {
  66 + dashboardService.getDashboard(vm.action.targetDashboardId).then(
  67 + function success(dashboard) {
  68 + dashboard = dashboardUtils.validateAndUpdateDashboard(dashboard);
  69 + var states = dashboard.configuration.states;
  70 + vm.selectedDashboardStateIds = Object.keys(states);
  71 + }
  72 + );
  73 + }
  74 + });
  75 +
  76 + $scope.$watch('vm.action.type', function(newType) {
  77 + if (newType) {
  78 + switch (newType) {
  79 + case vm.types.widgetActionTypes.openDashboardState.value:
  80 + case vm.types.widgetActionTypes.updateDashboardState.value:
  81 + case vm.types.widgetActionTypes.openDashboard.value:
  82 + if (angular.isUndefined(vm.action.setEntityId)) {
  83 + vm.action.setEntityId = true;
  84 + }
  85 + break;
  86 + }
  87 + }
  88 + });
  89 +
  90 + function checkActionName() {
  91 + var actionNameIsUnique = true;
  92 + if (vm.action.actionSourceId && vm.action.name) {
  93 + var sourceActions = vm.widgetActions[vm.action.actionSourceId];
  94 + if (sourceActions) {
  95 + var result = $filter('filter')(sourceActions, {name: vm.action.name}, true);
  96 + if (result && result.length && result[0].id !== vm.action.id) {
  97 + actionNameIsUnique = false;
  98 + }
  99 + }
  100 + }
  101 + $scope.theForm.name.$setValidity('actionNameNotUnique', actionNameIsUnique);
  102 + }
  103 +
  104 + function actionSourceName (actionSource) {
  105 + if (actionSource) {
  106 + return utils.customTranslation(actionSource.name, actionSource.name);
  107 + } else {
  108 + return '';
  109 + }
  110 + }
  111 +
  112 + function dashboardStateSearch (query) {
  113 + if (vm.action.type == vm.types.widgetActionTypes.openDashboard.value) {
  114 + var deferred = $q.defer();
  115 + var result = query ? vm.selectedDashboardStateIds.filter(
  116 + createFilterForDashboardState(query)) : vm.selectedDashboardStateIds;
  117 + if (result && result.length) {
  118 + deferred.resolve(result);
  119 + } else {
  120 + deferred.resolve([query]);
  121 + }
  122 + return deferred.promise;
  123 + } else {
  124 + return vm.fetchDashboardStates({query: query});
  125 + }
  126 + }
  127 +
  128 + function createFilterForDashboardState (query) {
  129 + var lowercaseQuery = angular.lowercase(query);
  130 + return function filterFn(stateId) {
  131 + return (angular.lowercase(stateId).indexOf(lowercaseQuery) === 0);
  132 + };
  133 + }
  134 +
  135 + function cleanupAction(action) {
  136 + var result = {};
  137 + result.id = action.id;
  138 + result.actionSourceId = action.actionSourceId;
  139 + result.name = action.name;
  140 + result.icon = action.icon;
  141 + result.type = action.type;
  142 + switch (action.type) {
  143 + case vm.types.widgetActionTypes.openDashboardState.value:
  144 + case vm.types.widgetActionTypes.updateDashboardState.value:
  145 + result.targetDashboardStateId = action.targetDashboardStateId;
  146 + result.openRightLayout = action.openRightLayout;
  147 + result.setEntityId = action.setEntityId;
  148 + break;
  149 + case vm.types.widgetActionTypes.openDashboard.value:
  150 + result.targetDashboardId = action.targetDashboardId;
  151 + result.targetDashboardStateId = action.targetDashboardStateId;
  152 + result.setEntityId = action.setEntityId;
  153 + break;
  154 + case vm.types.widgetActionTypes.custom.value:
  155 + result.customFunction = action.customFunction;
  156 + break;
  157 + }
  158 + return result;
  159 + }
  160 +
38 function cancel() { 161 function cancel() {
39 $mdDialog.cancel(); 162 $mdDialog.cancel();
40 } 163 }
41 164
42 function save() { 165 function save() {
43 $scope.theForm.$setPristine(); 166 $scope.theForm.$setPristine();
44 - $mdDialog.hide(vm.action); 167 + $mdDialog.hide(cleanupAction(vm.action));
45 } 168 }
46 } 169 }
@@ -31,12 +31,12 @@ @@ -31,12 +31,12 @@
31 <md-dialog-content> 31 <md-dialog-content>
32 <div class="md-dialog-content"> 32 <div class="md-dialog-content">
33 <md-content class="md-padding" layout="column"> 33 <md-content class="md-padding" layout="column">
34 - <fieldset ng-disabled="loading"> 34 + <fieldset ng-disabled="loading" layout="column">
35 <md-input-container class="md-block"> 35 <md-input-container class="md-block">
36 <label translate>widget-config.action-source</label> 36 <label translate>widget-config.action-source</label>
37 <md-select name="actionSource" required aria-label="{{ 'widget-config.action-source' | translate }}" ng-model="vm.action.actionSourceId"> 37 <md-select name="actionSource" required aria-label="{{ 'widget-config.action-source' | translate }}" ng-model="vm.action.actionSourceId">
38 <md-option ng-repeat="(actionSourceId, actionSource) in vm.actionSources" ng-value="actionSourceId"> 38 <md-option ng-repeat="(actionSourceId, actionSource) in vm.actionSources" ng-value="actionSourceId">
39 - {{actionSource.name}} 39 + {{vm.actionSourceName(actionSource)}}
40 </md-option> 40 </md-option>
41 </md-select> 41 </md-select>
42 <div ng-messages="theForm.actionSource.$error"> 42 <div ng-messages="theForm.actionSource.$error">
@@ -48,6 +48,7 @@ @@ -48,6 +48,7 @@
48 <input name="name" required ng-model="vm.action.name"> 48 <input name="name" required ng-model="vm.action.name">
49 <div ng-messages="theForm.name.$error"> 49 <div ng-messages="theForm.name.$error">
50 <div ng-message="required" translate>widget-config.action-name-required</div> 50 <div ng-message="required" translate>widget-config.action-name-required</div>
  51 + <div ng-message="actionNameNotUnique" translate>widget-config.action-name-not-unique</div>
51 </div> 52 </div>
52 </md-input-container> 53 </md-input-container>
53 <tb-material-icon-select ng-model="vm.action.icon"> 54 <tb-material-icon-select ng-model="vm.action.icon">
@@ -63,6 +64,57 @@ @@ -63,6 +64,57 @@
63 <div ng-message="required" translate>widget-config.action-type-required</div> 64 <div ng-message="required" translate>widget-config.action-type-required</div>
64 </div> 65 </div>
65 </md-input-container> 66 </md-input-container>
  67 + <div layout="column"
  68 + style="padding-bottom: 20px;"
  69 + ng-if="vm.action.type == vm.types.widgetActionTypes.openDashboard.value">
  70 + <div class="md-caption tb-required"
  71 + style="padding-left: 3px; padding-bottom: 10px; color: rgba(0,0,0,0.57);" translate>widget-action.target-dashboard</div>
  72 + <tb-dashboard-autocomplete the-form="theForm"
  73 + tb-required="true"
  74 + ng-model="vm.action.targetDashboardId"
  75 + select-first-dashboard="false">
  76 + </tb-dashboard-autocomplete>
  77 + </div>
  78 + <md-autocomplete ng-if="vm.action.type == vm.types.widgetActionTypes.openDashboardState.value ||
  79 + vm.action.type == vm.types.widgetActionTypes.updateDashboardState.value ||
  80 + vm.action.type == vm.types.widgetActionTypes.openDashboard.value"
  81 + ng-required="vm.action.type == vm.types.widgetActionTypes.openDashboardState.value"
  82 + md-no-cache="true"
  83 + md-input-name="targetDashboardState"
  84 + ng-model="vm.action.targetDashboardStateId"
  85 + md-selected-item="vm.action.targetDashboardStateId"
  86 + md-search-text="vm.targetDashboardStateSearchText"
  87 + md-search-text-change="vm.targetDashboardStateSearchTextChanged()"
  88 + md-items="item in vm.dashboardStateSearch(vm.targetDashboardStateSearchText)"
  89 + md-item-text="item"
  90 + md-min-length="0"
  91 + md-floating-label="{{ 'widget-action.target-dashboard-state' | translate }}"
  92 + md-select-on-match="true">
  93 + <md-item-template>
  94 + <div>
  95 + <span md-highlight-text="vm.targetDashboardStateSearchText" md-highlight-flags="^i">{{item}}</span>
  96 + </div>
  97 + </md-item-template>
  98 + <div ng-messages="theForm.targetDashboardState.$error">
  99 + <div translate ng-message="required">widget-action.target-dashboard-state-required</div>
  100 + </div>
  101 + </md-autocomplete>
  102 + <md-checkbox ng-if="vm.action.type == vm.types.widgetActionTypes.openDashboardState.value ||
  103 + vm.action.type == vm.types.widgetActionTypes.updateDashboardState.value"
  104 + flex aria-label="{{ 'widget-action.open-right-layout' | translate }}"
  105 + ng-model="vm.action.openRightLayout">{{ 'widget-action.open-right-layout' | translate }}
  106 + </md-checkbox>
  107 + <md-checkbox ng-if="vm.action.type == vm.types.widgetActionTypes.openDashboardState.value ||
  108 + vm.action.type == vm.types.widgetActionTypes.updateDashboardState.value ||
  109 + vm.action.type == vm.types.widgetActionTypes.openDashboard.value"
  110 + flex aria-label="{{ 'widget-action.set-entity-from-widget' | translate }}"
  111 + ng-model="vm.action.setEntityId">{{ 'widget-action.set-entity-from-widget' | translate }}
  112 + </md-checkbox>
  113 + <tb-js-func ng-if="vm.action.type == vm.types.widgetActionTypes.custom.value"
  114 + ng-model="vm.action.customFunction"
  115 + function-args="{{ ['$event', 'widgetContext', 'entityId'] }}"
  116 + validation-args="{{ [] }}">
  117 + </tb-js-func>
66 </fieldset> 118 </fieldset>
67 </md-content> 119 </md-content>
68 </div> 120 </div>
@@ -468,6 +468,7 @@ function WidgetConfig($compile, $templateCache, $rootScope, $translate, $timeout @@ -468,6 +468,7 @@ function WidgetConfig($compile, $templateCache, $rootScope, $translate, $timeout
468 aliasController: '=', 468 aliasController: '=',
469 functionsOnly: '=', 469 functionsOnly: '=',
470 fetchEntityKeys: '&', 470 fetchEntityKeys: '&',
  471 + fetchDashboardStates: '&',
471 onCreateEntityAlias: '&', 472 onCreateEntityAlias: '&',
472 theForm: '=' 473 theForm: '='
473 }, 474 },
@@ -277,7 +277,10 @@ @@ -277,7 +277,10 @@
277 </md-tab> 277 </md-tab>
278 <md-tab label="{{ 'widget-config.actions' | translate }}"> 278 <md-tab label="{{ 'widget-config.actions' | translate }}">
279 <md-content class="md-padding" layout="column"> 279 <md-content class="md-padding" layout="column">
280 - <tb-manage-widget-actions action-sources="actionSources" widget-actions="actions"> 280 + <tb-manage-widget-actions
  281 + action-sources="actionSources"
  282 + widget-actions="actions"
  283 + fetch-dashboard-states="fetchDashboardStates({query: query})">
281 </tb-manage-widget-actions> 284 </tb-manage-widget-actions>
282 </md-content> 285 </md-content>
283 </md-tab> 286 </md-tab>
@@ -20,7 +20,7 @@ import Subscription from '../../api/subscription'; @@ -20,7 +20,7 @@ import Subscription from '../../api/subscription';
20 /* eslint-disable angular/angularelement */ 20 /* eslint-disable angular/angularelement */
21 21
22 /*@ngInject*/ 22 /*@ngInject*/
23 -export default function WidgetController($scope, $timeout, $window, $element, $q, $log, $injector, $filter, $compile, tbRaf, types, utils, timeService, 23 +export default function WidgetController($scope, $state, $timeout, $window, $element, $q, $log, $injector, $filter, $compile, tbRaf, types, utils, timeService,
24 datasourceService, alarmService, entityService, deviceService, visibleRect, isEdit, isMobile, stDiff, dashboardTimewindow, 24 datasourceService, alarmService, entityService, deviceService, visibleRect, isEdit, isMobile, stDiff, dashboardTimewindow,
25 dashboardTimewindowApi, widget, aliasController, stateController, widgetInfo, widgetType) { 25 dashboardTimewindowApi, widget, aliasController, stateController, widgetInfo, widgetType) {
26 26
@@ -44,6 +44,20 @@ export default function WidgetController($scope, $timeout, $window, $element, $q @@ -44,6 +44,20 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
44 44
45 var cafs = {}; 45 var cafs = {};
46 46
  47 + var actionDescriptorsBySourceId = {};
  48 + if (widget.config.actions) {
  49 + for (var actionSourceId in widget.config.actions) {
  50 + var descriptors = widget.config.actions[actionSourceId];
  51 + var actionDescriptors = [];
  52 + descriptors.forEach(function(descriptor) {
  53 + var actionDescriptor = angular.copy(descriptor);
  54 + actionDescriptor.displayName = utils.customTranslation(descriptor.name, descriptor.name);
  55 + actionDescriptors.push(actionDescriptor);
  56 + });
  57 + actionDescriptorsBySourceId[actionSourceId] = actionDescriptors;
  58 + }
  59 + }
  60 +
47 var widgetContext = { 61 var widgetContext = {
48 inited: false, 62 inited: false,
49 $container: null, 63 $container: null,
@@ -103,9 +117,32 @@ export default function WidgetController($scope, $timeout, $window, $element, $q @@ -103,9 +117,32 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
103 utils: { 117 utils: {
104 formatValue: formatValue 118 formatValue: formatValue
105 }, 119 },
  120 + actionsApi: {
  121 + actionDescriptorsBySourceId: actionDescriptorsBySourceId,
  122 + getActionDescriptors: getActionDescriptors,
  123 + handleWidgetAction: handleWidgetAction
  124 + },
106 stateController: stateController 125 stateController: stateController
107 }; 126 };
108 127
  128 + widgetContext.customHeaderActions = [];
  129 + var headerActionsDescriptors = getActionDescriptors(types.widgetActionSources.headerButton.value);
  130 + for (var i=0;i<headerActionsDescriptors.length;i++) {
  131 + var descriptor = headerActionsDescriptors[i];
  132 + var headerAction = {};
  133 + headerAction.name = descriptor.name;
  134 + headerAction.displayName = descriptor.displayName;
  135 + headerAction.icon = descriptor.icon;
  136 + headerAction.descriptor = descriptor;
  137 + headerAction.onAction = function($event) {
  138 + var entityInfo = getFirstEntityInfo();
  139 + var entityId = entityInfo ? entityInfo.entityId : null;
  140 + var entityName = entityInfo ? entityInfo.entityName : null;
  141 + handleWidgetAction($event, this.descriptor, entityId, entityName);
  142 + }
  143 + widgetContext.customHeaderActions.push(headerAction);
  144 + }
  145 +
109 var subscriptionContext = { 146 var subscriptionContext = {
110 $scope: $scope, 147 $scope: $scope,
111 $q: $q, 148 $q: $q,
@@ -376,6 +413,87 @@ export default function WidgetController($scope, $timeout, $window, $element, $q @@ -376,6 +413,87 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
376 return deferred.promise; 413 return deferred.promise;
377 } 414 }
378 415
  416 + function getActionDescriptors(actionSourceId) {
  417 + var result = widgetContext.actionsApi.actionDescriptorsBySourceId[actionSourceId];
  418 + if (!result) {
  419 + result = [];
  420 + }
  421 + return result;
  422 + }
  423 +
  424 + function handleWidgetAction($event, descriptor, entityId, entityName) {
  425 + var type = descriptor.type;
  426 + switch (type) {
  427 + case types.widgetActionTypes.openDashboardState.value:
  428 + case types.widgetActionTypes.updateDashboardState.value:
  429 + var targetDashboardStateId = descriptor.targetDashboardStateId;
  430 + var targetEntityId;
  431 + if (descriptor.setEntityId) {
  432 + targetEntityId = entityId;
  433 + }
  434 + var params = {};
  435 + if (targetEntityId) {
  436 + params.entityId = targetEntityId;
  437 + if (entityName) {
  438 + params.entityName = entityName;
  439 + }
  440 + }
  441 + if (type == types.widgetActionTypes.openDashboardState.value) {
  442 + widgetContext.stateController.openState(targetDashboardStateId, params, descriptor.openRightLayout);
  443 + } else {
  444 + widgetContext.stateController.updateState(targetDashboardStateId, params, descriptor.openRightLayout);
  445 + }
  446 + break;
  447 + case types.widgetActionTypes.openDashboard.value:
  448 + var targetDashboardId = descriptor.targetDashboardId;
  449 + targetDashboardStateId = descriptor.targetDashboardStateId;
  450 + targetEntityId;
  451 + if (descriptor.setEntityId) {
  452 + targetEntityId = entityId;
  453 + }
  454 + var stateObject = {};
  455 + stateObject.params = {};
  456 + if (targetEntityId) {
  457 + stateObject.params.entityId = targetEntityId;
  458 + if (entityName) {
  459 + stateObject.params.entityName = entityName;
  460 + }
  461 + }
  462 + if (targetDashboardStateId) {
  463 + stateObject.id = targetDashboardStateId;
  464 + }
  465 + var stateParams = {
  466 + dashboardId: targetDashboardId,
  467 + state: angular.toJson([ stateObject ])
  468 + }
  469 + $state.go('home.dashboards.dashboard', stateParams);
  470 + break;
  471 + case types.widgetActionTypes.custom.value:
  472 + var customFunction = descriptor.customFunction;
  473 + if (angular.isDefined(customFunction) && customFunction.length > 0) {
  474 + try {
  475 + var customActionFunction = new Function('$event', 'widgetContext', 'entityId', 'entityName', customFunction);
  476 + customActionFunction($event, widgetContext, entityId, entityName);
  477 + } catch (e) {
  478 + //
  479 + }
  480 + }
  481 + break;
  482 + }
  483 + }
  484 +
  485 + function getFirstEntityInfo() {
  486 + var entityInfo;
  487 + for (var id in widgetContext.subscriptions) {
  488 + var subscription = widgetContext.subscriptions[id];
  489 + entityInfo = subscription.getFirstEntityInfo();
  490 + if (entityInfo) {
  491 + break;
  492 + }
  493 + }
  494 + return entityInfo;
  495 + }
  496 +
379 function configureWidgetElement() { 497 function configureWidgetElement() {
380 498
381 $scope.displayLegend = angular.isDefined(widget.config.showLegend) ? 499 $scope.displayLegend = angular.isDefined(widget.config.showLegend) ?
@@ -36,6 +36,7 @@ export default function AddWidgetController($scope, widgetService, entityService @@ -36,6 +36,7 @@ export default function AddWidgetController($scope, widgetService, entityService
36 vm.add = add; 36 vm.add = add;
37 vm.cancel = cancel; 37 vm.cancel = cancel;
38 vm.fetchEntityKeys = fetchEntityKeys; 38 vm.fetchEntityKeys = fetchEntityKeys;
  39 + vm.fetchDashboardStates = fetchDashboardStates;
39 vm.createEntityAlias = createEntityAlias; 40 vm.createEntityAlias = createEntityAlias;
40 41
41 vm.widgetConfig = { 42 vm.widgetConfig = {
@@ -128,6 +129,26 @@ export default function AddWidgetController($scope, widgetService, entityService @@ -128,6 +129,26 @@ export default function AddWidgetController($scope, widgetService, entityService
128 return deferred.promise; 129 return deferred.promise;
129 } 130 }
130 131
  132 + function fetchDashboardStates (query) {
  133 + var deferred = $q.defer();
  134 + var stateIds = Object.keys(vm.dashboard.configuration.states);
  135 + var result = query ? stateIds.filter(
  136 + createFilterForDashboardState(query)) : stateIds;
  137 + if (result && result.length) {
  138 + deferred.resolve(result);
  139 + } else {
  140 + deferred.resolve([query]);
  141 + }
  142 + return deferred.promise;
  143 + }
  144 +
  145 + function createFilterForDashboardState (query) {
  146 + var lowercaseQuery = angular.lowercase(query);
  147 + return function filterFn(stateId) {
  148 + return (angular.lowercase(stateId).indexOf(lowercaseQuery) === 0);
  149 + };
  150 + }
  151 +
131 function createEntityAlias (event, alias, allowedEntityTypes) { 152 function createEntityAlias (event, alias, allowedEntityTypes) {
132 153
133 var deferred = $q.defer(); 154 var deferred = $q.defer();
@@ -42,6 +42,7 @@ @@ -42,6 +42,7 @@
42 alias-controller="vm.aliasController" 42 alias-controller="vm.aliasController"
43 functions-only="vm.functionsOnly" 43 functions-only="vm.functionsOnly"
44 fetch-entity-keys="vm.fetchEntityKeys(entityAliasId, query, type)" 44 fetch-entity-keys="vm.fetchEntityKeys(entityAliasId, query, type)"
  45 + fetch-dashboard-states="vm.fetchDashboardStates(query)"
45 on-create-entity-alias="vm.createEntityAlias(event, alias, allowedEntityTypes)" 46 on-create-entity-alias="vm.createEntityAlias(event, alias, allowedEntityTypes)"
46 the-form="theForm"></tb-widget-config> 47 the-form="theForm"></tb-widget-config>
47 </fieldset> 48 </fieldset>
@@ -94,6 +94,26 @@ export default function EditWidgetDirective($compile, $templateCache, types, wid @@ -94,6 +94,26 @@ export default function EditWidgetDirective($compile, $templateCache, types, wid
94 return deferred.promise; 94 return deferred.promise;
95 }; 95 };
96 96
  97 + scope.fetchDashboardStates = function(query) {
  98 + var deferred = $q.defer();
  99 + var stateIds = Object.keys(scope.dashboard.configuration.states);
  100 + var result = query ? stateIds.filter(
  101 + createFilterForDashboardState(query)) : stateIds;
  102 + if (result && result.length) {
  103 + deferred.resolve(result);
  104 + } else {
  105 + deferred.resolve([query]);
  106 + }
  107 + return deferred.promise;
  108 + }
  109 +
  110 + function createFilterForDashboardState (query) {
  111 + var lowercaseQuery = angular.lowercase(query);
  112 + return function filterFn(stateId) {
  113 + return (angular.lowercase(stateId).indexOf(lowercaseQuery) === 0);
  114 + };
  115 + }
  116 +
97 scope.createEntityAlias = function (event, alias, allowedEntityTypes) { 117 scope.createEntityAlias = function (event, alias, allowedEntityTypes) {
98 118
99 var deferred = $q.defer(); 119 var deferred = $q.defer();
@@ -26,6 +26,7 @@ @@ -26,6 +26,7 @@
26 alias-controller="aliasController" 26 alias-controller="aliasController"
27 functions-only="widgetEditMode" 27 functions-only="widgetEditMode"
28 fetch-entity-keys="fetchEntityKeys(entityAliasId, query, type)" 28 fetch-entity-keys="fetchEntityKeys(entityAliasId, query, type)"
  29 + fetch-dashboard-states="fetchDashboardStates(query)"
29 on-create-entity-alias="createEntityAlias(event, alias, allowedEntityTypes)" 30 on-create-entity-alias="createEntityAlias(event, alias, allowedEntityTypes)"
30 the-form="theForm"></tb-widget-config> 31 the-form="theForm"></tb-widget-config>
31 </fieldset> 32 </fieldset>
@@ -15,7 +15,7 @@ @@ -15,7 +15,7 @@
15 */ 15 */
16 16
17 /*@ngInject*/ 17 /*@ngInject*/
18 -export default function DefaultStateController($scope, $location, $state, $stateParams, $translate, types, dashboardUtils) { 18 +export default function DefaultStateController($scope, $location, $state, $stateParams, utils, types, dashboardUtils) {
19 19
20 var vm = this; 20 var vm = this;
21 21
@@ -50,6 +50,9 @@ export default function DefaultStateController($scope, $location, $state, $state @@ -50,6 +50,9 @@ export default function DefaultStateController($scope, $location, $state, $state
50 } 50 }
51 51
52 function updateState(id, params, openRightLayout) { 52 function updateState(id, params, openRightLayout) {
  53 + if (!id) {
  54 + id = getStateId();
  55 + }
53 if (vm.states && vm.states[id]) { 56 if (vm.states && vm.states[id]) {
54 if (!params) { 57 if (!params) {
55 params = {}; 58 params = {};
@@ -110,15 +113,7 @@ export default function DefaultStateController($scope, $location, $state, $state @@ -110,15 +113,7 @@ export default function DefaultStateController($scope, $location, $state, $state
110 } 113 }
111 114
112 function getStateName(id, state) { 115 function getStateName(id, state) {
113 - var result = '';  
114 - var translationId = types.translate.customTranslationsPrefix + state.name;  
115 - var translation = $translate.instant(translationId);  
116 - if (translation != translationId) {  
117 - result = translation + '';  
118 - } else {  
119 - result = id;  
120 - }  
121 - return result; 116 + return utils.customTranslation(state.name, id);
122 } 117 }
123 118
124 function parseState(stateJson) { 119 function parseState(stateJson) {
@@ -55,6 +55,9 @@ export default function EntityStateController($scope, $location, $state, $stateP @@ -55,6 +55,9 @@ export default function EntityStateController($scope, $location, $state, $stateP
55 } 55 }
56 56
57 function updateState(id, params, openRightLayout) { 57 function updateState(id, params, openRightLayout) {
  58 + if (!id) {
  59 + id = getStateId();
  60 + }
58 if (vm.states && vm.states[id]) { 61 if (vm.states && vm.states[id]) {
59 resolveEntity(params).then( 62 resolveEntity(params).then(
60 function success(entityName) { 63 function success(entityName) {
@@ -121,17 +124,10 @@ export default function EntityStateController($scope, $location, $state, $stateP @@ -121,17 +124,10 @@ export default function EntityStateController($scope, $location, $state, $stateP
121 var result = ''; 124 var result = '';
122 if (vm.stateObject[index]) { 125 if (vm.stateObject[index]) {
123 var stateName = vm.states[vm.stateObject[index].id].name; 126 var stateName = vm.states[vm.stateObject[index].id].name;
124 - var translationId = types.translate.customTranslationsPrefix + stateName;  
125 - var translation = $translate.instant(translationId);  
126 - if (translation != translationId) {  
127 - stateName = translation + '';  
128 - } 127 + stateName = utils.customTranslation(stateName, stateName);
129 var params = vm.stateObject[index].params; 128 var params = vm.stateObject[index].params;
130 - if (params && params.entityName) {  
131 - result = utils.insertVariable(stateName, 'entityName', params.entityName);  
132 - } else {  
133 - result = stateName;  
134 - } 129 + var entityName = params && params.entityName ? params.entityName : '';
  130 + result = utils.insertVariable(stateName, 'entityName', entityName);
135 } 131 }
136 return result; 132 return result;
137 } 133 }
@@ -102,6 +102,7 @@ export default function AddWidgetToDashboardDialogController($scope, $mdDialog, @@ -102,6 +102,7 @@ export default function AddWidgetToDashboardDialogController($scope, $mdDialog,
102 if (vm.addToDashboardType === 0) { 102 if (vm.addToDashboardType === 0) {
103 dashboardService.getDashboard(vm.dashboardId).then( 103 dashboardService.getDashboard(vm.dashboardId).then(
104 function success(dashboard) { 104 function success(dashboard) {
  105 + dashboard = dashboardUtils.validateAndUpdateDashboard(dashboard);
105 selectTargetState($event, dashboard).then( 106 selectTargetState($event, dashboard).then(
106 function(targetState) { 107 function(targetState) {
107 selectTargetLayout($event, dashboard, targetState).then( 108 selectTargetLayout($event, dashboard, targetState).then(
@@ -1104,11 +1104,16 @@ export default angular.module('thingsboard.locale', []) @@ -1104,11 +1104,16 @@ export default angular.module('thingsboard.locale', [])
1104 "export": "Export widget" 1104 "export": "Export widget"
1105 }, 1105 },
1106 "widget-action": { 1106 "widget-action": {
1107 - "header-button": "Header button", 1107 + "header-button": "Widget header button",
1108 "open-dashboard-state": "Navigate to new dashboard state", 1108 "open-dashboard-state": "Navigate to new dashboard state",
1109 "update-dashboard-state": "Update current dashboard state", 1109 "update-dashboard-state": "Update current dashboard state",
1110 "open-dashboard": "Navigate to other dashboard", 1110 "open-dashboard": "Navigate to other dashboard",
1111 - "custom": "Custom action" 1111 + "custom": "Custom action",
  1112 + "target-dashboard-state": "Target dashboard state",
  1113 + "target-dashboard-state-required": "Target dashboard state is required",
  1114 + "set-entity-from-widget": "Set entity from widget",
  1115 + "target-dashboard": "Target dashboard",
  1116 + "open-right-layout": "Open right dashboard layout (mobile view)"
1112 }, 1117 },
1113 "widgets-bundle": { 1118 "widgets-bundle": {
1114 "current": "Current bundle", 1119 "current": "Current bundle",
@@ -1174,6 +1179,7 @@ export default angular.module('thingsboard.locale', []) @@ -1174,6 +1179,7 @@ export default angular.module('thingsboard.locale', [])
1174 "action-source-required": "Action source is required.", 1179 "action-source-required": "Action source is required.",
1175 "action-name": "Name", 1180 "action-name": "Name",
1176 "action-name-required": "Action name is required.", 1181 "action-name-required": "Action name is required.",
  1182 + "action-name-not-unique": "Another action with the same name already exists.<br/>Action name should be unique within the same action source.",
1177 "action-icon": "Icon", 1183 "action-icon": "Icon",
1178 "action-type": "Type", 1184 "action-type": "Type",
1179 "action-type-required": "Action type is required.", 1185 "action-type-required": "Action type is required.",
@@ -1205,6 +1211,10 @@ export default angular.module('thingsboard.locale', []) @@ -1205,6 +1211,10 @@ export default angular.module('thingsboard.locale', [])
1205 "es_ES": "Spanish" 1211 "es_ES": "Spanish"
1206 }, 1212 },
1207 "custom": { 1213 "custom": {
  1214 + "widget-action": {
  1215 + "action-cell-button": "Action cell button",
  1216 + "row-click": "On row click"
  1217 + }
1208 } 1218 }
1209 } 1219 }
1210 } 1220 }
@@ -158,13 +158,7 @@ function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdDia @@ -158,13 +158,7 @@ function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdDia
158 vm.ctx.widgetActions = [ vm.searchAction ]; 158 vm.ctx.widgetActions = [ vm.searchAction ];
159 159
160 if (vm.settings.alarmsTitle && vm.settings.alarmsTitle.length) { 160 if (vm.settings.alarmsTitle && vm.settings.alarmsTitle.length) {
161 - var translationId = types.translate.customTranslationsPrefix + vm.settings.alarmsTitle;  
162 - var translation = $translate.instant(translationId);  
163 - if (translation != translationId) {  
164 - vm.alarmsTitle = translation + '';  
165 - } else {  
166 - vm.alarmsTitle = vm.settings.alarmsTitle;  
167 - } 161 + vm.alarmsTitle = utils.customTranslation(vm.settings.alarmsTitle, vm.settings.alarmsTitle);
168 } else { 162 } else {
169 vm.alarmsTitle = $translate.instant('alarm.alarms'); 163 vm.alarmsTitle = $translate.instant('alarm.alarms');
170 } 164 }
@@ -226,6 +220,9 @@ function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdDia @@ -226,6 +220,9 @@ function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdDia
226 'table.md-table td.md-cell.md-checkbox-cell md-checkbox:not(.md-checked) .md-icon {\n'+ 220 'table.md-table td.md-cell.md-checkbox-cell md-checkbox:not(.md-checked) .md-icon {\n'+
227 'border-color: ' + mdDarkSecondary + ';\n'+ 221 'border-color: ' + mdDarkSecondary + ';\n'+
228 '}\n'+ 222 '}\n'+
  223 + 'table.md-table td.md-cell.tb-action-cell button.md-icon-button md-icon {\n'+
  224 + 'color: ' + mdDarkSecondary + ';\n'+
  225 + '}\n'+
229 'table.md-table td.md-cell.md-placeholder {\n'+ 226 'table.md-table td.md-cell.md-placeholder {\n'+
230 'color: ' + mdDarkDisabled + ';\n'+ 227 'color: ' + mdDarkDisabled + ';\n'+
231 '}\n'+ 228 '}\n'+
@@ -539,13 +536,7 @@ function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdDia @@ -539,13 +536,7 @@ function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdDia
539 for (var d = 0; d < vm.alarmSource.dataKeys.length; d++ ) { 536 for (var d = 0; d < vm.alarmSource.dataKeys.length; d++ ) {
540 var dataKey = vm.alarmSource.dataKeys[d]; 537 var dataKey = vm.alarmSource.dataKeys[d];
541 538
542 - var translationId = types.translate.customTranslationsPrefix + dataKey.label;  
543 - var translation = $translate.instant(translationId);  
544 - if (translation != translationId) {  
545 - dataKey.title = translation + '';  
546 - } else {  
547 - dataKey.title = dataKey.label;  
548 - } 539 + dataKey.title = utils.customTranslation(dataKey.label, dataKey.label);
549 540
550 var keySettings = dataKey.settings; 541 var keySettings = dataKey.settings;
551 542
@@ -65,8 +65,9 @@ function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $tra @@ -65,8 +65,9 @@ function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $tra
65 vm.currentEntity = null; 65 vm.currentEntity = null;
66 66
67 vm.displayEntityName = true; 67 vm.displayEntityName = true;
  68 + vm.entityNameColumnTitle = '';
68 vm.displayEntityType = true; 69 vm.displayEntityType = true;
69 - vm.displayActions = false; //TODO: Widget actions 70 + vm.actionCellDescriptors = [];
70 vm.displayPagination = true; 71 vm.displayPagination = true;
71 vm.defaultPageSize = 10; 72 vm.defaultPageSize = 10;
72 vm.defaultSortOrder = 'entityName'; 73 vm.defaultSortOrder = 'entityName';
@@ -92,6 +93,7 @@ function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $tra @@ -92,6 +93,7 @@ function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $tra
92 vm.onReorder = onReorder; 93 vm.onReorder = onReorder;
93 vm.onPaginate = onPaginate; 94 vm.onPaginate = onPaginate;
94 vm.onRowClick = onRowClick; 95 vm.onRowClick = onRowClick;
  96 + vm.onActionButtonClick = onActionButtonClick;
95 vm.isCurrent = isCurrent; 97 vm.isCurrent = isCurrent;
96 98
97 vm.cellStyle = cellStyle; 99 vm.cellStyle = cellStyle;
@@ -141,14 +143,10 @@ function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $tra @@ -141,14 +143,10 @@ function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $tra
141 143
142 vm.ctx.widgetActions = [ vm.searchAction ]; 144 vm.ctx.widgetActions = [ vm.searchAction ];
143 145
  146 + vm.actionCellDescriptors = vm.ctx.actionsApi.getActionDescriptors('actionCellButton');
  147 +
144 if (vm.settings.entitiesTitle && vm.settings.entitiesTitle.length) { 148 if (vm.settings.entitiesTitle && vm.settings.entitiesTitle.length) {
145 - var translationId = types.translate.customTranslationsPrefix + vm.settings.entitiesTitle;  
146 - var translation = $translate.instant(translationId);  
147 - if (translation != translationId) {  
148 - vm.entitiesTitle = translation + '';  
149 - } else {  
150 - vm.entitiesTitle = vm.settings.entitiesTitle;  
151 - } 149 + vm.entitiesTitle = utils.customTranslation(vm.settings.entitiesTitle, vm.settings.entitiesTitle);
152 } else { 150 } else {
153 vm.entitiesTitle = $translate.instant('entity.entities'); 151 vm.entitiesTitle = $translate.instant('entity.entities');
154 } 152 }
@@ -157,6 +155,13 @@ function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $tra @@ -157,6 +155,13 @@ function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $tra
157 155
158 vm.searchAction.show = angular.isDefined(vm.settings.enableSearch) ? vm.settings.enableSearch : true; 156 vm.searchAction.show = angular.isDefined(vm.settings.enableSearch) ? vm.settings.enableSearch : true;
159 vm.displayEntityName = angular.isDefined(vm.settings.displayEntityName) ? vm.settings.displayEntityName : true; 157 vm.displayEntityName = angular.isDefined(vm.settings.displayEntityName) ? vm.settings.displayEntityName : true;
  158 +
  159 + if (vm.settings.entityNameColumnTitle && vm.settings.entityNameColumnTitle.length) {
  160 + vm.entityNameColumnTitle = utils.customTranslation(vm.settings.entityNameColumnTitle, vm.settings.entityNameColumnTitle);
  161 + } else {
  162 + vm.entityNameColumnTitle = $translate.instant('entity.entity-name');
  163 + }
  164 +
160 vm.displayEntityType = angular.isDefined(vm.settings.displayEntityType) ? vm.settings.displayEntityType : true; 165 vm.displayEntityType = angular.isDefined(vm.settings.displayEntityType) ? vm.settings.displayEntityType : true;
161 vm.displayPagination = angular.isDefined(vm.settings.displayPagination) ? vm.settings.displayPagination : true; 166 vm.displayPagination = angular.isDefined(vm.settings.displayPagination) ? vm.settings.displayPagination : true;
162 167
@@ -185,6 +190,8 @@ function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $tra @@ -185,6 +190,8 @@ function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $tra
185 //var mdDarkIcon = mdDarkSecondary; 190 //var mdDarkIcon = mdDarkSecondary;
186 var mdDarkDivider = defaultColor.setAlpha(0.12).toRgbString(); 191 var mdDarkDivider = defaultColor.setAlpha(0.12).toRgbString();
187 192
  193 + //md-icon.md-default-theme, md-icon {
  194 +
188 var cssString = 'table.md-table th.md-column {\n'+ 195 var cssString = 'table.md-table th.md-column {\n'+
189 'color: ' + mdDarkSecondary + ';\n'+ 196 'color: ' + mdDarkSecondary + ';\n'+
190 '}\n'+ 197 '}\n'+
@@ -204,6 +211,9 @@ function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $tra @@ -204,6 +211,9 @@ function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $tra
204 'table.md-table td.md-cell.md-checkbox-cell md-checkbox:not(.md-checked) .md-icon {\n'+ 211 'table.md-table td.md-cell.md-checkbox-cell md-checkbox:not(.md-checked) .md-icon {\n'+
205 'border-color: ' + mdDarkSecondary + ';\n'+ 212 'border-color: ' + mdDarkSecondary + ';\n'+
206 '}\n'+ 213 '}\n'+
  214 + 'table.md-table td.md-cell.tb-action-cell button.md-icon-button md-icon {\n'+
  215 + 'color: ' + mdDarkSecondary + ';\n'+
  216 + '}\n'+
207 'table.md-table td.md-cell.md-placeholder {\n'+ 217 'table.md-table td.md-cell.md-placeholder {\n'+
208 'color: ' + mdDarkDisabled + ';\n'+ 218 'color: ' + mdDarkDisabled + ';\n'+
209 '}\n'+ 219 '}\n'+
@@ -261,11 +271,37 @@ function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $tra @@ -261,11 +271,37 @@ function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $tra
261 } 271 }
262 272
263 function onRowClick($event, entity) { 273 function onRowClick($event, entity) {
  274 + if ($event) {
  275 + $event.stopPropagation();
  276 + }
264 if (vm.currentEntity != entity) { 277 if (vm.currentEntity != entity) {
265 vm.currentEntity = entity; 278 vm.currentEntity = entity;
  279 + var descriptors = vm.ctx.actionsApi.getActionDescriptors('rowClick');
  280 + if (descriptors.length) {
  281 + var entityId;
  282 + var entityName;
  283 + if (vm.currentEntity) {
  284 + entityId = vm.currentEntity.id;
  285 + entityName = vm.currentEntity.entityName;
  286 + }
  287 + vm.ctx.actionsApi.handleWidgetAction($event, descriptors[0], entityId, entityName);
  288 + }
266 } 289 }
267 } 290 }
268 291
  292 + function onActionButtonClick($event, entity, actionDescriptor) {
  293 + if ($event) {
  294 + $event.stopPropagation();
  295 + }
  296 + var entityId;
  297 + var entityName;
  298 + if (entity) {
  299 + entityId = entity.id;
  300 + entityName = entity.entityName;
  301 + }
  302 + vm.ctx.actionsApi.handleWidgetAction($event, actionDescriptor, entityId, entityName);
  303 + }
  304 +
269 function isCurrent(entity) { 305 function isCurrent(entity) {
270 return (vm.currentEntity && entity && vm.currentEntity.id && entity.id) && 306 return (vm.currentEntity && entity && vm.currentEntity.id && entity.id) &&
271 (vm.currentEntity.id.id === entity.id.id); 307 (vm.currentEntity.id.id === entity.id.id);
@@ -393,13 +429,7 @@ function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $tra @@ -393,13 +429,7 @@ function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $tra
393 } 429 }
394 vm.dataKeys.push(dataKey); 430 vm.dataKeys.push(dataKey);
395 431
396 - var translationId = types.translate.customTranslationsPrefix + dataKey.label;  
397 - var translation = $translate.instant(translationId);  
398 - if (translation != translationId) {  
399 - dataKey.title = translation + '';  
400 - } else {  
401 - dataKey.title = dataKey.label;  
402 - } 432 + dataKey.title = utils.customTranslation(dataKey.label, dataKey.label);
403 433
404 var keySettings = dataKey.settings; 434 var keySettings = dataKey.settings;
405 435
@@ -41,10 +41,10 @@ @@ -41,10 +41,10 @@
41 <table md-table> 41 <table md-table>
42 <thead md-head md-order="vm.query.order" md-on-reorder="vm.onReorder"> 42 <thead md-head md-order="vm.query.order" md-on-reorder="vm.onReorder">
43 <tr md-row> 43 <tr md-row>
44 - <th md-column ng-if="vm.displayEntityName" md-order-by="entityName"><span translate>entity.entity-name</span></th> 44 + <th md-column ng-if="vm.displayEntityName" md-order-by="entityName"><span>{{vm.entityNameColumnTitle}}</span></th>
45 <th md-column ng-if="vm.displayEntityType" md-order-by="entityType"><span translate>entity.entity-type</span></th> 45 <th md-column ng-if="vm.displayEntityType" md-order-by="entityType"><span translate>entity.entity-type</span></th>
46 <th md-column md-order-by="{{ key.name }}" ng-repeat="key in vm.dataKeys"><span>{{ key.title }}</span></th> 46 <th md-column md-order-by="{{ key.name }}" ng-repeat="key in vm.dataKeys"><span>{{ key.title }}</span></th>
47 - <th md-column ng-if="vm.displayActions"><span>&nbsp</span></th> 47 + <th md-column ng-if="vm.actionCellDescriptors.length"><span>&nbsp</span></th>
48 </tr> 48 </tr>
49 </thead> 49 </thead>
50 <tbody md-body> 50 <tbody md-body>
@@ -57,14 +57,18 @@ @@ -57,14 +57,18 @@
57 ng-style="vm.cellStyle(entity, key)" 57 ng-style="vm.cellStyle(entity, key)"
58 ng-bind-html="vm.cellContent(entity, key)"> 58 ng-bind-html="vm.cellContent(entity, key)">
59 </td> 59 </td>
60 - <td md-cell ng-if="vm.displayActions" class="tb-action-cell">  
61 - <!--md-button class="md-icon-button" aria-label="{{ 'entity.details' | translate }}"  
62 - ng-click="vm.openEntityDetails($event, entity)">  
63 - <md-icon aria-label="{{ 'entity.details' | translate }}" class="material-icons">more_horiz</md-icon> 60 + <td md-cell ng-if="vm.actionCellDescriptors.length" class="tb-action-cell"
  61 + ng-style="{minWidth: vm.actionCellDescriptors.length*36+'px',
  62 + maxWidth: vm.actionCellDescriptors.length*36+'px',
  63 + width: vm.actionCellDescriptors.length*36+'px'}">
  64 + <md-button class="md-icon-button" ng-repeat="actionDescriptor in vm.actionCellDescriptors"
  65 + aria-label="{{ actionDescriptor.displayName }}"
  66 + ng-click="vm.onActionButtonClick($event, entity, actionDescriptor)">
  67 + <md-icon aria-label="{{ actionDescriptor.displayName }}" class="material-icons">{{actionDescriptor.icon}}</md-icon>
64 <md-tooltip md-direction="top"> 68 <md-tooltip md-direction="top">
65 - {{ 'entity.details' | translate }} 69 + {{ actionDescriptor.displayName }}
66 </md-tooltip> 70 </md-tooltip>
67 - </md-button--> 71 + </md-button>
68 </td> 72 </td>
69 </tr> 73 </tr>
70 </tbody> 74 </tbody>