UI for Ensemble Workflow in Angular

This article is translation of one from Habrahabr InterSystems blog (Russian).
The original post can be found here: https://habrahabr.ru/company/intersystems/blog/251611/
 

Everyone familiar with InterSystems Ensemble, an integration and application development platform, knows what the Ensemble Workflow subsystem is and how useful it can be for automating human interaction. For those who don’t know Ensemble (and/or Workflow), I will briefly describe its capabilities (others may skip this part and learn how they can use the Workflow interface in Angular.js).

InterSystems Ensemble

InterSystems Ensemble is an integration and application development platform intended for integrating heterogeneous systems, automating business processes and creating new composite applications that augment the functionality of integrated applications with new business logic or a new user interface: EAI, SOA, BPM, BAM and even BI (thanks to InterSystems DeepSee, a built-in technology for developing analytical applications).

Ensemble has the following key components:

Adaptors – components that interact with applications, technologies and data sources. Ensemble is supplied with technology and application integration adaptors (web and REST services, File, FTP, Email, SQL, EDI, HL7, SAP, Siebel, 1S Enterprise, etc.). You can create your own adaptors using the Adaptor SDK.

Business services – components that transform data coming from external systems into Ensemble messages and start business processes and/or business operations.

Business processes – executable processes used for orchestrating services and operations for automating the interaction between systems and/or people (via the Workflow subsystem). Processes are either described in the declarative Business Process Language or are implemented with Caché Object Script. The logic of interaction with the outside world is separated from the specific implementation of this interaction by services and operations.

Business operations – components that are responsible for sending/receiving messages to and from external systems, and converting Ensemble messages into formats compatible with such systems.

Message transformation – Ensemble components that convert messages from one format to another using the declarative Data Transformation Language.

Business rules allow the administrators of an integrated solution to change the behavior of Ensemble business processes in particular decision points without writing a line of code.

Workflow management – the Ensemble Workflow subsystem provides the automation of task assignment.

Business metrics allow you to collect and calculate KPI’s. Combined with dashboards, they are used for implementing business activity monitoring (BAM) solutions.

Let’s go back to workflow management and take a closer look at the functionality of the Ensemble Workflow subsystem.

Workflow management and the Ensemble Workflow subsystem

According to the definition by Workflow Management Coalition (www.WfMC.org), “workflows” are fully or partially automated business processes, where documents, information or tasks are passed from one participant to another according to established rules and procedures.”

Key aspects of a Workflow:

  • The purpose of a Workflow is to cover a “fragment” of work
  • A workflow is a set of procedural task execution rules
  • A Workflow user is someone working on tasks in the workflow management system
  • A role in a Workflow is a group of users working on a particular type of tasks.

The workflow management subsystem in Ensemble enables you to do the following:

  • Automate workflow management using Ensemble business processes
  • Flexibly configure the task assignment process
  • Work with a workflow management system via a special Workflow portal supplied with Ensemble
  • Organize the interaction of the workflow management subsystem with Ensemble's integration business processes
  • Use the business activity monitoring subsystem, Ensemble's management and monitoring tools
  • Easily configure and extend the functionality of the Workflow subsystem

The simplest example of workflow management automation is the Ensemble HelpDesk application that automates the interaction of support personnel and is part of the standard set of Ensemble examples (in the Ensdemo space). Ensemble receives issue reports and starts the HelpDesk business process.

A fragment of the HelpDesk business process algorithm

 

A business process sends a task to users with the Demo-Development role using a message of the EnsLib.Workflow.TaskRequest class, which defines all possible actions (“Fixed” or “Ignored”), as well as the “Comment” field. The body of the message also contains infromation about the error and the user who reported it. After this, a corresponding task appears in the Workflow portral of every user with a Demo-Development role.

Initially (if not defined in the TaskRequest message), the task is not associated with any particular user (just with a role), so the user has to accept it by clicking the corresponding button. You can also reject the task by clicking the “Defer” button.

Once done, you can perform any actions allowed for this task. In our case, we can click the “Fixed” button after providing a comment in the corresponding field. The HelpTask business process will process this event and will send a new message to users with a Demo-Testing role, thus signaling about the need to test the changes. If you click the “Ignored” button, the task will be marked as “Not a problem”, and its processing will stop.

As you can see from this example, Ensemble Workflow is a simple and intuitive system for organizing users’ workflows. More detailed information about the Ensemble Workflow subsystem can be found in the Defining Workflow section of the Ensemble manual.

The functionality of the Ensemble Workflow subsystem can be easily expended and integrated into an external composite application based on InterSystems Ensemble. As an example, let's take a look at an implementation of the user interface of Ensemble Workflow in an external composite application developed with Angular.js + REST API (It was written by Eduard Lebedyuk).

Ensemble Workflow interface in Angular.js.

For the Workflow interface to work with Angular.js, you need to install the following Ensemble applications on the server:

The installation process is described in the Readme files of the specified repositories.

At the moment (of original post publishing), the application has all the necessary functionality of Ensemble Workflow: display of the task list, additional fields and actions, sorting, full-text search in tasks. The user can accept/decline tasks. Detailed information about a task is shown in a modal window. (The implementation is just proof of concept and can be greatly improved. It also uses BasicAuth in a way which must not be used in production. We already have a far more complex examples).

At the moment of writing (original post), the application looks like this:

 

The UI uses the following libraries and frameworks: Angular.js, Twitter Bootstrap as well as FontAwesome icon fonts.

You can check out the user interface on our test server with the HelpDesk application running on it. Login: dev / Pass: 123

 

For those who are interested in the source code.

Here's structure of this small app:

The application has 4 Angular services (RESTSrvc, SessionSrvc, UtilSrvc and WorklistSrvc), 3 controllers (MainCtrl, TaskCtrl, TasksGridCtrl), a main page (index.csp) and 2 templates (task.csp and tasks.csp).

The RESTSrvc service has a single method, getPromise, and is a wrapper around the $http Angular.js service. The only purpose of RESTSrvc is to send HTTP requests to the server and return promise objects of these requests. Other services use RESTSrvc for making requests and their separation is, in essence, of a functional nature (it could be written better, I know ;) The code is 1.5 years old).

'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; 
        }
    }
};

RESTSrvc.$inject = ['$http', '$q']; servicesModule.factory('RESTSrvc', RESTSrvc);

SessionSrvc — contains a single method responsible for session closing. Authentication in this application was implemented using Basic access authentication (http://en.wikipedia.org/wiki/Basic_access_authentication), so there is no need for a separate authentication method, since every request has an authorization token in its 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 }
            });
        }
    }
};

SessionSrvc.$inject = ['RESTSrvc'];
servicesModule.factory('SessionSrvc', SessionSrvc);

UtilSrvc — contains auxiliary methods, such as getting a cookie value by name, getting object properties by name.

'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;
        }
    }
};

UtilSrvc.$inject = ['$cookies'];
servicesModule.factory('UtilSrvc', UtilSrvc);

WorklistSrvc is responsible for requests related to the task list data.

'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 } });
        }
    }
};

WorklistSrvc.$inject = ['RESTSrvc'];
servicesModule.factory('WorklistSrvc', WorklistSrvc);

MainCtrl — the main controller of the application responsible for user authentication.

'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 });
            });
    };

}

MainCtrl.$inject = ['$scope', '$location', '$cookies', 'WorklistSrvc', 'SessionSrvc', 'UtilSrvc'];
controllersModule.controller('MainCtrl', MainCtrl);

TasksGridCtrl — a controller responsible for the task list table and actions associated with it. It initializes the task list table, contains methods for loading the task list and specific tasks, as well as methods for processing users' actions (key presses, table sorting, row selection, filtering).

'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();
}

TasksGridCtrl.$inject = ['$scope', '$window', '$modal', '$cookies', 'WorklistSrvc'];
controllersModule.controller('TasksGridCtrl', TasksGridCtrl);

TaskCtrl — controller of the modal window containing detailed information about a task. Forms a list of fields and user actions, also processes clicks on buttons within the modal window.

'use strict';

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 — the file containing all application modules.

'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 — the main page of the application.

<!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 — the task list table template.

<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 — the modal window template.

 <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>

Also, you are free to use our REST API for your UI, especially given that it’s quite simple.

The URL map of our 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>

 

Comments

This is great! I've been recently investigating alternatives towards managing workflow tasks outside of DeepSee for clients. Thanks for taking the time in making this happen!

Do you happen to know in what HS version the Workflow REST API became available? I'm hoping I can make it available in our HS 2016.1 instances.

Workflow REST API is a separate project available on GitHub. You can install it on any version that supports REST, so Caché 2014.1+.

I pulled that one down and I've been able to validate the majority of the services.  I did run into an issue when saving a referral that I posted into the GitHub issues but I'm not sure if that is monitored so I thought I'd leave a comment here too.