UI для Ensemble Workflow на Angular

от автора

Те, кто знаком с платформой для интеграции и разработки приложений InterSystems Ensemble, знают, что такое подсистема Ensemble Workflow и как она бывает полезна для автоматизации взаимодействия людей. Для тех же, кто не знаком с Ensemble (и/или Workflow), я кратко опишу её возможности (остальные могут пропустить эту часть и узнать, как они могут использовать пользовательский интерфейс Workflow на Angular.js).

InterSystems Ensemble
Платформа для интеграции и разработки приложений InterSystems Ensemble предназначена для интеграции разрозненных систем, автоматизации бизнес-процессов и создания новых композитных приложений, дополняющих функционал интегрированных приложений новой бизнес-логикой или пользовательским интерфейсом. Ensemble обеспечивает решение задач: EAI, SOA, BPM, BAM и даже BI (за счет встроенной технологии для разработки аналитических приложений InterSystems DeepSee).

В Ensemble существуют следующие основные компоненты:
Адаптеры – компоненты для взаимодействия с приложениями, технологиями и источниками данных. Вместе с Ensemble поставляются технологические и прикладные интеграционные адаптеры (Web- и Rest- сервисы, File, FTP, Email, SQL, EDI, HL7, SAP, Siebel, 1C Предприятие и т.д.). Можно создавать собственные адаптеры с помощью Adapter SDK. Бизнес-службы – компоненты, преобразующие данные, поступающие от внешних систем, в сообщения Ensemble, и вызывающие на исполнение бизнес-процессы и/или бизнес-операции. Бизнес-процессы – исполняемые процессы, использующиеся для оркестровки служб и операций для автоматизации сценариев взаимодействия систем и/или людей (через подсистему Workflow). Процессы либо описываются на декларативном языке Business Process Language, либо реализуются на Caché Object Script. Логика взаимодействия процессов с внешним миром отделена от конкретной реализации взаимодействия с помощью служб и операций. Бизнес-операции – компоненты, обеспечивающие вызов/передачу сообщений внешним системам и преобразование сообщений Ensemble в формат, пригодный для передачи во внешние системы. Трансформации сообщений – компоненты Ensemble для трансформации сообщений из одного формата в другой. Для реализации используется декларативный язык Data Transformation Language. Бизнес-правила – позволяют администраторам интеграционного решения без программирования менять поведение бизнес-процессов Ensemble в указанных в процессах точках принятия решений. Управление потоками работ – подсистема Ensemble Workflow обеспечивает автоматизацию распределения задач между пользователями. Бизнес-метрики – позволяют собирать и вычислять ключевые показатели эффективности и вместе с инструментальными панелями (Dashboards) используются для создания решений по мониторингу бизнес-активности (Business Activity Monitoring, BAM).

Вернемся к управлению потоками работ и рассмотрим функционал подсистемы Ensemble Workflow более подробно.

Управление потоками работ и подсистема Ensemble Workflow
Согласно определению Workflow Management Coalition (www.WfMC.org), “потоки работ (Workflow) — это автоматизация бизнес процесса, полностью или частично, в рамках которого документы, информация или задачи передаются от одного участника к другому, в соответствии с набором процедурных правил.”

Ключевые элементы Workflow:
Задача Workflow — «фрагмент» работы Поток работ — процедурные правила выполнения задач Пользователь Workflow — человек, выполняющий задачи в системе управления потоками работ Роль Workflow — группа пользователей, которые выполняют определенные типы задач.

Подсистема управления потоками работ в Ensemble позволяет:
Автоматизировать управление потоками работ, используя бизнес-процессы Ensemble Гибко настраивать распределение работ Работать с подсистемой управления потоками работ через специализированный Workflow-портал, который поставляется вместе с Ensemble Организовать взаимодействие подсистемы управления потоками работ с интеграционными бизнес-процессами Ensemble Использовать подсистему мониторинга бизнес-активности, утилиты управления и мониторинга Ensemble Легко настраивать и расширять функционал подсистемы Workflow

Простейшим примером автоматизации управления потоками работ является приложение Ensemble HelpDesk для автоматизации взаимодействия сотрудников службы поддержки, которое входит в стандартную поставку примеров Ensemble и находится в области Ensdemo. Ensemble принимает сообщение о проблеме и запускает бизнес-процесс HelpDesk.

Фрагмент алгоритма бизнес-процесса HelpDesk

Бизнес-процесс отправляет пользователям роли Demo-Development задачу с помощью сообщения класса EnsLib.Workflow.TaskRequest, в котором определены возможные действия (“Исправлено” или “Проигнорировано”), а так же поле “Комментарий”. В тело сообщения также включена информация об ошибке и пользователе, сообщившем о проблеме. После этого в Workflow-портале любого пользователя роли Demo-Development появляется соответствующая задача.

Первоначально (если это не задано в сообщении TaskRequest) задача не ассоциирована ни с одним пользователем (а только с ролью), поэтому пользователю нужно ее принять, нажав соответствующую кнопку. Так же в любой момент можно отказаться от задачи, нажав кнопку “Уступить”.

После этого можно совершать доступные для конкретной задачи действия. В нашем случае мы можем нажать кнопку “Исправлено”, предварительно указав комментарий в соответствующем поле. Бизнес-процесс HelpDesk обработает это событие и отправит новое сообщение пользователям роли Demo-Testing, сигнализируя о необходимости тестирования произведенных исправлений. Если нажать кнопку “Проигнорировано”, то задача будет просто помечена как “Not a problem” и процесс обработки завершится.

Как видно из примера, Ensemble Workflow является простой и интуитивно понятной системой для организации потоков работ пользователей. Более подробную информацию о подсистеме Ensemble Workflow можно в документации Ensemble в разделе Defining Workflow.

Функциональность подсистемы Ensemble Workflow может быть легко расширена и встроена во внешнее композитное приложение на InterSystems Ensemble. В качестве примера рассмотрим реализацию функциональности пользовательского интерфейса Ensemble Workflow во внешнем композитном приложении, разработанном на Angular.js + REST API.

Интерфейс Ensemble Workflow на Angular.js.
Для работы пользовательского интерфейса Workflow на Angular.js необходимо установить на сервер Ensemble приложения:
UI на Angular.js REST API
Процесс установки описан в Readme указанных репозиториев.

На данный момент в приложении реализована вся базовая функциональность Ensemble Workflow: отображение списка задач, дополнительных полей и действий, сортировка, полнотекстовый поиск по задачам. Пользователь может принимать/отклонять задачи, подробная информация о задаче выводится в модальном окне.

Так же в ближайшее время в планах добавить в приложение возможность смены области (на данный момент приложение работает только в той области, в которой оно установлено).

На момент написания статьи приложение выглядит следующим образом:

Для последующей модификации интерфейса при необходимости, был использован Twitter Bootstrap

Некоторые технические детали реализации
В UI используются следующие библиотеки и фреймворки: js-фреймворк Angular.js, css-фреймворк Twitter Bootstrap, js-библиотека jQuery, а так же иконочные шрифты FontAwesome.

Приложение имеет 4 Angular-сервиса (RESTSrvc, SessionSrvc, UtilSrvc и WorklistSrvc), 3 контроллера (MainCtrl, TaskCtrl, TasksGridCtrl), главную страницу (index.csp) и 2 шаблона (task.csp и tasks.csp).

Сервис RESTSrvc имеет всего один метод getPromise и является оберткой вокруг сервиса $http Angular.js. Единственное предназначение RESTSrvc — отправлять HTTP-запросы на сервер и возвращать объекты promise этих запросов. Остальные сервисы используют RESTSrvc для осуществления запросов и их разделение носит, по существу, функциональный характер.
RESTSrvc.js’use strict’; function RESTSrvc($http, $q) { return { getPromise: function(config) { var deferred = $q.defer(); $http(config). success(function(data, status, headers, config) { deferred.resolve(data); }). error(function(data, status, headers, config) { deferred.reject(data, status, headers, config); }); return deferred.promise; } } }; // resolving minification problems RESTSrvc.$inject = [‘$http’, ‘$q’]; servicesModule.factory(‘RESTSrvc’, RESTSrvc);

SessionSrvc — содержит всего один метод, отвечающий за закрытие сессии. Аутентификация в приложении выполнена с помощью Basic access authetication (http://en.wikipedia.org/wiki/Basic_access_authentication), поэтому нет необходимости в аутентифицирующем методе, так как каждый запрос имеет в header’е токен авторизации.
SessionSrvc.js’use strict’; // Session service function SessionSrvc(RESTSrvc) { return { // save worklist object logout: function(baseAuthToken) { return RESTSrvc.getPromise( {method: ‘GET’, url: RESTWebApp.appName + ‘/logout’, headers: {‘Authorization’ : baseAuthToken} }); } } }; // resolving minification problems SessionSrvc.$inject = [‘RESTSrvc’]; servicesModule.factory(‘SessionSrvc’, SessionSrvc);

UtilSrvc — содержит вспомогательные методы, такие как получение значения cookie по имени, получение значения свойства объекта по имени.
UtilSrvc.js’use strict’; // Utils service function UtilSrvc($cookies) { return { // get cookie by name readCookie: function(name) { return $cookies[name]; }, // Function to get value of property of the object by name // Example: // var obj = {car: {body: {company: {name: ‘Mazda’}}}}; // getPropertyValue(obj, ‘car.body.company.name’) getPropertyValue: function(item, propertyStr) { var value = item; try { var properties = propertyStr.split(‘.’); for (var i = 0; i < properties.length; i++) { value = value[properties[i]]; if (value !== Object(value)) break; } } catch(ex) { console.log(‘Something goes wrong :/’); } return value == undefined ? » : value; } } }; // resolving minification problems UtilSrvc.$inject = [‘$cookies’]; servicesModule.factory(‘UtilSrvc’, UtilSrvc);

WorklistSrvc отвечает за выполнение запросов, связанных с данными списка задач.
WorklistSrvc.js’use strict’; // Worklist service function WorklistSrvc(RESTSrvc) { return { // save worklist object save: function(worklist, baseAuthToken) { return RESTSrvc.getPromise( {method: ‘POST’, url: RESTWebApp.appName + ‘/tasks/’ + worklist._id, data: worklist, headers: {‘Authorization’ : baseAuthToken} }); }, // get worklist by id get: function(id, baseAuthToken) { return RESTSrvc.getPromise( {method: ‘GET’, url: RESTWebApp.appName + ‘/tasks/’ + id,headers: {‘Authorization’ : baseAuthToken} }); }, // get all worklists for current user getAll: function(baseAuthToken) { return RESTSrvc.getPromise( {method: ‘GET’, url: RESTWebApp.appName + ‘/tasks’, headers: {‘Authorization’ : baseAuthToken} }); } } }; // resolving minification problems WorklistSrvc.$inject = [‘RESTSrvc’]; servicesModule.factory(‘WorklistSrvc’, WorklistSrvc);

MainCtrl — главный контроллер приложения, отвечает за аутентификацию пользователя.
MainCtrl.js’use strict’; // Main controller // Controls the authentication. Loads all the worklists for user. function MainCtrl($scope, $location, $cookies, WorklistSrvc, SessionSrvc, UtilSrvc) { $scope.page = {}; $scope.page.alerts = []; $scope.utils = UtilSrvc; $scope.page.loading = false; $scope.page.loginState = $cookies[‘Token’] ? 1 : 0; $scope.page.authToken = $cookies[‘Token’]; $scope.page.closeAlert = function(index) { if ($scope.page.alerts.length) { $(‘.alert:nth-child(‘+(index+1)+’)’).animate({opacity: 0, top: "-=150" }, 400, function() { $scope.page.alerts.splice(index, 1); $scope.$apply(); }); } }; $scope.page.addAlert = function(alert) { $scope.page.alerts.push(alert); if ($scope.page.alerts.length > 5) { $scope.page.closeAlert(0); } }; /* Authentication section */ $scope.page.makeBaseAuth = function(user, password) { var token = user + ‘:’ + password; var hash = Base64.encode(token); return "Basic " + hash; } // login $scope.page.doLogin = function(login, password) { var authToken = $scope.page.makeBaseAuth(login, password); $scope.page.loading = true; WorklistSrvc.getAll(authToken).then( function(data) { $scope.page.alerts = []; $scope.page.loginState = 1; $scope.page.authToken = authToken; // set cookie to restore loginState after page reload $cookies[‘User’] = login.toLowerCase(); $cookies[‘Token’] = $scope.page.authToken; // refresh the data on page $scope.page.loadSuccess(data); }, function(data, status, headers, config) { if (data.Error) { $scope.page.addAlert( {type: ‘danger’, msg: data.Error} ); } else { $scope.page.addAlert( {type: ‘danger’, msg: "Login unsuccessful"} ); } }) .then(function () { $scope.page.loading = false; }) }; // logout $scope.page.doExit = function() { SessionSrvc.logout($scope.page.authToken).then( function(data) { $scope.page.loginState = 0; $scope.page.grid.items = null; $scope.page.loading = false; // clear cookies delete $cookies[‘User’]; delete $cookies[‘Token’]; document.cookie = "CacheBrowserId" + "=; Path=/; expires=Thu, 01 Jan 1970 00:00:01 GMT;"; document.cookie = "CSPSESSIONID" + "=; Path=" + RESTWebApp.appName + "; expires=Thu, 01 Jan 1970 00:00:01 GMT;"; document.cookie = "CSPWSERVERID" + "=; Path=" + RESTWebApp.appName + "; expires=Thu, 01 Jan 1970 00:00:01 GMT;"; }, function(data, status, headers, config) { $scope.page.addAlert( {type: ‘danger’, msg: data.Error} ); }); }; } // resolving minification problems MainCtrl.$inject = [‘$scope’, ‘$location’, ‘$cookies’, ‘WorklistSrvc’, ‘SessionSrvc’, ‘UtilSrvc’]; controllersModule.controller(‘MainCtrl’, MainCtrl);

TasksGridCtrl — контроллер, отвечающий за таблицу списка задач и действия с ней. Он инициализирует таблицу списка задач, содержит методы для загрузки списка задач и конкретной задачи, а так же методы обработки действий пользователя (нажатие кнопок, сортировка таблицы, выделение строки таблицы, фильтрация).
TasksGridCtrl.js’use strict’; // TasksGrid controller // dependency injection function TasksGridCtrl($scope, $window, $modal, $cookies, WorklistSrvc) { // Initialize grid. // grid data: // grid title, css grid class, column names $scope.page.grid = { caption: ‘Inbox Tasks’, cssClass:’table table-condensed table-bordered table-hover’, columns: [{name: », property: ‘New’, align: ‘center’}, {name: ‘Priority’, property: ‘Priority’}, {name: ‘Subject’, property: ‘Subject’}, {name: ‘Message’, property: ‘Message’}, {name: ‘Role’, property: ‘RoleName’}, {name: ‘Assigned To’, property: ‘AssignedTo’}, {name: ‘Time Created’, property: ‘TimeCreated’}, {name: ‘Age’, property: ‘Age’}] }; // data initialization for Worklist $scope.page.dataInit = function() { if ($scope.page.loginState) { $scope.page.loadTasks(); } }; $scope.page.loadSuccess = function(data) { $scope.page.grid.items = data.children; // if we get data for other user — logout if (!$scope.page.checkUserValidity()) { $scope.page.doExit(); } var date = new Date(); var hours = (date.getHours() > 9) ? date.getHours() : ‘0’ + date.getHours(); var minutes = (date.getMinutes() > 9) ? date.getMinutes() : ‘0’ + date.getMinutes(); var secs = (date.getSeconds() > 9) ? date.getSeconds() : ‘0’ + date.getSeconds(); $(‘#updateTime’).animate({ opacity : 0 }, 100, function() { $(‘#updateTime’).animate({ opacity : 1 }, 1000);} ); $scope.page.grid.updateTime = ‘ [Last Update: ‘ + hours; $scope.page.grid.updateTime += ‘:’ + minutes + ‘:’ + secs + ‘]’; }; // all user’s tasks loading $scope.page.loadTasks = function() { $scope.page.loading = true; WorklistSrvc.getAll($scope.page.authToken).then( function(data) { $scope.page.loadSuccess(data); }, function(data, status, headers, config) { $scope.page.addAlert( {type: ‘danger’, msg: data.Error} ); }) .then(function () { $scope.page.loading = false; }) }; // load task (worklist) by id $scope.page.loadTask = function(id) { WorklistSrvc.get(id, $scope.page.authToken).then( function(data) { $scope.page.task = data; }, function(data, status, headers, config) { $scope.page.addAlert( {type: ‘danger’, msg: data.Error} ); }); }; // ‘Accept’ button handler. // Send worklist object with ‘$Accept’ action to server. $scope.page.accept = function(id) { // nothing to do, if no id if (!id) return; // get full worklist, set action and submit worklist. WorklistSrvc.get(id).then( function(data) { data.Task["%Action"] = "$Accept"; $scope.page.submit(data); }, function(data, status, headers, config) { $scope.page.addAlert( {type: ‘danger’, msg: data.Error} ); }); }; // ‘Yield’ button handler. // Send worklist object with ‘$Relinquish’ action to server. $scope.page.yield = function(id) { // nothing to do, if no id if (!id) return; // get full worklist, set action and submit worklist. WorklistSrvc.get(id).then( function(data) { data.Task["%Action"] = "$Relinquish"; $scope.page.submit(data); }, function(data, status, headers, config) { $scope.page.addAlert( {type: ‘danger’, msg: data.Error} ); }); }; // submit the worklist object $scope.page.submit = function(worklist) { // send object to server. If ok, refresh data on page. WorklistSrvc.save(worklist, $scope.page.authToken).then( function(data) { $scope.page.dataInit(); }, function(data, status, headers, config) { $scope.page.addAlert( {type: ‘danger’, msg: data.Error} ); } ); }; /* table section */ // sorting table $scope.page.sort = function(property, isUp) { $scope.page.predicate = property; $scope.page.isUp = !isUp; // change sorting icon $scope.page.sortIcon = ‘fa fa-sort-‘ + ($scope.page.isUp ? ‘up’:’down’) + ‘ pull-right’; }; // selecting row in table $scope.page.select = function(item) { if ($scope.page.grid.selected) { $scope.page.grid.selected.rowCss = »; if ($scope.page.grid.selected == item) { $scope.page.grid.selected = null; return; } } $scope.page.grid.selected = item; // change css class to highlight the row $scope.page.grid.selected.rowCss = ‘info’; }; // count currently displayed tasks $scope.page.totalCnt = function() { return $window.document.getElementById(‘tasksTable’).getElementsByTagName(‘TR’).length — 2; }; // if AssignedTo matches with current user — return ‘true’ $scope.page.isAssigned = function(selected) { if (selected) { if (selected.AssignedTo.toLowerCase() === $cookies[‘User’].toLowerCase()) return true; } return false; }; // watching for changes in ‘Search’ input // if there is change, reset the selection. $scope.$watch(‘query’, function() { if ($scope.page.grid.selected) { $scope.page.select($scope.page.grid.selected); } }); /* modal window open */ $scope.page.modalOpen = function (size, id) { // if no id — nothing to do if (!id) return; // obtainig the full object by id. If ok — open modal. WorklistSrvc.get(id).then( function(data) { // see http://angular-ui.github.io/bootstrap/ for more options var modalInstance = $modal.open({ templateUrl: ‘partials/task.csp’, controller: ‘TaskCtrl’, size: size, backdrop: true, resolve: { task : function() { return data; }, submit: function() { return $scope.page.submit } } }); // onResult modalInstance.result.then( function (reason) { if (reason === ‘save’) { $scope.page.addAlert( {type: ‘success’, msg: ‘Task saved’} ); } }, function () {}); }, function(data, status, headers, config) { $scope.page.addAlert( {type: ‘danger’, msg: data.Error} ); }); }; /* User’s validity checking. */ // If we get the data for other user, logout immediately $scope.page.checkUserValidity = function() { var user = $cookies[‘User’]; for (var i = 0; i < $scope.page.grid.items.length; i++) { if ($scope.page.grid.items[i].AssignedTo && (user.toLowerCase() !== $scope.page.grid.items[i].AssignedTo.toLowerCase())) { return false; } else if ($scope.page.grid.items[i].AssignedTo && (user.toLowerCase() == $scope.page.grid.items[i].AssignedTo.toLowerCase())) { return true; } } return true; }; // Check user’s validity every 10 minutes. setInterval(function() { $scope.page.dataInit() }, 600000); /* Initialize */ // sort table (by Age, asc) // to change sorting column change ‘columns[<index>]’ $scope.page.sort($scope.page.grid.columns[7].property, true); $scope.page.dataInit(); } // resolving minification problems TasksGridCtrl.$inject = [‘$scope’, ‘$window’, ‘$modal’, ‘$cookies’, ‘WorklistSrvc’]; controllersModule.controller(‘TasksGridCtrl’, TasksGridCtrl);

TaskCtrl — контроллер модального окна, содержащего подробную информацию о задаче. Формирует список полей и действий пользователя, а так же обрабатывает нажатия кнопок модального окна.
TaskCtrl.js’use strict’; // Task controller // dependency injection function TaskCtrl($scope, $routeParams, $location, $modalInstance, WorklistSrvc, task, submit) { $scope.page = { task:{} }; $scope.page.task = task; $scope.page.actions = ""; $scope.page.formFields = ""; $scope.page.formValues = task.Task[‘%FormValues’]; if (task.Task[‘%TaskStatus’].Request[‘%Actions’]) { $scope.page.actions = task.Task[‘%TaskStatus’].Request[‘%Actions’].split(‘,’); } if (task.Task[‘%TaskStatus’].Request[‘%FormFields’]) { $scope.page.formFields = task.Task[‘%TaskStatus’].Request[‘%FormFields’].split(‘,’); } // dismiss modal $scope.page.cancel = function () { $modalInstance.dismiss(‘cancel’); }; // perform a specified action $scope.page.doAction = function(action) { $scope.page.task.Task["%Action"] = action; $scope.page.task.Task[‘%FormValues’] = $scope.page.formValues; submit($scope.page.task); $modalInstance.close(action); } } // resolving minification problems TaskCtrl.$inject = [‘$scope’, ‘$routeParams’, ‘$location’, ‘$modalInstance’, ‘WorklistSrvc’, ‘task’, ‘submit’]; controllersModule.controller(‘TaskCtrl’, TaskCtrl);

app.js — файл, содержащий все модули приложения.
app.js’use strict’; /* Adding routes(when). [route], {[template path for ng-view], [controller for this template]} otherwise Set default route. $routeParams.id — :id parameter. */ var servicesModule = angular.module(‘servicesModule’,[]); var controllersModule = angular.module(‘controllersModule’, []); var app = angular.module(‘app’, [‘ngRoute’, ‘ngCookies’, ‘ui.bootstrap’, ‘servicesModule’, ‘controllersModule’]); app.config([ ‘$routeProvider’, function( $routeProvider ) { $routeProvider.when( ‘/tasks’, {templateUrl: ‘partials/tasks.csp’} ); $routeProvider.when( ‘/tasks/:id’, {templateUrl: ‘partials/task.csp’, controller: ‘TaskCtrl’} ); $routeProvider.otherwise( {redirectTo: ‘/tasks’} ); }]);

index.csp — главная страница приложения.
index.csp<!doctype html> <html> <head> <title>Ensemble Workflow</title> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> <!— CSS Initialization —> <link rel="stylesheet" type="text/css" href="css/bootstrap.min.css"> <link rel="stylesheet" type="text/css" href="css/font-awesome.min.css"> <link rel="stylesheet" type="text/css" href="css/bootstrap-theme.min.css"> <link rel="stylesheet" type="text/css" href="css/custom.css"> <script language="javascript"> // REST web-app name, global variable var RESTWebApp = {appName: ‘#($GET(^Settings("WF", "WebAppName")))#’}; </script> </head> <body ng-app="app" ng-controller="MainCtrl"> <nav class="navbar navbar-default navbar-fixed-top"> <div class="container-fluid"> <div class="navbar-header"> <a class="navbar-brand" href="#">Ensemble Workflow</a> </div> <div class="navbar-left"> <button ng-cloak ng-disabled="page.loginState != 1 || page.loading" type="button" class="btn btn-default navbar-btn" ng-click="page.dataInit();">Refresh Worklist</button> </div> <div class="navbar-left"> <form role="search" class="navbar-form"> <div class="form-group form-inline"> <label for="search" class="sr-only">Search</label> <input ng-cloak ng-disabled="page.loginState != 1" type="text" class="form-control" placeholder="Search" id="search" ng-model="query"> </div> </form> </div> <div class="navbar-right"> <form role="form" class="navbar-form form-inline" ng-show="page.loginState != 1" ng-model="user" ng-submit="page.doLogin(user.Login, user.PasswordSetter); user=»;" ng-cloak> <div class="form-group"> <input class="form-control uc-inline" ng-model="user.Login" placeholder="Username" ng-disabled="page.loading"> <input type="password" class="form-control uc-inline" ng-model="user.PasswordSetter" placeholder="Password" ng-disabled="page.loading"> <button type="submit" class="btn btn-default" ng-disabled="page.loading">Sign In</button> </div> </form> </div> <button ng-show="page.loginState == 1" type="button" ng-click="page.doExit();" class="btn navbar-btn btn-default pull-right" ng-cloak>Logout, <span class="label label-info" ng-bind="utils.readCookie(‘User’)"></span> </button> </div> </nav> <div class="container-fluid"> <div style="height: 20px;"> <div ng-show="page.loading" class="progress-bar progress-bar-striped progress-condensed active" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%" ng-cloak> Loading </div> </div> <!— Alerts —> <div ng-controller="AlertController" ng-cloak> <alert title="Click to dismiss" ng-repeat="alert in page.alerts" type="{{alert.type}}" ng-click="page.closeAlert($index, alert)">{{alert.msg}}</alert> </div> <div ng-show="page.loginState != 1" class="attention" ng-cloak> <p>Please, Log In first.</p> </div> <!— Loading template —> <div ng-view> </div> </div> </div> <!— Hooking scripts —> <script language="javascript" src="libs/angular.min.js"></script> <script language="javascript" src="libs/angular-route.min.js"></script> <script language="javascript" src="libs/angular-cookies.min.js"></script> <script language="javascript" src="libs/ui-bootstrap-custom-tpls-0.12.0.min.js"></script> <script language="javascript" src="libs/base64.js"></script> <script language="javascript" src="js/app.js"></script> <script language="javascript" src="js/services/RESTSrvc.js"></script> <script language="javascript" src="js/services/WorklistSrvc.js"></script> <script language="javascript" src="js/services/SessionSrvc.js"></script> <script language="javascript" src="js/services/UtilSrvc.js"></script> <script language="javascript" src="js/controllers/MainCtrl.js"></script> <script language="javascript" src="js/controllers/TaskCtrl.js"></script> <script language="javascript" src="js/controllers/TasksGridCtrl.js"></script> <script language="javascript" src="libs/jquery-1.11.2.min.js"></script> <script language="javascript" src="libs/bootstrap.min.js"></script> </body> </html>

tasks.csp — шаблон таблицы списка задач.
tasks.csp<div class="row-fluid"> <div class="span1"> </div> <div ng-hide="page.loginState != 1 || (page.loading && !page.totalCnt())" ng-controller="TasksGridCtrl"> <div class="panel panel-default top-buffer"> <table class="table-tasks" ng-class="page.grid.cssClass" id="tasksTable"> <caption class="text-left"> <b ng-bind="page.grid.caption"></b><b id="updateTime" ng-bind="page.grid.updateTime"></b> </caption> <thead style="cursor: pointer; vertical-align: middle;"> <tr> <th class="text-center">#</th> <!— In the cycle prints the name of the column, specify for each column click handler and the icon (sorting) —> <th ng-repeat="column in page.grid.columns" class="text-center" ng-click="page.sort(column.property, page.isUp)"> <span ng-bind="column.name" style="padding-right: 4px;"></span> <i style="margin-top: 3px;" ng-class="page.sortIcon" ng-show="column.property == page.predicate"></i> <i style="color: #ccc; margin-top: 3px;" class="fa fa-sort pull-right" ng-show="column.property != page.predicate"></i> </th> <th class="text-center">Action</th> </tr> </thead> <tfoot> <tr> <!— Control buttons and messages —> <td colspan="{{page.grid.columns.length + 2}}"> <p ng-hide="page.grid.items.length">There is no task(s) for current user.</p> <span ng-show="page.grid.items.length"> Showing {{page.totalCnt()}} of {{page.grid.items.length}} task(s). </span> </td> </tr> </tfoot> <tbody style="cursor: default;"> <!— In the cycle prints the table rows (sort by specified column) —> <tr ng-repeat="item in page.grid.items | orderBy:page.predicate:page.isUp | filter:query" ng-class="item.rowCss" > <td ng-bind="$index + 1" class="text-right"></td> <!— In the cycle prints the table cells to each row —> <td ng-repeat="column in page.grid.columns" style="text-align: {{column.align}};" ng-click="page.select(item)"> <span class="label label-info" ng-show="$first && item.New">New</span> <span ng-hide="$first" ng-bind="utils.getPropertyValue(item, column.property)"></span> </td> <td class="text-center"> <div title="Accept task" class="button button-success fa fa-plus-circle" ng-click="page.accept(item.ID)" ng-show="!page.isAssigned(item)"></div> <div title="Details" class="button button-info fa fa-search" ng-click="page.modalOpen(‘lg’, item.ID)" ng-show="page.isAssigned(item)"></div> <div title="Yield task" class="button button-danger fa fa-minus-circle" ng-click="page.yield(item.ID)" ng-show="page.isAssigned(item)"></div> </td> </tr> </tbody> </table> </div> </div> <div class="span1"> </div> </div> <br>

task.csp — шаблон модального окна.
task.csp <div class="modal-header"> <h3 class="modal-title">Task description</h3> </div> <div class="modal-body"> <div class="container-fluid"> <div class="row top-buffer"> <div class="col-xs-12 col-md-6"> <div class="form-group"> <label for="subject">Subject</label> <input id="subject" type="text" class="form-control task-info-input" ng-model="page.task.Task[‘%TaskStatus’].Request[‘%Subject’];" readonly> </div> </div> <div class="col-md-6"> <div class="form-group"> <label for="timeCreated">Time created</label> <input id="timeCreated" type="text" class="form-control task-info-input" ng-model="page.task.Task[‘%TaskStatus’].TimeCreated;" readonly> </div> </div> </div> <div class="row"> <div class="col-md-12"> <div class="form-group"> <label for="message">Message</label> <textarea id="message" class="form-control task-info-input" ng-model="page.task.Task[‘%TaskStatus’].Request[‘%Message’];" rows="3" readonly></textarea> </div> </div> </div> <div class="row"> <div class="col-md-6"> <div class="form-group"> <label for="role">Role</label> <input id="role" type="text" class="form-control task-info-input" ng-model="page.task.Task[‘%TaskStatus’].Role.Name;" readonly> </div> </div> <div class="col-md-3"> <div class="form-group"> <label for="assignedTo">Assigned to</label> <input id="assignedTo" type="text" class="form-control task-info-input" ng-model="page.task.Task[‘%TaskStatus’].AssignedTo;" readonly> </div> </div> <div class="col-md-3"> <div class="form-group"> <label for="priority">Priority</label> <input id="priority" type="text" class="form-control task-info-input" ng-model="page.task.Task[‘%Priority’];" readonly> </div> </div> </div> <div class="row" ng-show="page.formFields"> <div class="delimeter col-md-6 el-centered"> </div> </div> <div class="row" ng-repeat="formField in page.formFields"> <div class="col-md-12"> <div class="form-group"> <label for="form{{$index}}" ng-bind="formField"></label> <input id="form{{$index}}" type="text" class="form-control task-info-input" ng-model="page.formValues[formField]"> </div> </div> </div> </div> </div> <div class="modal-footer"> <button ng-repeat="action in page.actions" class="btn btn-primary top-buffer" ng-click="page.doAction(action)" ng-bind="action"></button> <button class="btn btn-success top-buffer" ng-click="page.doAction(‘$Save’)">Save</button> <button class="btn btn-warning top-buffer" ng-click="page.cancel()">Cancel</button> </div>

Так же, никто не запрещает использовать наш REST API для своего UI, тем более он довольно прост.
URL map нашего REST API<Routes> <Route Url="/logout" Method="GET" Call="Logout"/> <Route Url="/tasks" Method="GET" Call="GetTasks"/> <Route Url="/tasks/:id" Method="GET" Call="GetTask"/> <Route Url="/tasks/:id" Method="POST" Call="PostTask"/> <Route Url="/test" Method="GET" Call="Test"/> </Routes>

Вы можете опробовать пользовательский интерфейс на нашем тестовом сервере, на котором запущено приложение HelpDesk. Login: dev / Pass: 123 http://habrahabr.ru/post/251611/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *