From 8e90c903bb2ce29efbf9d734402af7d6bf2ecfb4 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 21 Jun 2017 13:43:49 +0300 Subject: [PATCH 1/2] TB-64: Implement widget actions table. --- ui/package.json | 1 + ui/src/app/api/widget.service.js | 31 ++- ui/src/app/common/types.constant.js | 24 ++ ui/src/app/common/utils.service.js | 41 ++- ui/src/app/components/dashboard.directive.js | 2 +- .../app/components/finish-render.directive.js | 33 +++ .../material-icon-select.directive.js | 88 ++++++ .../components/material-icon-select.tpl.html | 26 ++ .../material-icons-dialog.controller.js | 59 ++++ .../app/components/material-icons-dialog.scss | 31 +++ .../components/material-icons-dialog.tpl.html | 62 +++++ .../action/manage-widget-actions.directive.js | 255 ++++++++++++++++++ .../widget/action/manage-widget-actions.scss | 33 +++ .../action/manage-widget-actions.tpl.html | 99 +++++++ .../action/widget-action-dialog.controller.js | 46 ++++ .../action/widget-action-dialog.tpl.html | 81 ++++++ .../{ => widget}/widget-config.directive.js | 34 ++- .../{ => widget}/widget-config.tpl.html | 6 + .../{ => widget}/widget.controller.js | 2 +- .../{ => widget}/widget.directive.js | 6 +- .../app/components/{ => widget}/widget.scss | 0 ui/src/app/dashboard/add-widget.tpl.html | 1 + ui/src/app/dashboard/edit-widget.directive.js | 1 + ui/src/app/dashboard/edit-widget.tpl.html | 1 + ui/src/app/dashboard/index.js | 2 +- .../states/dashboard-state-dialog.tpl.html | 4 +- .../manage-dashboard-states.controller.js | 2 - ui/src/app/layout/index.js | 2 + ui/src/app/locale/locale.constant.js | 30 ++- 29 files changed, 977 insertions(+), 26 deletions(-) create mode 100644 ui/src/app/components/finish-render.directive.js create mode 100644 ui/src/app/components/material-icon-select.directive.js create mode 100644 ui/src/app/components/material-icon-select.tpl.html create mode 100644 ui/src/app/components/material-icons-dialog.controller.js create mode 100644 ui/src/app/components/material-icons-dialog.scss create mode 100644 ui/src/app/components/material-icons-dialog.tpl.html create mode 100644 ui/src/app/components/widget/action/manage-widget-actions.directive.js create mode 100644 ui/src/app/components/widget/action/manage-widget-actions.scss create mode 100644 ui/src/app/components/widget/action/manage-widget-actions.tpl.html create mode 100644 ui/src/app/components/widget/action/widget-action-dialog.controller.js create mode 100644 ui/src/app/components/widget/action/widget-action-dialog.tpl.html rename ui/src/app/components/{ => widget}/widget-config.directive.js (93%) rename ui/src/app/components/{ => widget}/widget-config.tpl.html (98%) rename ui/src/app/components/{ => widget}/widget.controller.js (99%) rename ui/src/app/components/{ => widget}/widget.directive.js (94%) rename ui/src/app/components/{ => widget}/widget.scss (100%) diff --git a/ui/package.json b/ui/package.json index 73d7bcd347..6884543d77 100644 --- a/ui/package.json +++ b/ui/package.json @@ -111,6 +111,7 @@ "ngtemplate-loader": "^1.3.1", "node-sass": "^3.9.3", "postcss-loader": "^0.13.0", + "raw-loader": "^0.5.1", "react-hot-loader": "^3.0.0-beta.6", "sass-loader": "^4.0.2", "style-loader": "^0.13.1", diff --git a/ui/src/app/api/widget.service.js b/ui/src/app/api/widget.service.js index f8f27ed932..e23e916cca 100644 --- a/ui/src/app/api/widget.service.js +++ b/ui/src/app/api/widget.service.js @@ -40,7 +40,7 @@ export default angular.module('thingsboard.api.widget', ['oc.lazyLoad', thingsbo .name; /*@ngInject*/ -function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, types, utils) { +function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, $translate, types, utils) { $window.$ = $; $window.jQuery = $; @@ -548,13 +548,21 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ ' }\n\n' + ' self.typeParameters = function() {\n\n' + - { - useCustomDatasources: false, - maxDatasources: -1 //unlimited - maxDataKeys: -1 //unlimited - } + return { + useCustomDatasources: false, + maxDatasources: -1 //unlimited + maxDataKeys: -1 //unlimited + }; ' }\n\n' + + ' self.actionSources = function() {\n\n' + + return { + 'headerButton': { + name: 'Header button', + multiple: true + } + }; + }\n\n' + ' self.onResize = function() {\n\n' + ' }\n\n' + @@ -611,6 +619,16 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ if (angular.isUndefined(result.typeParameters.maxDataKeys)) { result.typeParameters.maxDataKeys = -1; } + if (angular.isFunction(widgetTypeInstance.actionSources)) { + result.actionSources = widgetTypeInstance.actionSources(); + } else { + result.actionSources = {}; + } + for (var actionSourceId in types.widgetActionSources) { + result.actionSources[actionSourceId] = angular.copy(types.widgetActionSources[actionSourceId]); + result.actionSources[actionSourceId].name = $translate.instant(result.actionSources[actionSourceId].name); + } + return result; } catch (e) { utils.processWidgetException(e); @@ -650,6 +668,7 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ widgetInfo.typeDataKeySettingsSchema = widgetType.dataKeySettingsSchema; } widgetInfo.typeParameters = widgetType.typeParameters; + widgetInfo.actionSources = widgetType.actionSources; putWidgetInfoToCache(widgetInfo, bundleAlias, widgetInfo.alias, isSystem); putWidgetTypeFunctionToCache(widgetType.widgetTypeFunction, bundleAlias, widgetInfo.alias, isSystem); deferred.resolve(widgetInfo); diff --git a/ui/src/app/common/types.constant.js b/ui/src/app/common/types.constant.js index 81ce0fa4f9..1a51a1af2c 100644 --- a/ui/src/app/common/types.constant.js +++ b/ui/src/app/common/types.constant.js @@ -389,6 +389,30 @@ export default angular.module('thingsboard.types', []) } } }, + widgetActionSources: { + 'headerButton': { + name: 'widget-action.header-button', + multiple: true + } + }, + widgetActionTypes: { + openDashboardState: { + name: 'widget-action.open-dashboard-state', + value: 'openDashboardState' + }, + updateDashboardState: { + name: 'widget-action.update-dashboard-state', + value: 'updateDashboardState' + }, + openDashboard: { + name: 'widget-action.open-dashboard', + value: 'openDashboard' + }, + custom: { + name: 'widget-action.custom', + value: 'custom' + } + }, systemBundleAlias: { charts: "charts", cards: "cards" diff --git a/ui/src/app/common/utils.service.js b/ui/src/app/common/utils.service.js index a3433c255f..e6d48ae13a 100644 --- a/ui/src/app/common/utils.service.js +++ b/ui/src/app/common/utils.service.js @@ -13,6 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +/* eslint-disable import/no-unresolved, import/default */ + +import materialIconsCodepoints from 'raw-loader!material-design-icons/iconfont/codepoints'; + +/* eslint-enable import/no-unresolved, import/default */ + import tinycolor from "tinycolor2"; import jsonSchemaDefaults from "json-schema-defaults"; import thingsboardTypes from "./types.constant"; @@ -24,11 +31,14 @@ export default angular.module('thingsboard.utils', [thingsboardTypes]) const varsRegex = /\$\{([^\}]*)\}/g; /*@ngInject*/ -function Utils($mdColorPalette, $rootScope, $window, $translate, types) { +function Utils($mdColorPalette, $rootScope, $window, $translate, $q, $timeout, types) { var predefinedFunctions = {}, predefinedFunctionsList = [], - materialColors = []; + materialColors = [], + materialIcons = []; + + var commonUsedMaterialIcons = [ 'more_horiz', 'close', 'play_arrow' ]; predefinedFunctions['Sin'] = "return Math.round(1000*Math.sin(time/5000));"; predefinedFunctions['Cos'] = "return Math.round(1000*Math.cos(time/5000));"; @@ -122,6 +132,8 @@ function Utils($mdColorPalette, $rootScope, $window, $translate, types) { getDefaultDatasourceJson: getDefaultDatasourceJson, getDefaultAlarmDataKeys: getDefaultAlarmDataKeys, getMaterialColor: getMaterialColor, + getMaterialIcons: getMaterialIcons, + getCommonMaterialIcons: getCommonMaterialIcons, getPredefinedFunctionBody: getPredefinedFunctionBody, getPredefinedFunctionsList: getPredefinedFunctionsList, genMaterialColor: genMaterialColor, @@ -154,6 +166,31 @@ function Utils($mdColorPalette, $rootScope, $window, $translate, types) { return materialColors[colorIndex].value; } + function getMaterialIcons() { + var deferred = $q.defer(); + if (materialIcons.length) { + deferred.resolve(materialIcons); + } else { + $timeout(function() { + var codepointsArray = materialIconsCodepoints.split("\n"); + codepointsArray.forEach(function (codepoint) { + if (codepoint && codepoint.length) { + var values = codepoint.split(' '); + if (values && values.length == 2) { + materialIcons.push(values[0]); + } + } + }); + deferred.resolve(materialIcons); + }); + } + return deferred.promise; + } + + function getCommonMaterialIcons() { + return commonUsedMaterialIcons; + } + function genMaterialColor(str) { var hash = Math.abs(hashCode(str)); return getMaterialColor(hash); diff --git a/ui/src/app/components/dashboard.directive.js b/ui/src/app/components/dashboard.directive.js index d1427b6091..e61abcc644 100644 --- a/ui/src/app/components/dashboard.directive.js +++ b/ui/src/app/components/dashboard.directive.js @@ -20,7 +20,7 @@ import 'javascript-detect-element-resize/detect-element-resize'; import angularGridster from 'angular-gridster'; import thingsboardTypes from '../common/types.constant'; import thingsboardApiWidget from '../api/widget.service'; -import thingsboardWidget from './widget.directive'; +import thingsboardWidget from './widget/widget.directive'; import thingsboardToast from '../services/toast'; import thingsboardTimewindow from './timewindow.directive'; import thingsboardEvents from './tb-event-directives'; diff --git a/ui/src/app/components/finish-render.directive.js b/ui/src/app/components/finish-render.directive.js new file mode 100644 index 0000000000..9092e2db4d --- /dev/null +++ b/ui/src/app/components/finish-render.directive.js @@ -0,0 +1,33 @@ +/* + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export default angular.module('thingsboard.directives.finishRender', []) + .directive('tbOnFinishRender', OnFinishRender) + .name; + +/*@ngInject*/ +function OnFinishRender($timeout) { + return { + restrict: 'A', + link: function (scope, element, attr) { + if (scope.$last === true) { + $timeout(function () { + scope.$emit(attr.tbOnFinishRender); + }); + } + } + }; +} diff --git a/ui/src/app/components/material-icon-select.directive.js b/ui/src/app/components/material-icon-select.directive.js new file mode 100644 index 0000000000..45ab2cdd2c --- /dev/null +++ b/ui/src/app/components/material-icon-select.directive.js @@ -0,0 +1,88 @@ +/* + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import MaterialIconsDialogController from './material-icons-dialog.controller'; + +/* eslint-disable import/no-unresolved, import/default */ + +import materialIconSelectTemplate from './material-icon-select.tpl.html'; +import materialIconsDialogTemplate from './material-icons-dialog.tpl.html'; + +/* eslint-enable import/no-unresolved, import/default */ + + +export default angular.module('thingsboard.directives.materialIconSelect', []) + .controller('MaterialIconsDialogController', MaterialIconsDialogController) + .directive('tbMaterialIconSelect', MaterialIconSelect) + .name; + +/*@ngInject*/ +function MaterialIconSelect($compile, $templateCache, $document, $mdDialog) { + + var linker = function (scope, element, attrs, ngModelCtrl) { + var template = $templateCache.get(materialIconSelectTemplate); + element.html(template); + + scope.tbRequired = angular.isDefined(scope.tbRequired) ? scope.tbRequired : false; + scope.icon = null; + + scope.updateView = function () { + ngModelCtrl.$setViewValue(scope.icon); + } + + ngModelCtrl.$render = function () { + if (ngModelCtrl.$viewValue) { + scope.icon = ngModelCtrl.$viewValue; + } + if (!scope.icon || !scope.icon.length) { + scope.icon = 'more_horiz'; + } + } + + scope.$watch('icon', function () { + scope.updateView(); + }); + + scope.openIconDialog = function($event) { + if ($event) { + $event.stopPropagation(); + } + $mdDialog.show({ + controller: 'MaterialIconsDialogController', + controllerAs: 'vm', + templateUrl: materialIconsDialogTemplate, + parent: angular.element($document[0].body), + locals: {icon: scope.icon}, + skipHide: true, + fullscreen: true, + targetEvent: $event + }).then(function (icon) { + scope.icon = icon; + }); + } + + $compile(element.contents())(scope); + } + + return { + restrict: "E", + require: "^ngModel", + link: linker, + scope: { + tbRequired: '=?', + } + }; +} diff --git a/ui/src/app/components/material-icon-select.tpl.html b/ui/src/app/components/material-icon-select.tpl.html new file mode 100644 index 0000000000..77ef87f2e3 --- /dev/null +++ b/ui/src/app/components/material-icon-select.tpl.html @@ -0,0 +1,26 @@ + +
+ {{icon}} + + + + + + +
\ No newline at end of file diff --git a/ui/src/app/components/material-icons-dialog.controller.js b/ui/src/app/components/material-icons-dialog.controller.js new file mode 100644 index 0000000000..94b18dccb4 --- /dev/null +++ b/ui/src/app/components/material-icons-dialog.controller.js @@ -0,0 +1,59 @@ +/* + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import './material-icons-dialog.scss'; + +/*@ngInject*/ +export default function MaterialIconsDialogController($scope, $mdDialog, $timeout, utils, icon) { + + var vm = this; + + vm.selectedIcon = icon; + + vm.showAll = false; + vm.loadingIcons = false; + + $scope.$watch('vm.showAll', function(showAll) { + if (showAll) { + vm.loadingIcons = true; + $timeout(function() { + utils.getMaterialIcons().then( + function success(icons) { + vm.icons = icons; + } + ); + }); + } else { + vm.icons = utils.getCommonMaterialIcons(); + } + }); + + $scope.$on('iconsLoadFinished', function() { + vm.loadingIcons = false; + }); + + vm.cancel = cancel; + vm.selectIcon = selectIcon; + + function cancel() { + $mdDialog.cancel(); + } + + function selectIcon($event, icon) { + vm.selectedIcon = icon; + $mdDialog.hide(vm.selectedIcon); + } +} diff --git a/ui/src/app/components/material-icons-dialog.scss b/ui/src/app/components/material-icons-dialog.scss new file mode 100644 index 0000000000..dbd83951b2 --- /dev/null +++ b/ui/src/app/components/material-icons-dialog.scss @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.tb-material-icons-dialog { + button.md-icon-button.tb-select-icon-button { + border: solid 1px orange; + border-radius: 0%; + padding: 16px; + height: 56px; + width: 56px; + margin: 10px; + } + .tb-icons-load { + top: 64px; + background: rgba(255,255,255,0.75); + z-index: 3; + } +} \ No newline at end of file diff --git a/ui/src/app/components/material-icons-dialog.tpl.html b/ui/src/app/components/material-icons-dialog.tpl.html new file mode 100644 index 0000000000..8a2eb6537b --- /dev/null +++ b/ui/src/app/components/material-icons-dialog.tpl.html @@ -0,0 +1,62 @@ + + +
+ +
+

{{ 'icon.select-icon' | translate }}

+ +
+ + + +
+ + + +
+
+ + +
+ +
+ +
+ +
+ + {{icon}} + + {{ icon }} + + +
+
+
+
+ + + + {{ 'action.cancel' | translate }} + + +
+
diff --git a/ui/src/app/components/widget/action/manage-widget-actions.directive.js b/ui/src/app/components/widget/action/manage-widget-actions.directive.js new file mode 100644 index 0000000000..b2737296a8 --- /dev/null +++ b/ui/src/app/components/widget/action/manage-widget-actions.directive.js @@ -0,0 +1,255 @@ +/** + * Created by igor on 6/20/17. + */ +/* + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import './manage-widget-actions.scss'; + +import thingsboardMaterialIconSelect from '../../material-icon-select.directive'; + +import WidgetActionDialogController from './widget-action-dialog.controller'; + +/* eslint-disable import/no-unresolved, import/default */ + +import manageWidgetActionsTemplate from './manage-widget-actions.tpl.html'; +import widgetActionDialogTemplate from './widget-action-dialog.tpl.html'; + +/* eslint-enable import/no-unresolved, import/default */ + +export default angular.module('thingsboard.directives.widgetActions', [thingsboardMaterialIconSelect]) + .controller('WidgetActionDialogController', WidgetActionDialogController) + .directive('tbManageWidgetActions', ManageWidgetActions) + .name; + +/*@ngInject*/ +function ManageWidgetActions() { + return { + restrict: "E", + scope: true, + bindToController: { + actionSources: '=', + widgetActions: '=' + }, + controller: ManageWidgetActionsController, + controllerAs: 'vm', + templateUrl: manageWidgetActionsTemplate + }; +} + +/* eslint-disable angular/angularelement */ + + +/*@ngInject*/ +function ManageWidgetActionsController($rootScope, $scope, $document, $mdDialog, $q, $filter, + $translate, $timeout, types) { + + let vm = this; + + vm.allActions = []; + + vm.actions = []; + vm.actionsCount = 0; + + vm.query = { + order: 'actionSourceName', + limit: 10, + page: 1, + search: null + }; + + vm.enterFilterMode = enterFilterMode; + vm.exitFilterMode = exitFilterMode; + vm.onReorder = onReorder; + vm.onPaginate = onPaginate; + vm.addAction = addAction; + vm.editAction = editAction; + vm.deleteAction = deleteAction; + + $timeout(function(){ + $scope.manageWidgetActionsForm.querySearchInput.$pristine = false; + }); + + $scope.$watch('vm.widgetActions', function() { + if (vm.widgetActions) { + reloadActions(); + } + }); + + $scope.$watch("vm.query.search", function(newVal, prevVal) { + if (!angular.equals(newVal, prevVal) && vm.query.search != null) { + updateActions(); + } + }); + + function enterFilterMode () { + vm.query.search = ''; + } + + function exitFilterMode () { + vm.query.search = null; + updateActions(); + } + + function onReorder () { + updateActions(); + } + + function onPaginate () { + updateActions(); + } + + function addAction($event) { + if ($event) { + $event.stopPropagation(); + } + openWidgetActionDialog($event, null, true); + } + + function editAction ($event, action) { + if ($event) { + $event.stopPropagation(); + } + openWidgetActionDialog($event, action, false); + } + + function deleteAction($event, action) { + if ($event) { + $event.stopPropagation(); + } + if (action) { + var title = $translate.instant('widget-config.delete-action-title'); + var content = $translate.instant('widget-config.delete-action-text', {actionName: action.name}); + var confirm = $mdDialog.confirm() + .targetEvent($event) + .title(title) + .htmlContent(content) + .ariaLabel(title) + .cancel($translate.instant('action.no')) + .ok($translate.instant('action.yes')); + + confirm._options.skipHide = true; + confirm._options.fullscreen = true; + + $mdDialog.show(confirm).then(function () { + var index = getActionIndex(action.id, vm.allActions); + if (index > -1) { + vm.allActions.splice(index, 1); + } + var targetActions = vm.widgetActions[action.actionSourceId]; + index = getActionIndex(action.id, targetActions); + if (index > -1) { + targetActions.splice(index, 1); + } + $scope.manageWidgetActionsForm.$setDirty(); + updateActions(); + }); + } + } + + function openWidgetActionDialog($event, action, isAdd) { + var prevActionId = null; + if (!isAdd) { + prevActionId = action.id; + } + $mdDialog.show({ + controller: 'WidgetActionDialogController', + controllerAs: 'vm', + templateUrl: widgetActionDialogTemplate, + parent: angular.element($document[0].body), + locals: {isAdd: isAdd, actionSources: vm.actionSources, action: angular.copy(action)}, + skipHide: true, + fullscreen: true, + targetEvent: $event + }).then(function (action) { + saveAction(action, prevActionId); + updateActions(); + }); + } + + function getActionIndex(id, actions) { + var result = $filter('filter')(actions, {id: id}, true); + if (result && result.length) { + return actions.indexOf(result[0]); + } + return -1; + } + + function saveAction(action, prevActionId) { + action.actionSourceName = vm.actionSources[action.actionSourceId].name; + action.typeName = $translate.instant(types.widgetActionTypes[action.type].name); + var actionSourceId = action.actionSourceId; + var widgetAction = angular.copy(action); + delete widgetAction.actionSourceId; + delete widgetAction.actionSourceName; + delete widgetAction.typeName; + var targetActions = vm.widgetActions[actionSourceId]; + if (!targetActions) { + targetActions = []; + vm.widgetActions[actionSourceId] = targetActions; + } + if (prevActionId) { + var index = getActionIndex(prevActionId, vm.allActions); + if (index > -1) { + vm.allActions[index] = action; + } + index = getActionIndex(prevActionId, targetActions); + if (index > -1) { + targetActions[index] = widgetAction; + } + } else { + vm.allActions.push(action); + targetActions.push(widgetAction); + } + $scope.manageWidgetActionsForm.$setDirty(); + } + + function reloadActions() { + vm.allActions = []; + vm.actions = []; + vm.actionsCount = 0; + + for (var actionSourceId in vm.widgetActions) { + var actionSource = vm.actionSources[actionSourceId]; + var actionSourceActions = vm.widgetActions[actionSourceId]; + for (var i=0;i +
+ +
+ widget-config.actions + + + add + + {{ 'widget-config.add-action' | translate }} + + + + search + + {{ 'action.search' | translate }} + + +
+
+ +
+ + search + + {{ 'widget-config.search-actions' | translate }} + + + + + + + + close + + {{ 'action.close' | translate }} + + +
+
+ + + + + + + + + + + + + + + + + + + + +
widget-config.action-sourcewidget-config.action-namewidget-config.action-iconwidget-config.action-type 
{{action.actionSourceName}}{{action.name}} + {{action.icon}} + {{action.typeName}} + + edit + + {{ 'widget-config.edit-action' | translate }} + + + + delete + + {{ 'widget-config.delete-action' | translate }} + + +
+
+ + +
diff --git a/ui/src/app/components/widget/action/widget-action-dialog.controller.js b/ui/src/app/components/widget/action/widget-action-dialog.controller.js new file mode 100644 index 0000000000..948308b591 --- /dev/null +++ b/ui/src/app/components/widget/action/widget-action-dialog.controller.js @@ -0,0 +1,46 @@ +/* + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*@ngInject*/ +export default function WidgetActionDialogController($scope, $mdDialog, types, utils, isAdd, actionSources, action) { + + var vm = this; + + vm.types = types; + + vm.isAdd = isAdd; + vm.actionSources = actionSources; + + if (vm.isAdd) { + vm.action = { + id: utils.guid() + }; + } else { + vm.action = action; + } + + vm.cancel = cancel; + vm.save = save; + + function cancel() { + $mdDialog.cancel(); + } + + function save() { + $scope.theForm.$setPristine(); + $mdDialog.hide(vm.action); + } +} diff --git a/ui/src/app/components/widget/action/widget-action-dialog.tpl.html b/ui/src/app/components/widget/action/widget-action-dialog.tpl.html new file mode 100644 index 0000000000..ccb9d41bd6 --- /dev/null +++ b/ui/src/app/components/widget/action/widget-action-dialog.tpl.html @@ -0,0 +1,81 @@ + + +
+ +
+

{{ (vm.isAdd ? 'widget-config.add-action' : 'widget-config.edit-action') | translate }}

+ + + + +
+
+ + + +
+ +
+ + + + + {{actionSource.name}} + + +
+
widget-config.action-source-required
+
+
+ + + +
+
widget-config.action-name-required
+
+
+ + + + + + + {{ actionType.name | translate }} + + +
+
widget-config.action-type-required
+
+
+
+
+
+
+ + + + {{ (vm.isAdd ? 'action.add' : 'action.save') | translate }} + + + {{ 'action.cancel' | translate }} + + +
+
diff --git a/ui/src/app/components/widget-config.directive.js b/ui/src/app/components/widget/widget-config.directive.js similarity index 93% rename from ui/src/app/components/widget-config.directive.js rename to ui/src/app/components/widget/widget-config.directive.js index 4a083c10a7..3df9263c59 100644 --- a/ui/src/app/components/widget-config.directive.js +++ b/ui/src/app/components/widget/widget-config.directive.js @@ -14,13 +14,14 @@ * limitations under the License. */ import jsonSchemaDefaults from 'json-schema-defaults'; -import thingsboardTypes from '../common/types.constant'; -import thingsboardUtils from '../common/utils.service'; -import thingsboardEntityAliasSelect from './entity-alias-select.directive'; -import thingsboardDatasource from './datasource.directive'; -import thingsboardTimewindow from './timewindow.directive'; -import thingsboardLegendConfig from './legend-config.directive'; -import thingsboardJsonForm from "./json-form.directive"; +import thingsboardTypes from '../../common/types.constant'; +import thingsboardUtils from '../../common/utils.service'; +import thingsboardEntityAliasSelect from '../entity-alias-select.directive'; +import thingsboardDatasource from '../datasource.directive'; +import thingsboardTimewindow from '../timewindow.directive'; +import thingsboardLegendConfig from '../legend-config.directive'; +import thingsboardJsonForm from '../json-form.directive'; +import thingsboardManageWidgetActions from './action/manage-widget-actions.directive'; import 'angular-ui-ace'; /* eslint-disable import/no-unresolved, import/default */ @@ -38,6 +39,7 @@ export default angular.module('thingsboard.directives.widgetConfig', [thingsboar thingsboardDatasource, thingsboardTimewindow, thingsboardLegendConfig, + thingsboardManageWidgetActions, 'ui.ace']) .directive('tbWidgetConfig', WidgetConfig) .name; @@ -117,6 +119,10 @@ function WidgetConfig($compile, $templateCache, $rootScope, $translate, $timeout scope.showLegend = angular.isDefined(config.showLegend) ? config.showLegend : scope.widgetType === types.widgetType.timeseries.value; scope.legendConfig = config.legendConfig; + scope.actions = config.actions; + if (!scope.actions) { + scope.actions = {}; + } if (scope.widgetType !== types.widgetType.rpc.value && scope.widgetType !== types.widgetType.alarm.value && scope.widgetType !== types.widgetType.static.value @@ -324,6 +330,19 @@ function WidgetConfig($compile, $templateCache, $rootScope, $translate, $timeout } }); + scope.$watch('actions', function () { + if (ngModelCtrl.$viewValue && ngModelCtrl.$viewValue.config) { + var value = ngModelCtrl.$viewValue; + var config = value.config; + config.actions = scope.actions; + ngModelCtrl.$setViewValue(value); + scope.updateValidity(); + /*if (scope.theForm) { + scope.theForm.$setDirty(); + }*/ + } + }, true); + scope.addDatasource = function () { var newDatasource; if (scope.functionsOnly) { @@ -443,6 +462,7 @@ function WidgetConfig($compile, $templateCache, $rootScope, $translate, $timeout isDataEnabled: '=?', widgetType: '=', typeParameters: '=', + actionSources: '=', widgetSettingsSchema: '=', datakeySettingsSchema: '=', aliasController: '=', diff --git a/ui/src/app/components/widget-config.tpl.html b/ui/src/app/components/widget/widget-config.tpl.html similarity index 98% rename from ui/src/app/components/widget-config.tpl.html rename to ui/src/app/components/widget/widget-config.tpl.html index 14cb27c8cd..2cc264c0d7 100644 --- a/ui/src/app/components/widget-config.tpl.html +++ b/ui/src/app/components/widget/widget-config.tpl.html @@ -275,4 +275,10 @@ + + + + + + diff --git a/ui/src/app/components/widget.controller.js b/ui/src/app/components/widget/widget.controller.js similarity index 99% rename from ui/src/app/components/widget.controller.js rename to ui/src/app/components/widget/widget.controller.js index f2e231fa29..3bda80991a 100644 --- a/ui/src/app/components/widget.controller.js +++ b/ui/src/app/components/widget/widget.controller.js @@ -15,7 +15,7 @@ */ import $ from 'jquery'; import 'javascript-detect-element-resize/detect-element-resize'; -import Subscription from '../api/subscription'; +import Subscription from '../../api/subscription'; /* eslint-disable angular/angularelement */ diff --git a/ui/src/app/components/widget.directive.js b/ui/src/app/components/widget/widget.directive.js similarity index 94% rename from ui/src/app/components/widget.directive.js rename to ui/src/app/components/widget/widget.directive.js index a0f1b83c48..40d5a83976 100644 --- a/ui/src/app/components/widget.directive.js +++ b/ui/src/app/components/widget/widget.directive.js @@ -16,9 +16,9 @@ import './widget.scss'; -import thingsboardLegend from './legend.directive'; -import thingsboardTypes from '../common/types.constant'; -import thingsboardApiDatasource from '../api/datasource.service'; +import thingsboardLegend from '../legend.directive'; +import thingsboardTypes from '../../common/types.constant'; +import thingsboardApiDatasource from '../../api/datasource.service'; import WidgetController from './widget.controller'; diff --git a/ui/src/app/components/widget.scss b/ui/src/app/components/widget/widget.scss similarity index 100% rename from ui/src/app/components/widget.scss rename to ui/src/app/components/widget/widget.scss diff --git a/ui/src/app/dashboard/add-widget.tpl.html b/ui/src/app/dashboard/add-widget.tpl.html index 894c23d482..3920f91aeb 100644 --- a/ui/src/app/dashboard/add-widget.tpl.html +++ b/ui/src/app/dashboard/add-widget.tpl.html @@ -34,6 +34,7 @@
- {{ vm.isAdd ? 'Add' : 'Save' }} + {{ (vm.isAdd ? 'action.add' : 'action.save') | translate }} - Cancel + {{ 'action.cancel' | translate }} diff --git a/ui/src/app/dashboard/states/manage-dashboard-states.controller.js b/ui/src/app/dashboard/states/manage-dashboard-states.controller.js index 3595f65705..1aa95b7b02 100644 --- a/ui/src/app/dashboard/states/manage-dashboard-states.controller.js +++ b/ui/src/app/dashboard/states/manage-dashboard-states.controller.js @@ -175,8 +175,6 @@ export default function ManageDashboardStatesController($scope, $mdDialog, $filt $scope.theForm.$setDirty(); updateStates(); }); - - } } diff --git a/ui/src/app/layout/index.js b/ui/src/app/layout/index.js index 2a27c93806..9d3e1d16c6 100644 --- a/ui/src/app/layout/index.js +++ b/ui/src/app/layout/index.js @@ -26,6 +26,7 @@ import thingsboardApiLogin from '../api/login.service'; import thingsboardApiUser from '../api/user.service'; import thingsboardNoAnimate from '../components/no-animate.directive'; +import thingsboardOnFinishRender from '../components/finish-render.directive'; import thingsboardSideMenu from '../components/side-menu.directive'; import thingsboardDashboardAutocomplete from '../components/dashboard-autocomplete.directive'; @@ -81,6 +82,7 @@ export default angular.module('thingsboard.home', [ thingsboardApiLogin, thingsboardApiUser, thingsboardNoAnimate, + thingsboardOnFinishRender, thingsboardSideMenu, thingsboardDashboardAutocomplete ]) diff --git a/ui/src/app/locale/locale.constant.js b/ui/src/app/locale/locale.constant.js index 4710b9281f..63f20e5410 100644 --- a/ui/src/app/locale/locale.constant.js +++ b/ui/src/app/locale/locale.constant.js @@ -1103,6 +1103,13 @@ export default angular.module('thingsboard.locale', []) "undo": "Undo widget changes", "export": "Export widget" }, + "widget-action": { + "header-button": "Header button", + "open-dashboard-state": "Navigate to new dashboard state", + "update-dashboard-state": "Update current dashboard state", + "open-dashboard": "Navigate to other dashboard", + "custom": "Custom action" + }, "widgets-bundle": { "current": "Current bundle", "widgets-bundles": "Widgets Bundles", @@ -1158,7 +1165,22 @@ export default angular.module('thingsboard.locale', []) "remove-datasource": "Remove datasource", "add-datasource": "Add datasource", "target-device": "Target device", - "alarm-source": "Alarm source" + "alarm-source": "Alarm source", + "actions": "Actions", + "action": "Action", + "add-action": "Add action", + "search-actions": "Search actions", + "action-source": "Action source", + "action-source-required": "Action source is required.", + "action-name": "Name", + "action-name-required": "Action name is required.", + "action-icon": "Icon", + "action-type": "Type", + "action-type-required": "Action type is required.", + "edit-action": "Edit action", + "delete-action": "Delete action", + "delete-action-title": "Delete widget action", + "delete-action-text": "Are you sure you want delete widget action with name '{{actionName}}'?" }, "widget-type": { "import": "Import widget type", @@ -1168,6 +1190,12 @@ export default angular.module('thingsboard.locale', []) "widget-type-file": "Widget type file", "invalid-widget-type-file-error": "Unable to import widget type: Invalid widget type data structure." }, + "icon": { + "icon": "Icon", + "select-icon": "Select icon", + "material-icons": "Material icons", + "show-all": "Show all icons" + }, "language": { "language": "Language", "en_US": "English", From 2fdb372c893c842afdc5884d14a20a6e120b9d13 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 22 Jun 2017 17:40:26 +0300 Subject: [PATCH 2/2] TB-64: Implement widget actions. --- ui/src/app/api/subscription.js | 43 ++++++ ui/src/app/api/widget.service.js | 2 +- ui/src/app/common/types.constant.js | 3 +- ui/src/app/common/utils.service.js | 23 +++- .../dashboard-autocomplete.directive.js | 24 +++- ui/src/app/components/dashboard.directive.js | 10 ++ ui/src/app/components/dashboard.tpl.html | 10 ++ .../material-icon-select.directive.js | 2 + .../app/components/material-icon-select.scss | 27 ++++ .../components/material-icon-select.tpl.html | 8 +- .../action/manage-widget-actions.directive.js | 41 ++++-- .../action/widget-action-dialog.controller.js | 127 +++++++++++++++++- .../action/widget-action-dialog.tpl.html | 56 +++++++- .../widget/widget-config.directive.js | 1 + .../components/widget/widget-config.tpl.html | 5 +- .../components/widget/widget.controller.js | 120 ++++++++++++++++- ui/src/app/dashboard/add-widget.controller.js | 21 +++ ui/src/app/dashboard/add-widget.tpl.html | 1 + ui/src/app/dashboard/edit-widget.directive.js | 20 +++ ui/src/app/dashboard/edit-widget.tpl.html | 1 + .../states/default-state-controller.js | 15 +-- .../states/entity-state-controller.js | 16 +-- ...d-widget-to-dashboard-dialog.controller.js | 1 + ui/src/app/locale/locale.constant.js | 14 +- ui/src/app/widget/lib/alarms-table-widget.js | 19 +-- .../app/widget/lib/entities-table-widget.js | 60 ++++++--- .../widget/lib/entities-table-widget.tpl.html | 20 +-- 27 files changed, 595 insertions(+), 95 deletions(-) create mode 100644 ui/src/app/components/material-icon-select.scss diff --git a/ui/src/app/api/subscription.js b/ui/src/app/api/subscription.js index 215da0f5ad..dd93626ed9 100644 --- a/ui/src/app/api/subscription.js +++ b/ui/src/app/api/subscription.js @@ -171,6 +171,48 @@ export default class Subscription { return deferred.promise; } + getFirstEntityInfo() { + var entityId; + var entityName; + if (this.type === this.ctx.types.widgetType.rpc.value) { + if (this.targetDeviceId) { + entityId = { + entityType: this.ctx.entityType.device, + id: this.targetDeviceId + } + entityName = this.targetDeviceName; + } + } else if (this.type == this.ctx.types.widgetType.alarm.value) { + if (this.alarmSource && this.alarmSource.entityType && this.alarmSource.entityId) { + entityId = { + entityType: this.alarmSource.entityType, + id: this.alarmSource.entityId + } + entityName = this.alarmSource.entityName; + } + } else { + for (var i=0;i 0) { scope.dashboard = dashboards[0]; + scope.updateView(); } }, function fail() { }); diff --git a/ui/src/app/components/dashboard.directive.js b/ui/src/app/components/dashboard.directive.js index e61abcc644..1a4cdba4e4 100644 --- a/ui/src/app/components/dashboard.directive.js +++ b/ui/src/app/components/dashboard.directive.js @@ -187,6 +187,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $ vm.showWidgetActions = showWidgetActions; vm.widgetTitleStyle = widgetTitleStyle; vm.widgetTitle = widgetTitle; + vm.customWidgetHeaderActions = customWidgetHeaderActions; vm.widgetActions = widgetActions; vm.dropWidgetShadow = dropWidgetShadow; vm.enableWidgetFullscreen = enableWidgetFullscreen; @@ -875,6 +876,15 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $ } } + function customWidgetHeaderActions(widget) { + var ctx = widgetContext(widget); + if (ctx && ctx.customHeaderActions && ctx.customHeaderActions.length) { + return ctx.customHeaderActions; + } else { + return []; + } + } + function widgetActions(widget) { var ctx = widgetContext(widget); if (ctx && ctx.widgetActions && ctx.widgetActions.length) { diff --git a/ui/src/app/components/dashboard.tpl.html b/ui/src/app/components/dashboard.tpl.html index aea3dfcf32..5d46fea5ca 100644 --- a/ui/src/app/components/dashboard.tpl.html +++ b/ui/src/app/components/dashboard.tpl.html @@ -52,6 +52,16 @@
+ + + {{action.displayName}} + + + -
+
{{icon}} - - - - + +
\ No newline at end of file diff --git a/ui/src/app/components/widget/action/manage-widget-actions.directive.js b/ui/src/app/components/widget/action/manage-widget-actions.directive.js index b2737296a8..93ca06fdd1 100644 --- a/ui/src/app/components/widget/action/manage-widget-actions.directive.js +++ b/ui/src/app/components/widget/action/manage-widget-actions.directive.js @@ -42,7 +42,8 @@ function ManageWidgetActions() { scope: true, bindToController: { actionSources: '=', - widgetActions: '=' + widgetActions: '=', + fetchDashboardStates: '&', }, controller: ManageWidgetActionsController, controllerAs: 'vm', @@ -55,7 +56,7 @@ function ManageWidgetActions() { /*@ngInject*/ function ManageWidgetActionsController($rootScope, $scope, $document, $mdDialog, $q, $filter, - $translate, $timeout, types) { + $translate, $timeout, utils, types) { let vm = this; @@ -165,12 +166,30 @@ function ManageWidgetActionsController($rootScope, $scope, $document, $mdDialog, if (!isAdd) { prevActionId = action.id; } + var availableActionSources = {}; + for (var id in vm.actionSources) { + var actionSource = vm.actionSources[id]; + if (actionSource.multiple) { + availableActionSources[id] = actionSource; + } else { + if (!isAdd && action.actionSourceId == id) { + availableActionSources[id] = actionSource; + } else { + var result = $filter('filter')(vm.allActions, {actionSourceId: id}); + if (!result || !result.length) { + availableActionSources[id] = actionSource; + } + } + } + } $mdDialog.show({ controller: 'WidgetActionDialogController', controllerAs: 'vm', templateUrl: widgetActionDialogTemplate, parent: angular.element($document[0].body), - locals: {isAdd: isAdd, actionSources: vm.actionSources, action: angular.copy(action)}, + locals: {isAdd: isAdd, fetchDashboardStates: vm.fetchDashboardStates, + actionSources: availableActionSources, widgetActions: vm.widgetActions, + action: angular.copy(action)}, skipHide: true, fullscreen: true, targetEvent: $event @@ -189,7 +208,8 @@ function ManageWidgetActionsController($rootScope, $scope, $document, $mdDialog, } function saveAction(action, prevActionId) { - action.actionSourceName = vm.actionSources[action.actionSourceId].name; + var actionSourceName = vm.actionSources[action.actionSourceId].name; + action.actionSourceName = utils.customTranslation(actionSourceName, actionSourceName); action.typeName = $translate.instant(types.widgetActionTypes[action.type].name); var actionSourceId = action.actionSourceId; var widgetAction = angular.copy(action); @@ -227,15 +247,10 @@ function ManageWidgetActionsController($rootScope, $scope, $document, $mdDialog, var actionSourceActions = vm.widgetActions[actionSourceId]; for (var i=0;i
-
+
- {{actionSource.name}} + {{vm.actionSourceName(actionSource)}}
@@ -48,6 +48,7 @@
widget-config.action-name-required
+
widget-config.action-name-not-unique
@@ -63,6 +64,57 @@
widget-config.action-type-required
+
+
widget-action.target-dashboard
+ + +
+ + +
+ {{item}} +
+
+
+
widget-action.target-dashboard-state-required
+
+
+ {{ 'widget-action.open-right-layout' | translate }} + + {{ 'widget-action.set-entity-from-widget' | translate }} + + +
diff --git a/ui/src/app/components/widget/widget-config.directive.js b/ui/src/app/components/widget/widget-config.directive.js index 3df9263c59..afdaf8d745 100644 --- a/ui/src/app/components/widget/widget-config.directive.js +++ b/ui/src/app/components/widget/widget-config.directive.js @@ -468,6 +468,7 @@ function WidgetConfig($compile, $templateCache, $rootScope, $translate, $timeout aliasController: '=', functionsOnly: '=', fetchEntityKeys: '&', + fetchDashboardStates: '&', onCreateEntityAlias: '&', theForm: '=' }, diff --git a/ui/src/app/components/widget/widget-config.tpl.html b/ui/src/app/components/widget/widget-config.tpl.html index 2cc264c0d7..f2ee0f112d 100644 --- a/ui/src/app/components/widget/widget-config.tpl.html +++ b/ui/src/app/components/widget/widget-config.tpl.html @@ -277,7 +277,10 @@ - + diff --git a/ui/src/app/components/widget/widget.controller.js b/ui/src/app/components/widget/widget.controller.js index b2877f3d38..113399a8cc 100644 --- a/ui/src/app/components/widget/widget.controller.js +++ b/ui/src/app/components/widget/widget.controller.js @@ -20,7 +20,7 @@ import Subscription from '../../api/subscription'; /* eslint-disable angular/angularelement */ /*@ngInject*/ -export default function WidgetController($scope, $timeout, $window, $element, $q, $log, $injector, $filter, $compile, tbRaf, types, utils, timeService, +export default function WidgetController($scope, $state, $timeout, $window, $element, $q, $log, $injector, $filter, $compile, tbRaf, types, utils, timeService, datasourceService, alarmService, entityService, deviceService, visibleRect, isEdit, isMobile, stDiff, dashboardTimewindow, dashboardTimewindowApi, widget, aliasController, stateController, widgetInfo, widgetType) { @@ -44,6 +44,20 @@ export default function WidgetController($scope, $timeout, $window, $element, $q var cafs = {}; + var actionDescriptorsBySourceId = {}; + if (widget.config.actions) { + for (var actionSourceId in widget.config.actions) { + var descriptors = widget.config.actions[actionSourceId]; + var actionDescriptors = []; + descriptors.forEach(function(descriptor) { + var actionDescriptor = angular.copy(descriptor); + actionDescriptor.displayName = utils.customTranslation(descriptor.name, descriptor.name); + actionDescriptors.push(actionDescriptor); + }); + actionDescriptorsBySourceId[actionSourceId] = actionDescriptors; + } + } + var widgetContext = { inited: false, $container: null, @@ -103,9 +117,32 @@ export default function WidgetController($scope, $timeout, $window, $element, $q utils: { formatValue: formatValue }, + actionsApi: { + actionDescriptorsBySourceId: actionDescriptorsBySourceId, + getActionDescriptors: getActionDescriptors, + handleWidgetAction: handleWidgetAction + }, stateController: stateController }; + widgetContext.customHeaderActions = []; + var headerActionsDescriptors = getActionDescriptors(types.widgetActionSources.headerButton.value); + for (var i=0;i 0) { + try { + var customActionFunction = new Function('$event', 'widgetContext', 'entityId', 'entityName', customFunction); + customActionFunction($event, widgetContext, entityId, entityName); + } catch (e) { + // + } + } + break; + } + } + + function getFirstEntityInfo() { + var entityInfo; + for (var id in widgetContext.subscriptions) { + var subscription = widgetContext.subscriptions[id]; + entityInfo = subscription.getFirstEntityInfo(); + if (entityInfo) { + break; + } + } + return entityInfo; + } + function configureWidgetElement() { $scope.displayLegend = angular.isDefined(widget.config.showLegend) ? diff --git a/ui/src/app/dashboard/add-widget.controller.js b/ui/src/app/dashboard/add-widget.controller.js index fd2ada7b4a..c15492dbd9 100644 --- a/ui/src/app/dashboard/add-widget.controller.js +++ b/ui/src/app/dashboard/add-widget.controller.js @@ -36,6 +36,7 @@ export default function AddWidgetController($scope, widgetService, entityService vm.add = add; vm.cancel = cancel; vm.fetchEntityKeys = fetchEntityKeys; + vm.fetchDashboardStates = fetchDashboardStates; vm.createEntityAlias = createEntityAlias; vm.widgetConfig = { @@ -128,6 +129,26 @@ export default function AddWidgetController($scope, widgetService, entityService return deferred.promise; } + function fetchDashboardStates (query) { + var deferred = $q.defer(); + var stateIds = Object.keys(vm.dashboard.configuration.states); + var result = query ? stateIds.filter( + createFilterForDashboardState(query)) : stateIds; + if (result && result.length) { + deferred.resolve(result); + } else { + deferred.resolve([query]); + } + return deferred.promise; + } + + function createFilterForDashboardState (query) { + var lowercaseQuery = angular.lowercase(query); + return function filterFn(stateId) { + return (angular.lowercase(stateId).indexOf(lowercaseQuery) === 0); + }; + } + function createEntityAlias (event, alias, allowedEntityTypes) { var deferred = $q.defer(); diff --git a/ui/src/app/dashboard/add-widget.tpl.html b/ui/src/app/dashboard/add-widget.tpl.html index 3920f91aeb..4ca29f0b01 100644 --- a/ui/src/app/dashboard/add-widget.tpl.html +++ b/ui/src/app/dashboard/add-widget.tpl.html @@ -42,6 +42,7 @@ alias-controller="vm.aliasController" functions-only="vm.functionsOnly" fetch-entity-keys="vm.fetchEntityKeys(entityAliasId, query, type)" + fetch-dashboard-states="vm.fetchDashboardStates(query)" on-create-entity-alias="vm.createEntityAlias(event, alias, allowedEntityTypes)" the-form="theForm">
diff --git a/ui/src/app/dashboard/edit-widget.directive.js b/ui/src/app/dashboard/edit-widget.directive.js index 5d32706f64..9bf1548d4a 100644 --- a/ui/src/app/dashboard/edit-widget.directive.js +++ b/ui/src/app/dashboard/edit-widget.directive.js @@ -94,6 +94,26 @@ export default function EditWidgetDirective($compile, $templateCache, types, wid return deferred.promise; }; + scope.fetchDashboardStates = function(query) { + var deferred = $q.defer(); + var stateIds = Object.keys(scope.dashboard.configuration.states); + var result = query ? stateIds.filter( + createFilterForDashboardState(query)) : stateIds; + if (result && result.length) { + deferred.resolve(result); + } else { + deferred.resolve([query]); + } + return deferred.promise; + } + + function createFilterForDashboardState (query) { + var lowercaseQuery = angular.lowercase(query); + return function filterFn(stateId) { + return (angular.lowercase(stateId).indexOf(lowercaseQuery) === 0); + }; + } + scope.createEntityAlias = function (event, alias, allowedEntityTypes) { var deferred = $q.defer(); diff --git a/ui/src/app/dashboard/edit-widget.tpl.html b/ui/src/app/dashboard/edit-widget.tpl.html index c6bfd5a8c4..1f724a8749 100644 --- a/ui/src/app/dashboard/edit-widget.tpl.html +++ b/ui/src/app/dashboard/edit-widget.tpl.html @@ -26,6 +26,7 @@ alias-controller="aliasController" functions-only="widgetEditMode" fetch-entity-keys="fetchEntityKeys(entityAliasId, query, type)" + fetch-dashboard-states="fetchDashboardStates(query)" on-create-entity-alias="createEntityAlias(event, alias, allowedEntityTypes)" the-form="theForm"> diff --git a/ui/src/app/dashboard/states/default-state-controller.js b/ui/src/app/dashboard/states/default-state-controller.js index 716d333229..d39478f14c 100644 --- a/ui/src/app/dashboard/states/default-state-controller.js +++ b/ui/src/app/dashboard/states/default-state-controller.js @@ -15,7 +15,7 @@ */ /*@ngInject*/ -export default function DefaultStateController($scope, $location, $state, $stateParams, $translate, types, dashboardUtils) { +export default function DefaultStateController($scope, $location, $state, $stateParams, utils, types, dashboardUtils) { var vm = this; @@ -50,6 +50,9 @@ export default function DefaultStateController($scope, $location, $state, $state } function updateState(id, params, openRightLayout) { + if (!id) { + id = getStateId(); + } if (vm.states && vm.states[id]) { if (!params) { params = {}; @@ -110,15 +113,7 @@ export default function DefaultStateController($scope, $location, $state, $state } function getStateName(id, state) { - var result = ''; - var translationId = types.translate.customTranslationsPrefix + state.name; - var translation = $translate.instant(translationId); - if (translation != translationId) { - result = translation + ''; - } else { - result = id; - } - return result; + return utils.customTranslation(state.name, id); } function parseState(stateJson) { diff --git a/ui/src/app/dashboard/states/entity-state-controller.js b/ui/src/app/dashboard/states/entity-state-controller.js index dd295cc89e..4510449e28 100644 --- a/ui/src/app/dashboard/states/entity-state-controller.js +++ b/ui/src/app/dashboard/states/entity-state-controller.js @@ -55,6 +55,9 @@ export default function EntityStateController($scope, $location, $state, $stateP } function updateState(id, params, openRightLayout) { + if (!id) { + id = getStateId(); + } if (vm.states && vm.states[id]) { resolveEntity(params).then( function success(entityName) { @@ -121,17 +124,10 @@ export default function EntityStateController($scope, $location, $state, $stateP var result = ''; if (vm.stateObject[index]) { var stateName = vm.states[vm.stateObject[index].id].name; - var translationId = types.translate.customTranslationsPrefix + stateName; - var translation = $translate.instant(translationId); - if (translation != translationId) { - stateName = translation + ''; - } + stateName = utils.customTranslation(stateName, stateName); var params = vm.stateObject[index].params; - if (params && params.entityName) { - result = utils.insertVariable(stateName, 'entityName', params.entityName); - } else { - result = stateName; - } + var entityName = params && params.entityName ? params.entityName : ''; + result = utils.insertVariable(stateName, 'entityName', entityName); } return result; } diff --git a/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.controller.js b/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.controller.js index bcf37dd825..a133829086 100644 --- a/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.controller.js +++ b/ui/src/app/entity/attribute/add-widget-to-dashboard-dialog.controller.js @@ -102,6 +102,7 @@ export default function AddWidgetToDashboardDialogController($scope, $mdDialog, if (vm.addToDashboardType === 0) { dashboardService.getDashboard(vm.dashboardId).then( function success(dashboard) { + dashboard = dashboardUtils.validateAndUpdateDashboard(dashboard); selectTargetState($event, dashboard).then( function(targetState) { selectTargetLayout($event, dashboard, targetState).then( diff --git a/ui/src/app/locale/locale.constant.js b/ui/src/app/locale/locale.constant.js index 63f20e5410..4d4ebe2600 100644 --- a/ui/src/app/locale/locale.constant.js +++ b/ui/src/app/locale/locale.constant.js @@ -1104,11 +1104,16 @@ export default angular.module('thingsboard.locale', []) "export": "Export widget" }, "widget-action": { - "header-button": "Header button", + "header-button": "Widget header button", "open-dashboard-state": "Navigate to new dashboard state", "update-dashboard-state": "Update current dashboard state", "open-dashboard": "Navigate to other dashboard", - "custom": "Custom action" + "custom": "Custom action", + "target-dashboard-state": "Target dashboard state", + "target-dashboard-state-required": "Target dashboard state is required", + "set-entity-from-widget": "Set entity from widget", + "target-dashboard": "Target dashboard", + "open-right-layout": "Open right dashboard layout (mobile view)" }, "widgets-bundle": { "current": "Current bundle", @@ -1174,6 +1179,7 @@ export default angular.module('thingsboard.locale', []) "action-source-required": "Action source is required.", "action-name": "Name", "action-name-required": "Action name is required.", + "action-name-not-unique": "Another action with the same name already exists.
Action name should be unique within the same action source.", "action-icon": "Icon", "action-type": "Type", "action-type-required": "Action type is required.", @@ -1205,6 +1211,10 @@ export default angular.module('thingsboard.locale', []) "es_ES": "Spanish" }, "custom": { + "widget-action": { + "action-cell-button": "Action cell button", + "row-click": "On row click" + } } } } diff --git a/ui/src/app/widget/lib/alarms-table-widget.js b/ui/src/app/widget/lib/alarms-table-widget.js index 5a65af20b0..7618e6a829 100644 --- a/ui/src/app/widget/lib/alarms-table-widget.js +++ b/ui/src/app/widget/lib/alarms-table-widget.js @@ -158,13 +158,7 @@ function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdDia vm.ctx.widgetActions = [ vm.searchAction ]; if (vm.settings.alarmsTitle && vm.settings.alarmsTitle.length) { - var translationId = types.translate.customTranslationsPrefix + vm.settings.alarmsTitle; - var translation = $translate.instant(translationId); - if (translation != translationId) { - vm.alarmsTitle = translation + ''; - } else { - vm.alarmsTitle = vm.settings.alarmsTitle; - } + vm.alarmsTitle = utils.customTranslation(vm.settings.alarmsTitle, vm.settings.alarmsTitle); } else { vm.alarmsTitle = $translate.instant('alarm.alarms'); } @@ -226,6 +220,9 @@ function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdDia 'table.md-table td.md-cell.md-checkbox-cell md-checkbox:not(.md-checked) .md-icon {\n'+ 'border-color: ' + mdDarkSecondary + ';\n'+ '}\n'+ + 'table.md-table td.md-cell.tb-action-cell button.md-icon-button md-icon {\n'+ + 'color: ' + mdDarkSecondary + ';\n'+ + '}\n'+ 'table.md-table td.md-cell.md-placeholder {\n'+ 'color: ' + mdDarkDisabled + ';\n'+ '}\n'+ @@ -539,13 +536,7 @@ function AlarmsTableWidgetController($element, $scope, $filter, $mdMedia, $mdDia for (var d = 0; d < vm.alarmSource.dataKeys.length; d++ ) { var dataKey = vm.alarmSource.dataKeys[d]; - var translationId = types.translate.customTranslationsPrefix + dataKey.label; - var translation = $translate.instant(translationId); - if (translation != translationId) { - dataKey.title = translation + ''; - } else { - dataKey.title = dataKey.label; - } + dataKey.title = utils.customTranslation(dataKey.label, dataKey.label); var keySettings = dataKey.settings; diff --git a/ui/src/app/widget/lib/entities-table-widget.js b/ui/src/app/widget/lib/entities-table-widget.js index 4b8aa6ec2b..733e7cb592 100644 --- a/ui/src/app/widget/lib/entities-table-widget.js +++ b/ui/src/app/widget/lib/entities-table-widget.js @@ -65,8 +65,9 @@ function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $tra vm.currentEntity = null; vm.displayEntityName = true; + vm.entityNameColumnTitle = ''; vm.displayEntityType = true; - vm.displayActions = false; //TODO: Widget actions + vm.actionCellDescriptors = []; vm.displayPagination = true; vm.defaultPageSize = 10; vm.defaultSortOrder = 'entityName'; @@ -92,6 +93,7 @@ function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $tra vm.onReorder = onReorder; vm.onPaginate = onPaginate; vm.onRowClick = onRowClick; + vm.onActionButtonClick = onActionButtonClick; vm.isCurrent = isCurrent; vm.cellStyle = cellStyle; @@ -141,14 +143,10 @@ function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $tra vm.ctx.widgetActions = [ vm.searchAction ]; + vm.actionCellDescriptors = vm.ctx.actionsApi.getActionDescriptors('actionCellButton'); + if (vm.settings.entitiesTitle && vm.settings.entitiesTitle.length) { - var translationId = types.translate.customTranslationsPrefix + vm.settings.entitiesTitle; - var translation = $translate.instant(translationId); - if (translation != translationId) { - vm.entitiesTitle = translation + ''; - } else { - vm.entitiesTitle = vm.settings.entitiesTitle; - } + vm.entitiesTitle = utils.customTranslation(vm.settings.entitiesTitle, vm.settings.entitiesTitle); } else { vm.entitiesTitle = $translate.instant('entity.entities'); } @@ -157,6 +155,13 @@ function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $tra vm.searchAction.show = angular.isDefined(vm.settings.enableSearch) ? vm.settings.enableSearch : true; vm.displayEntityName = angular.isDefined(vm.settings.displayEntityName) ? vm.settings.displayEntityName : true; + + if (vm.settings.entityNameColumnTitle && vm.settings.entityNameColumnTitle.length) { + vm.entityNameColumnTitle = utils.customTranslation(vm.settings.entityNameColumnTitle, vm.settings.entityNameColumnTitle); + } else { + vm.entityNameColumnTitle = $translate.instant('entity.entity-name'); + } + vm.displayEntityType = angular.isDefined(vm.settings.displayEntityType) ? vm.settings.displayEntityType : true; vm.displayPagination = angular.isDefined(vm.settings.displayPagination) ? vm.settings.displayPagination : true; @@ -185,6 +190,8 @@ function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $tra //var mdDarkIcon = mdDarkSecondary; var mdDarkDivider = defaultColor.setAlpha(0.12).toRgbString(); + //md-icon.md-default-theme, md-icon { + var cssString = 'table.md-table th.md-column {\n'+ 'color: ' + mdDarkSecondary + ';\n'+ '}\n'+ @@ -204,6 +211,9 @@ function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $tra 'table.md-table td.md-cell.md-checkbox-cell md-checkbox:not(.md-checked) .md-icon {\n'+ 'border-color: ' + mdDarkSecondary + ';\n'+ '}\n'+ + 'table.md-table td.md-cell.tb-action-cell button.md-icon-button md-icon {\n'+ + 'color: ' + mdDarkSecondary + ';\n'+ + '}\n'+ 'table.md-table td.md-cell.md-placeholder {\n'+ 'color: ' + mdDarkDisabled + ';\n'+ '}\n'+ @@ -261,11 +271,37 @@ function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $tra } function onRowClick($event, entity) { + if ($event) { + $event.stopPropagation(); + } if (vm.currentEntity != entity) { vm.currentEntity = entity; + var descriptors = vm.ctx.actionsApi.getActionDescriptors('rowClick'); + if (descriptors.length) { + var entityId; + var entityName; + if (vm.currentEntity) { + entityId = vm.currentEntity.id; + entityName = vm.currentEntity.entityName; + } + vm.ctx.actionsApi.handleWidgetAction($event, descriptors[0], entityId, entityName); + } } } + function onActionButtonClick($event, entity, actionDescriptor) { + if ($event) { + $event.stopPropagation(); + } + var entityId; + var entityName; + if (entity) { + entityId = entity.id; + entityName = entity.entityName; + } + vm.ctx.actionsApi.handleWidgetAction($event, actionDescriptor, entityId, entityName); + } + function isCurrent(entity) { return (vm.currentEntity && entity && vm.currentEntity.id && entity.id) && (vm.currentEntity.id.id === entity.id.id); @@ -393,13 +429,7 @@ function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $tra } vm.dataKeys.push(dataKey); - var translationId = types.translate.customTranslationsPrefix + dataKey.label; - var translation = $translate.instant(translationId); - if (translation != translationId) { - dataKey.title = translation + ''; - } else { - dataKey.title = dataKey.label; - } + dataKey.title = utils.customTranslation(dataKey.label, dataKey.label); var keySettings = dataKey.settings; diff --git a/ui/src/app/widget/lib/entities-table-widget.tpl.html b/ui/src/app/widget/lib/entities-table-widget.tpl.html index 247d4792e8..162e5f74de 100644 --- a/ui/src/app/widget/lib/entities-table-widget.tpl.html +++ b/ui/src/app/widget/lib/entities-table-widget.tpl.html @@ -41,10 +41,10 @@ - + - + @@ -57,14 +57,18 @@ ng-style="vm.cellStyle(entity, key)" ng-bind-html="vm.cellContent(entity, key)"> -
entity.entity-name{{vm.entityNameColumnTitle}} entity.entity-type {{ key.title }}  
- +