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 приложения:

    Процесс установки описан в 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
    Метки:
    InterSystems 119,76
    Вендор: СУБД Caché, OLAP DeepSee, шина Ensemble
    Поделиться публикацией
    Похожие публикации
    Комментарии 0

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

    Самое читаемое