Те, кто знаком с платформой для интеграции и разработки приложений 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 приложения:
Процесс установки описан в 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 для осуществления запросов и их разделение носит, по существу, функциональный характер.
'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’е токен авторизации.
'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 по имени, получение значения свойства объекта по имени.
'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 отвечает за выполнение запросов, связанных с данными списка задач.
'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 — главный контроллер приложения, отвечает за аутентификацию пользователя.
'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 — контроллер, отвечающий за таблицу списка задач и действия с ней. Он инициализирует таблицу списка задач, содержит методы для загрузки списка задач и конкретной задачи, а так же методы обработки действий пользователя (нажатие кнопок, сортировка таблицы, выделение строки таблицы, фильтрация).
'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 — контроллер модального окна, содержащего подробную информацию о задаче. Формирует список полей и действий пользователя, а так же обрабатывает нажатия кнопок модального окна.
'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 — файл, содержащий все модули приложения.
'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 — главная страница приложения.
<!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 — шаблон таблицы списка задач.
<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 — шаблон модального окна.
<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, тем более он довольно прост.
<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
Автор: iEcho