UI для Ensemble Workflow на Angular

image

Те, кто знаком с платформой для интеграции и разработки приложений 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).

image

Вернемся к управлению потоками работ и рассмотрим функционал подсистемы 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.
image
Фрагмент алгоритма бизнес-процесса HelpDesk

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

Первоначально (если это не задано в сообщении 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: отображение списка задач, дополнительных полей и действий, сортировка, полнотекстовый поиск по задачам. Пользователь может принимать/отклонять задачи, подробная информация о задаче выводится в модальном окне.

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

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

image
Для последующей модификации интерфейса при необходимости, был использован 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

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

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