diff --git a/ui/src/app/common/types.constant.js b/ui/src/app/common/types.constant.js index 7267f0e223..125674ecfe 100644 --- a/ui/src/app/common/types.constant.js +++ b/ui/src/app/common/types.constant.js @@ -828,6 +828,10 @@ export default angular.module('thingsboard.types', []) custom: { name: 'widget-action.custom', value: 'custom' + }, + customPretty: { + name: 'widget-action.custom-pretty', + value: 'customPretty' } }, systemBundleAlias: { diff --git a/ui/src/app/components/widget/action/custom-action-pretty-editor.directive.js b/ui/src/app/components/widget/action/custom-action-pretty-editor.directive.js new file mode 100644 index 0000000000..c6a8c725dd --- /dev/null +++ b/ui/src/app/components/widget/action/custom-action-pretty-editor.directive.js @@ -0,0 +1,993 @@ +/* + * Copyright © 2016-2019 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 './custom-action-pretty-editor.scss'; +import customActionPrettyEditorTemplate from './custom-action-pretty-editor.tpl.html'; + +import 'brace/ext/language_tools'; +import 'brace/ext/searchbox'; +import 'brace/mode/html'; +import 'brace/mode/css'; +import 'brace/snippets/text'; +import 'brace/snippets/html'; +import 'brace/snippets/css'; + +import beautify from 'js-beautify'; +import Split from "split.js"; + +const html_beautify = beautify.html; +const css_beautify = beautify.css; + +export default angular.module('thingsboard.directives.customActionPrettyEditor', []) + .directive('tbCustomActionPrettyEditor', CustomActionPrettyEditor) + .name; + +/*@ngInject*/ +function CustomActionPrettyEditor($compile, $templateCache, $window, $timeout) { + + var linker = function (scope, element, attrs, ngModelCtrl) { + var template = $templateCache.get(customActionPrettyEditorTemplate); + element.html(template); + var ace_editors = []; + scope.fullscreen = false; + scope.htmlEditorOptions = { + useWrapMode: true, + mode: 'html', + advanced: { + enableSnippets: true, + enableBasicAutocompletion: true, + enableLiveAutocompletion: true + }, + onLoad: function (_ace) { + ace_editors.push(_ace); + } + }; + scope.cssEditorOptions = { + useWrapMode: true, + mode: 'css', + advanced: { + enableSnippets: true, + enableBasicAutocompletion: true, + enableLiveAutocompletion: true + }, + onLoad: function (_ace) { + ace_editors.push(_ace); + } + }; + + scope.addResource = addResource; + scope.beautifyCss = beautifyCss; + scope.beautifyHtml = beautifyHtml; + scope.removeResource = removeResource; + scope.toggleFullscreen = toggleFullscreen; + + var sampleJsFunction = "/* There are three examples: for delete, edit and add entity */\n" + + "/* Delete entity example */\n" + + "//\n" + + "//var $injector = widgetContext.$scope.$injector;\n" + + "//var $mdDialog = $injector.get('$mdDialog'),\n" + + "// $document = $injector.get('$document'),\n" + + "// types = $injector.get('types'),\n" + + "// assetService = $injector.get('assetService'),\n" + + "// deviceService = $injector.get('deviceService')\n" + + "// $rootScope = $injector.get('$rootScope'),\n" + + "// $q = $injector.get('$q');\n" + + "//\n" + + "//openDeleteEntityDialog();\n" + + "//\n" + + "//function openDeleteEntityDialog() {\n" + + "// var title = 'Delete ' + entityId.entityType\n" + + "// .toLowerCase() + ' ' +\n" + + "// entityName;\n" + + "// var content = 'Are you sure you want to delete the ' +\n" + + "// entityId.entityType.toLowerCase() + ' ' +\n" + + "// entityName + '?';\n" + + "// var confirm = $mdDialog.confirm()\n" + + "// .targetEvent($event)\n" + + "// .title(title)\n" + + "// .htmlContent(content)\n" + + "// .ariaLabel(title)\n" + + "// .cancel('Cancel')\n" + + "// .ok('Delete');\n" + + "// $mdDialog.show(confirm).then(function() {\n" + + "// deleteEntity();\n" + + "// })\n" + + "//}\n" + + "//\n" + + "//function deleteEntity() {\n" + + "// deleteEntityPromise(entityId).then(\n" + + "// function success() {\n" + + "// updateAliasData();\n" + + "// },\n" + + "// function fail() {\n" + + "// showErrorDialog();\n" + + "// }\n" + + "// );\n" + + "//}\n" + + "//\n" + + "//function deleteEntityPromise(entityId) {\n" + + "// if (entityId.entityType == types.entityType.asset) {\n" + + "// return assetService.deleteAsset(entityId.id);\n" + + "// } else if (entityId.entityType == types.entityType.device) {\n" + + "// return deviceService.deleteDevice(entityId.id);\n" + + "// }\n" + + "//}\n" + + "//\n" + + "//function updateAliasData() {\n" + + "// var aliasIds = [];\n" + + "// for (var id in widgetContext.aliasController.resolvedAliases) {\n" + + "// aliasIds.push(id);\n" + + "// }\n" + + "// var tasks = [];\n" + + "// aliasIds.forEach(function(aliasId) {\n" + + "// widgetContext.aliasController.setAliasUnresolved(aliasId);\n" + + "// tasks.push(widgetContext.aliasController.getAliasInfo(aliasId));\n" + + "// });\n" + + "// $q.all(tasks).then(function() {\n" + + "// $rootScope.$broadcast('entityAliasesChanged', aliasIds);\n" + + "// });\n" + + "//}\n" + + "//\n" + + "//function showErrorDialog() {\n" + + "// var title = 'Error';\n" + + "// var content = 'An error occurred while deleting the entity. Please try again.';\n" + + "// var alert = $mdDialog.alert()\n" + + "// .title(title)\n" + + "// .htmlContent(content)\n" + + "// .ariaLabel(title)\n" + + "// .parent(angular.element($document[0].body))\n" + + "// .targetEvent($event)\n" + + "// .multiple(true)\n" + + "// .clickOutsideToClose(true)\n" + + "// .ok('CLOSE');\n" + + "// $mdDialog.show(alert);\n" + + "//}\n" + + "//\n" + + "/* Edit entity example */\n" + + "//\n" + + "//var $injector = widgetContext.$scope.$injector;\n" + + "//var $mdDialog = $injector.get('$mdDialog'),\n" + + "// $document = $injector.get('$document'),\n" + + "// $q = $injector.get('$q'),\n" + + "// types = $injector.get('types'),\n" + + "// $rootScope = $injector.get('$rootScope'),\n" + + "// entityService = $injector.get('entityService'),\n" + + "// attributeService = $injector.get('attributeService'),\n" + + "// entityRelationService = $injector.get('entityRelationService');\n" + + "//\n" + + "//openEditEntityDialog();\n" + + "//\n" + + "//function openEditEntityDialog() {\n" + + "// $mdDialog.show({\n" + + "// controller: ['$scope','$mdDialog', EditEntityDialogController],\n" + + "// controllerAs: 'vm',\n" + + "// template: htmlTemplate,\n" + + "// locals: {\n" + + "// entityId: entityId\n" + + "// },\n" + + "// parent: angular.element($document[0].body),\n" + + "// targetEvent: $event,\n" + + "// multiple: true,\n" + + "// clickOutsideToClose: false\n" + + "// });\n" + + "//}\n" + + "//\n" + + "//function EditEntityDialogController($scope,$mdDialog) {\n" + + "// var vm = this;\n" + + "// vm.entityId = entityId;\n" + + "// vm.entityName = entityName;\n" + + "// vm.entityType = entityId.entityType;\n" + + "// vm.allowedEntityTypes = [types.entityType.asset, types.entityType.device];\n" + + "// vm.allowedRelatedEntityTypes = [];\n" + + "// vm.entitySearchDirection = types.entitySearchDirection;\n" + + "// vm.attributes = {};\n" + + "// vm.serverAttributes = {};\n" + + "// vm.relations = [];\n" + + "// vm.newRelations = [];\n" + + "// vm.relationsToDelete = [];\n" + + "// getEntityInfo();\n" + + "// \n" + + "// vm.addRelation = function() {\n" + + "// var relation = {\n" + + "// direction: null,\n" + + "// relationType: null,\n" + + "// relatedEntity: null\n" + + "// };\n" + + "// vm.newRelations.push(relation);\n" + + "// $scope.editEntityForm.$setDirty();\n" + + "// };\n" + + "// vm.removeRelation = function(index) {\n" + + "// if (index > -1) {\n" + + "// vm.newRelations.splice(index, 1);\n" + + "// $scope.editEntityForm.$setDirty();\n" + + "// }\n" + + "// };\n" + + "// vm.removeOldRelation = function(index, relation) {\n" + + "// if (index > -1) {\n" + + "// vm.relations.splice(index, 1);\n" + + "// vm.relationsToDelete.push(relation);\n" + + "// $scope.editEntityForm.$setDirty();\n" + + "// }\n" + + "// };\n" + + "// vm.save = function() {\n" + + "// saveAttributes();\n" + + "// saveRelations();\n" + + "// $scope.editEntityForm.$setPristine();\n" + + "// };\n" + + "// vm.cancel = function() {\n" + + "// $mdDialog.hide();\n" + + "// };\n" + + "// \n" + + "// function getEntityAttributes(attributes) {\n" + + "// for (var i = 0; i < attributes.length; i++) {\n" + + "// vm.attributes[attributes[i].key] = attributes[i].value; \n" + + "// }\n" + + "// vm.serverAttributes = angular.copy(vm.attributes);\n" + + "// }\n" + + "// \n" + + "// function getEntityRelations(relations) {\n" + + "// var relationsFrom = relations[0];\n" + + "// var relationsTo = relations[1];\n" + + "// for (var i=0; i < relationsFrom.length; i++) {\n" + + "// var relation = {\n" + + "// direction: types.entitySearchDirection.from,\n" + + "// relationType: relationsFrom[i].type,\n" + + "// relatedEntity: relationsFrom[i].to\n" + + "// };\n" + + "// vm.relations.push(relation);\n" + + "// }\n" + + "// for (var i=0; i < relationsTo.length; i++) {\n" + + "// var relation = {\n" + + "// direction: types.entitySearchDirection.to,\n" + + "// relationType: relationsTo[i].type,\n" + + "// relatedEntity: relationsTo[i].from\n" + + "// };\n" + + "// vm.relations.push(relation);\n" + + "// }\n" + + "// }\n" + + "// \n" + + "// function getEntityInfo() {\n" + + "// entityService.getEntity(entityId.entityType, entityId.id).then(\n" + + "// function(entity) {\n" + + "// vm.entity = entity;\n" + + "// vm.type = vm.entity.type;\n" + + "// });\n" + + "// attributeService.getEntityAttributesValues(entityId.entityType, entityId.id, 'SERVER_SCOPE').then(\n" + + "// function(data){\n" + + "// if (data.length) {\n" + + "// getEntityAttributes(data);\n" + + "// }\n" + + "// });\n" + + "// $q.all([entityRelationService.findInfoByFrom(entityId.id, entityId.entityType), entityRelationService.findInfoByTo(entityId.id, entityId.entityType)]).then(\n" + + "// function(relations){\n" + + "// getEntityRelations(relations);\n" + + "// });\n" + + "// }\n" + + "// \n" + + "// function saveAttributes() {\n" + + "// var attributesArray = [];\n" + + "// for (var key in vm.attributes) {\n" + + "// if (vm.attributes[key] !== vm.serverAttributes[key]) {\n" + + "// attributesArray.push({key: key, value: vm.attributes[key]});\n" + + "// }\n" + + "// }\n" + + "// if (attributesArray.length > 0) {\n" + + "// attributeService.saveEntityAttributes(entityId.entityType, entityId.id, \"SERVER_SCOPE\", attributesArray);\n" + + "// } \n" + + "// }\n" + + "// \n" + + "// function saveRelations() {\n" + + "// var tasks = [];\n" + + "// for (var i=0; i < vm.newRelations.length; i++) {\n" + + "// var relation = {\n" + + "// type: vm.newRelations[i].relationType\n" + + "// };\n" + + "// if (vm.newRelations[i].direction == types.entitySearchDirection.from) {\n" + + "// relation.to = vm.newRelations[i].relatedEntity;\n" + + "// relation.from = entityId;\n" + + "// } else {\n" + + "// relation.to = entityId;\n" + + "// relation.from = vm.newRelations[i].relatedEntity;\n" + + "// }\n" + + "// tasks.push(entityRelationService.saveRelation(relation));\n" + + "// }\n" + + "// for (var i=0; i < vm.relationsToDelete.length; i++) {\n" + + "// var relation = {\n" + + "// type: vm.relationsToDelete[i].relationType\n" + + "// };\n" + + "// if (vm.relationsToDelete[i].direction == types.entitySearchDirection.from) {\n" + + "// relation.to = vm.relationsToDelete[i].relatedEntity;\n" + + "// relation.from = entityId;\n" + + "// } else {\n" + + "// relation.to = entityId;\n" + + "// relation.from = vm.relationsToDelete[i].relatedEntity;\n" + + "// }\n" + + "// tasks.push(entityRelationService.deleteRelation(relation.from.id, relation.from.entityType, relation.type, relation.to.id, relation.to.entityType));\n" + + "// }\n" + + "// $q.all(tasks).then(function(){\n" + + "// vm.relations = vm.relations.concat(vm.newRelations);\n" + + "// vm.newRelations = [];\n" + + "// vm.relationsToDelete = [];\n" + + "// updateAliasData();\n" + + "// });\n" + + "// }\n" + + "// \n" + + "// function updateAliasData() {\n" + + "// var aliasIds = [];\n" + + "// for (var id in widgetContext.aliasController.resolvedAliases) {\n" + + "// aliasIds.push(id);\n" + + "// }\n" + + "// var tasks = [];\n" + + "// aliasIds.forEach(function(aliasId) {\n" + + "// widgetContext.aliasController.setAliasUnresolved(aliasId);\n" + + "// tasks.push(widgetContext.aliasController.getAliasInfo(aliasId));\n" + + "// });\n" + + "// $q.all(tasks).then(function() {\n" + + "// $rootScope.$broadcast('entityAliasesChanged', aliasIds);\n" + + "// });\n" + + "// }\n" + + "//}\n" + + "//\n" + + "/* Add entity example */\n" + + "//\n" + + "//var $injector = widgetContext.$scope.$injector;\n" + + "//var $mdDialog = $injector.get('$mdDialog'),\n" + + "// $document = $injector.get('$document'),\n" + + "// $q = $injector.get('$q'),\n" + + "// $rootScope = $injector.get('$rootScope'),\n" + + "// types = $injector.get('types'),\n" + + "// assetService = $injector.get('assetService'),\n" + + "// deviceService = $injector.get('deviceService'),\n" + + "// attributeService = $injector.get('attributeService'),\n" + + "// entityRelationService = $injector.get('entityRelationService');\n" + + "//\n" + + "//openAddEntityDialog();\n" + + "//\n" + + "//function openAddEntityDialog() {\n" + + "// $mdDialog.show({\n" + + "// controller: ['$scope','$mdDialog', AddEntityDialogController],\n" + + "// controllerAs: 'vm',\n" + + "// template: htmlTemplate,\n" + + "// locals: {\n" + + "// entityId: entityId\n" + + "// },\n" + + "// parent: angular.element($document[0].body),\n" + + "// targetEvent: $event,\n" + + "// multiple: true,\n" + + "// clickOutsideToClose: false\n" + + "// });\n" + + "//}\n" + + "//\n" + + "//function AddEntityDialogController($scope, $mdDialog) {\n" + + "// var vm = this;\n" + + "// vm.allowedEntityTypes = [types.entityType.asset, types.entityType.device];\n" + + "// vm.allowedRelatedEntityTypes = [];\n" + + "// vm.entitySearchDirection = types.entitySearchDirection;\n" + + "// vm.attributes = {};\n" + + "// vm.relations = [];\n" + + "// \n" + + "// vm.addRelation = function() {\n" + + "// var relation = {\n" + + "// direction: null,\n" + + "// relationType: null,\n" + + "// relatedEntity: null\n" + + "// };\n" + + "// vm.relations.push(relation);\n" + + "// };\n" + + "// vm.removeRelation = function(index) {\n" + + "// if (index > -1) {\n" + + "// vm.relations.splice(index, 1);\n" + + "// }\n" + + "// };\n" + + "// vm.save = function() {\n" + + "// $scope.addEntityForm.$setPristine();\n" + + "// saveEntityPromise().then(\n" + + "// function (entity) {\n" + + "// saveAttributes(entity.id);\n" + + "// saveRelations(entity.id);\n" + + "// $mdDialog.hide();\n" + + "// }\n" + + "// );\n" + + "// };\n" + + "// vm.cancel = function() {\n" + + "// $mdDialog.hide();\n" + + "// };\n" + + "// \n" + + "// \n" + + "// function saveEntityPromise() {\n" + + "// var entity = {\n" + + "// name: vm.entityName,\n" + + "// type: vm.type\n" + + "// };\n" + + "// if (vm.entityType == types.entityType.asset) {\n" + + "// return assetService.saveAsset(entity);\n" + + "// } else if (vm.entityType == types.entityType.device) {\n" + + "// return deviceService.saveDevice(entity);\n" + + "// }\n" + + "// }\n" + + "// \n" + + "// function saveAttributes(entityId) {\n" + + "// var attributesArray = [];\n" + + "// for (var key in vm.attributes) {\n" + + "// attributesArray.push({key: key, value: vm.attributes[key]});\n" + + "// }\n" + + "// if (attributesArray.length > 0) {\n" + + "// attributeService.saveEntityAttributes(entityId.entityType, entityId.id, \"SERVER_SCOPE\", attributesArray);\n" + + "// } \n" + + "// }\n" + + "// \n" + + "// function saveRelations(entityId) {\n" + + "// var tasks = [];\n" + + "// for (var i=0; i < vm.relations.length; i++) {\n" + + "// var relation = {\n" + + "// type: vm.relations[i].relationType\n" + + "// };\n" + + "// if (vm.relations[i].direction == types.entitySearchDirection.from) {\n" + + "// relation.to = vm.relations[i].relatedEntity;\n" + + "// relation.from = entityId;\n" + + "// } else {\n" + + "// relation.to = entityId;\n" + + "// relation.from = vm.relations[i].relatedEntity;\n" + + "// }\n" + + "// tasks.push(entityRelationService.saveRelation(relation));\n" + + "// }\n" + + "// $q.all(tasks).then(function(){\n" + + "// updateAliasData();\n" + + "// });\n" + + "// }\n" + + "// \n" + + "// function updateAliasData() {\n" + + "// var aliasIds = [];\n" + + "// for (var id in widgetContext.aliasController.resolvedAliases) {\n" + + "// aliasIds.push(id);\n" + + "// }\n" + + "// var tasks = [];\n" + + "// aliasIds.forEach(function(aliasId) {\n" + + "// widgetContext.aliasController.setAliasUnresolved(aliasId);\n" + + "// tasks.push(widgetContext.aliasController.getAliasInfo(aliasId));\n" + + "// });\n" + + "// $q.all(tasks).then(function() {\n" + + "// $rootScope.$broadcast('entityAliasesChanged', aliasIds);\n" + + "// });\n" + + "// }\n" + + "//}\n" + + "\n" + + "\n"; + + var sampleHtmlTemplate = '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n'; + + var sampleCss = '/* There are two examples: for edit and add entity */\n' + + '/* Edit entity example */\n' + + '/*\n' + + '.edit-entity-form md-input-container {\n' + + ' padding-right: 10px;\n' + + '}\n' + + '\n' + + '.edit-entity-form .boolean-value-input {\n' + + ' padding-left: 5px;\n' + + '}\n' + + '\n' + + '.edit-entity-form .boolean-value-input .checkbox-label {\n' + + ' margin-bottom: 8px;\n' + + ' color: rgba(0,0,0,0.54);\n' + + ' font-size: 12px;\n' + + '}\n' + + '\n' + + '.relations-list .header {\n' + + ' padding-right: 5px;\n' + + ' padding-bottom: 5px;\n' + + ' padding-left: 5px;\n' + + '}\n' + + '\n' + + '.relations-list .header .cell {\n' + + ' padding-right: 5px;\n' + + ' padding-left: 5px;\n' + + ' font-size: 12px;\n' + + ' font-weight: 700;\n' + + ' color: rgba(0, 0, 0, .54);\n' + + ' white-space: nowrap;\n' + + '}\n' + + '\n' + + '.relations-list .body {\n' + + ' padding-right: 5px;\n' + + ' padding-bottom: 15px;\n' + + ' padding-left: 5px;\n' + + '}\n' + + '\n' + + '.relations-list .body .row {\n' + + ' padding-top: 5px;\n' + + '}\n' + + '\n' + + '.relations-list .body .cell {\n' + + ' padding-right: 5px;\n' + + ' padding-left: 5px;\n' + + '}\n' + + '\n' + + '.relations-list .body md-autocomplete-wrap md-input-container {\n' + + ' height: 30px;\n' + + '}\n' + + '\n' + + '.relations-list .body .md-button {\n' + + ' margin: 0;\n' + + '}\n' + + '\n' + + '.relations-list.old-relations tb-entity-select tb-entity-autocomplete button {\n' + + ' display: none;\n' + + '} \n' + + '*/\n' + + '/* Add entity example */\n' + + '/*\n' + + '.add-entity-form md-input-container {\n' + + ' padding-right: 10px;\n' + + '}\n' + + '\n' + + '.add-entity-form .boolean-value-input {\n' + + ' padding-left: 5px;\n' + + '}\n' + + '\n' + + '.add-entity-form .boolean-value-input .checkbox-label {\n' + + ' margin-bottom: 8px;\n' + + ' color: rgba(0,0,0,0.54);\n' + + ' font-size: 12px;\n' + + '}\n' + + '\n' + + '.relations-list .header {\n' + + ' padding-right: 5px;\n' + + ' padding-bottom: 5px;\n' + + ' padding-left: 5px;\n' + + '}\n' + + '\n' + + '.relations-list .header .cell {\n' + + ' padding-right: 5px;\n' + + ' padding-left: 5px;\n' + + ' font-size: 12px;\n' + + ' font-weight: 700;\n' + + ' color: rgba(0, 0, 0, .54);\n' + + ' white-space: nowrap;\n' + + '}\n' + + '\n' + + '.relations-list .body {\n' + + ' padding-right: 5px;\n' + + ' padding-bottom: 15px;\n' + + ' padding-left: 5px;\n' + + '}\n' + + '\n' + + '.relations-list .body .row {\n' + + ' padding-top: 5px;\n' + + '}\n' + + '\n' + + '.relations-list .body .cell {\n' + + ' padding-right: 5px;\n' + + ' padding-left: 5px;\n' + + '}\n' + + '\n' + + '.relations-list .body md-autocomplete-wrap md-input-container {\n' + + ' height: 30px;\n' + + '}\n' + + '\n' + + '.relations-list .body .md-button {\n' + + ' margin: 0;\n' + + '}\n' + + '*/\n' + + '\n' + + '\n'; + + scope.$watch('action', function () { + ngModelCtrl.$setViewValue(scope.action); + }); + + ngModelCtrl.$render = function () { + scope.action = ngModelCtrl.$viewValue; + if (angular.isUndefined(scope.action.customHtml) && angular.isUndefined(scope.action.customCss) && angular.isUndefined(scope.action.customFunction)) { + scope.action.customFunction = sampleJsFunction; + scope.action.customHtml = sampleHtmlTemplate; + scope.action.customCss = sampleCss; + } + }; + + function removeResource(index) { + if (index > -1) { + scope.action.customResources.splice(index, 1); + scope.theForm.$setDirty(); + } + } + + function addResource() { + if (!scope.action.customResources) { + scope.action.customResources = []; + } + scope.action.customResources.push({url: ''}); + scope.theForm.$setDirty(); + } + + function beautifyHtml() { + var res = html_beautify(scope.action.customHtml, {indent_size: 4, wrap_line_length: 60}); + scope.action.customHtml = res; + } + + function beautifyCss() { + var res = css_beautify(scope.action.customCss, {indent_size: 4}); + scope.action.customCss = res; + } + + function toggleFullscreen() { + scope.fullscreen = !scope.fullscreen; + if (scope.fullscreen) { + scope.customActionEditorElement = angular.element('.tb-custom-action-editor'); + angular.element(scope.customActionEditorElement[0]).ready(function () { + var w = scope.customActionEditorElement.width(); + if (w > 0) { + initSplitLayout(); + } else { + scope.$watch( + function () { + return scope.customActionEditorElement[0].offsetWidth || parseInt(scope.customActionEditorElement.css('width'), 10); + }, + function (newSize) { + if (newSize > 0) { + initSplitLayout(); + } + } + ); + } + }); + } else { + scope.layoutInited = false; + } + } + + function onDividerDrag() { + scope.$broadcast('update-ace-editor-size'); + for (var i = 0; i < ace_editors.length; i++) { + var ace = ace_editors[i]; + ace.resize(); + ace.renderer.updateFull(); + } + } + + function initSplitLayout() { + if (!scope.layoutInited) { + Split([angular.element('#left-panel', scope.customActionEditorElement)[0], angular.element('#right-panel', scope.customActionEditorElement)[0]], { + sizes: [50, 50], + gutterSize: 8, + cursor: 'col-resize', + onDrag: function () { + onDividerDrag() + } + }); + + onDividerDrag(); + + scope.$applyAsync(function () { + scope.layoutInited = true; + var w = angular.element($window); + $timeout(function () { + w.triggerHandler('resize') + }); + }); + + } + } + + $compile(element.contents())(scope); + }; + + return { + restrict: "E", + require: "^ngModel", + scope: { + theForm: '=?', + }, + link: linker + }; +} diff --git a/ui/src/app/components/widget/action/custom-action-pretty-editor.scss b/ui/src/app/components/widget/action/custom-action-pretty-editor.scss new file mode 100644 index 0000000000..6c977a1b1a --- /dev/null +++ b/ui/src/app/components/widget/action/custom-action-pretty-editor.scss @@ -0,0 +1,96 @@ +/** + * Copyright © 2016-2019 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-custom-action-pretty { + box-sizing: border-box; + padding: 8px; + background-color: #fff; + + .tb-fullscreen-panel { + .tb-custom-action-editor-container { + height: calc(100% - 40px); + } + + #right-panel { + padding-top: 8px; + padding-left: 3px; + } + + tb-js-func .tb-js-func-panel { + box-sizing: border-box; + } + + md-tabs-content-wrapper, + md-tab-content { + height: 100%; + } + } + + .html-panel, + .css-panel { + width: 100%; + min-width: 200px; + height: 100%; + min-height: 200px; + } + + .tb-split { + box-sizing: border-box; + overflow-x: hidden; + overflow-y: auto; + } + + .tb-content { + border: 1px solid #c0c0c0; + } + + .gutter { + background-color: #eee; + background-repeat: no-repeat; + background-position: 50%; + } + + .gutter.gutter-horizontal { + cursor: col-resize; + background-image: url("../../../../../node_modules/split.js/grips/vertical.png"); + } + + .tb-split.tb-split-horizontal, + .gutter.gutter-horizontal { + float: left; + height: 100%; + } + + .tb-action-expand-button { + position: absolute; + right: 14px; + z-index: 1; + + &.tb-fullscreen-editor { + position: relative; + right: 0; + } + + .md-button { + min-width: auto; + } + } + + .tb-custom-action-editor { + &.tb-fullscreen-editor { + height: 100%; + } + } +} diff --git a/ui/src/app/components/widget/action/custom-action-pretty-editor.tpl.html b/ui/src/app/components/widget/action/custom-action-pretty-editor.tpl.html new file mode 100644 index 0000000000..80cdf2a786 --- /dev/null +++ b/ui/src/app/components/widget/action/custom-action-pretty-editor.tpl.html @@ -0,0 +1,186 @@ + +