In this article we will look at how you can use the Alfresco Process Services (APS) related ADF components to build a Process Management application. This could be useful if you are about to start on an ADF trial, you need to build a PoC with ADF, or you just want to play around with it and see what's available.
This article builds on two other articles in the ADF Developer Series. The first article talks about how to generate an application with Angular CLI and prepare it for use with ADF 2.0.0 and the second article improves on the first article by adding a navigation system, menu, toolbar, and logout functionality.
We want our new process management application interface to look something like this:
This application should allow the user to browse the Process Applications available in APS. While browsing the Apps the user will be able to look at the associated Process Definitions, and start processes based on those definitions. It will then be possible to interact with the active Task Instances. Running Process Instances can also be inspected directly. The application will cover most of of the available APS related ADF components.
It is likely that you will create different types of Process Management applications depending on the user role. And you will probably not use all the ADF components shown in this article in every application. For example, if the user should only interact with ongoing Task Instances assigned to them, then you would use ADF components shown in the My Tasks section. If the user should be able to start Process Instances, then you would look at ADF components talked about in the Process Apps section. If the user is a sys admin, then they would most likely benefit from the ADF components used in the My Processes section.
Before we start building an application with the Process Services related ADF components it might be a good idea to know what they actually are. At first you might think, great, now I can build a custom UI for all the workflows I got deployed in my Alfresco Content Services (ACS) installation. As you know, ACS has the Activiti workflow engine embedded, and most consulting projects revolving around Alfresco has traditionally implemented business processes with this workflow engine. The Process Services ADF components cannot be used for ACS workflows. They can only be used for Alfresco’s stand-alone Business Process Management solution, which is Alfresco Process Services (APS).
The Process Services ADF components uses indirectly, via the Alfresco JavaScript API, the APS ReST API, which is separate from the ACS ReST API.
The Process Services ADF components can also not currently be used to design and implement workflows. That still need to be done via the APS App Designer.
This article is part of series of articles covering ADF 2.0:
This articles assumes that you are starting with an ADF application that has a menu, navigation, toolbar, and logout as per the https://community.alfresco.com/community/application-development-framework/blog/2017/12/15/adding-na... article. You can either walkthrough this article first, or clone the source code as follows:
Martins-Macbook-Pro:ADF mbergljung$ git clone https://github.com/gravitonian/adf-workbench-nav20.git adf-workbench-process20
This clones the starter project within a new directory called adf-workbench-process20. Install all the packages for the project like this:
Martins-Macbook-Pro:ADF mbergljung$ cd adf-workbench-process20
Martins-Macbook-Pro:adf-workbench-process20 mbergljung$ npm install
Martins-Macbook-Pro:adf-workbench-process20 mbergljung$ npm dedup
The de-duplication (i.e. npm dedup) attempts to removes all duplicate packages by moving dependencies further up the tree, where they can be more effectively shared by multiple dependent packages.
While walking through this article it is a good idea to have the source codeavailable. You can clone the source as follows:
Martins-Macbook-Pro:ADF mbergljung$ git clone https://github.com/gravitonian/adf-workbench-process20.git adf-workbench-process20-src
Now that we have the Angular app ready for ADF, and we can login to APS, it’s time to add some Process Services specific ADF components. Let’s start by building a Process Application view. Why do we do this? It is because when you work with processes in Alfresco Process Services (APS) they are all contained within applications. You usually start by creating a Process App and then you define one or more process definitions and add them to it.
We are going to need a Process Apps parent page component. It will contain two child page components, one for the Process Apps List and one for the Process Apps Details page component. Plus a module to keep everything organized.
As usual, we can easily create a new module and the needed components with the Angular CLI tool:
Martins-MacBook-Pro:adf-workbench-process20 mbergljung$ ng g module process-apps --flat false --routing
create src/app/process-apps/process-apps-routing.module.ts (254 bytes)
create src/app/process-apps/process-apps.module.ts (300 bytes)
Martins-MacBook-Pro:adf-workbench-process20 mbergljung$ cd src/app/process-apps/
Martins-MacBook-Prorocess-apps mbergljung$ ng g component process-apps-page
create src/app/process-apps/process-apps-page/process-apps-page.component.css (0 bytes)
create src/app/process-apps/process-apps-page/process-apps-page.component.html (36 bytes)
create src/app/process-apps/process-apps-page/process-apps-page.component.spec.ts (693 bytes)
create src/app/process-apps/process-apps-page/process-apps-page.component.ts (311 bytes)
update src/app/process-apps/process-apps.module.ts (416 bytes)
Martins-MacBook-Prorocess-apps mbergljung$ ng g component process-apps-list-page
create src/app/process-apps/process-apps-list-page/process-apps-list-page.component.css (0 bytes)
create src/app/process-apps/process-apps-list-page/process-apps-list-page.component.html (41 bytes)
create src/app/process-apps/process-apps-list-page/process-apps-list-page.component.spec.ts (722 bytes)
create src/app/process-apps/process-apps-list-page/process-apps-list-page.component.ts (330 bytes)
update src/app/process-apps/process-apps.module.ts (552 bytes)
Martins-MacBook-Prorocess-apps mbergljung$ ng g component process-apps-details-page
create src/app/process-apps/process-apps-details-page/process-apps-details-page.component.css (0 bytes)
create src/app/process-apps/process-apps-details-page/process-apps-details-page.component.html (44 bytes)
create src/app/process-apps/process-apps-details-page/process-apps-details-page.component.spec.ts (743 bytes)
create src/app/process-apps/process-apps-details-page/process-apps-details-page.component.ts (342 bytes)
update src/app/process-apps/process-apps.module.ts (700 bytes)
This creates a Process Apps module with routing, a Process Apps parent page, and a Process Apps list page component where we will display the available process applications. It also creates a Process Apps details page where we can display more detailed information for a specific process application.
We need to set up a new route configuration with a parent route that has two child routes for the list page and details page. Let’s add it in the src/app/process-apps/process-apps-routing.module.ts file as follows:
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { ProcessAppsPageComponent } from './process-apps-page/process-apps-page.component';
import { ProcessAppsListPageComponent } from './process-apps-list-page/process-apps-list-page.component';
import { ProcessAppsDetailsPageComponent } from './process-apps-details-page/process-apps-details-page.component';
import { AuthGuardBpm } from '@alfresco/adf-core';
const routes: Routes = [ {
path: 'process-apps',
component: ProcessAppsPageComponent,
canActivate: [AuthGuardBpm],
data: {
title: 'Process Apps',
icon: 'apps',
hidden: false,
needBpmAuth: true,
isLogin: false
},
children: [
{ path: '', component: ProcessAppsListPageComponent, canActivate: [AuthGuardBpm] },
{ path: ':process-app-id', component: ProcessAppsDetailsPageComponent, canActivate: [AuthGuardBpm] }
]
}];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class ProcessAppsRoutingModule { }
When we use the http://localhost:4200/process-apps URL we will hit the parent page component ProcessAppsPageComponent and as there is a child component ProcessAppsListPageComponent
with empty path '' it will automatically be invoked and the process apps list displayed.
When one of the items in the process apps list is clicked, and we select a Details content action from the 'Three Dots' menu, then the http://localhost:4200/process-apps/<process-app-id> URL will be invoked taking the user to the ProcessAppsDetailsPageComponent.
If the data object properties are not familiar, then read the previous two articles mentioned in the introduction. They explain everything around these properties and the navigation system.
The AuthGuardBpm ADF component makes sure that the route cannot be activated if the user is not authenticated with APS. Which leads us to make sure that the app is set up to authenticate with APS and only that backend service. Open the src/app/app-login/app-login-page/app-login-page.component.html template file and make sure the providers property is configured as follows:
<div fxFlex="100">
<adf-login class="app-logo"
[providers]="'BPM'"
[copyrightText]="'© 2017 Alfresco Training.'"
Also, when we only login to the BPM server we need to change the function that gets the current user, it is currently getting the username from the ECM server session. Open up the src/app/app.component.ts file and update the getCurrentUser function as follows:
getCurrentUser() {
return this.authService.getBpmUsername();
}
For these routes to be known to the Angular Router, and then indirectly to the AppMenuService, we need to import this module in the AppModule. Open the src/app/app.module.ts file and add as follows:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { AppCommonModule } from './app-common/app-common.module';
import { AppLoginRoutingModule } from './app-login/app-login-routing.module';
import { AppLoginModule } from './app-login/app-login.module';
import { AppMenuService } from './app-menu/app-menu.service';
import { ProcessAppsModule } from './process-apps/process-apps.module';
import { ProcessAppsRoutingModule } from './process-apps/process-apps-routing.module';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
AppRoutingModule,
AppCommonModule,
AppLoginModule,
AppLoginRoutingModule,
ProcessAppsModule,
ProcessAppsRoutingModule
],
providers: [AppMenuService],
bootstrap: [AppComponent]
})
export class AppModule { }
First, start up the server as follows:
Martins-MacBook-Pro:adf-workbench-process20 mbergljung$ npm start
Then access the app at http://localhost:4200 and login to APS:
You should see the Process Apps menu item in the left Navigation Menu. Let's configure the login component to route to the Process Apps parent page directly after a successful login. Open up the src/app/app-login/app-login-page/app-login-page.component.ts file and uncomment the router code in the onLoginSuccess method and have it navigate to /process-apps:
onLoginSuccess($event) {
console.log('Successful login: ' + $event.value);
// Tell parent component that successful login has happened and menu should change
this.menuService.fireMenuChanged();
// Now, navigate somewhere...
this.router.navigate(['/process-apps']);
}
However, it will not work just yet as there is no router outlet for the child page. This can be fixed by putting a router-outlet in the Process Apps parent page template. Open up the src/app/process-apps/process-apps-page/process-apps-page.component.html file and add it as follows:
<router-outlet></router-outlet>
If you now again logout and then login, you will see that the Process Apps List page is displayed automatically after a successful login:
Now we can move on and implement the Process Apps List component that will provide a list of the applications that the logged in user has access to. Let’s start with the page template for the Process App list. Open up the src/app/process-apps/process-apps-list-page/process-apps-list-page.component.html file and replace whatever is there with the following:
<div class="process-apps-list-height">
<adf-apps
[layoutType]="'LIST'"
[filtersAppId]="processAppsFilter"
(appClick)="onAppClick($event)">
</adf-apps>
</div>
Here we are configuring the page view to display a list of Process Applications with the ADF component <adf-apps. We enclose the Process Apps list in a div with a class that sets the height of the page so the left navigation menu will be visible even if there are no apps available, or just one. Add the CSS class to the src/app/process-apps/process-apps-list-page/process-apps-list-page.component.css file as follows:
.process-apps-list-height {
height:900px;
}
The <adf-apps component is not yet known to the ProcessAppsModule, so your editor might scream about it. Import it as follows in the src/app/process-apps/process-apps.module.ts file:
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ProcessAppsRoutingModule } from './process-apps-routing.module';
import { ProcessAppsPageComponent } from './process-apps-page/process-apps-page.component';
import { ProcessAppsListPageComponent } from './process-apps-list-page/process-apps-list-page.component';
import { ProcessAppsDetailsPageComponent } from './process-apps-details-page/process-apps-details-page.component';
import { AppCommonModule } from '../app-common/app-common.module';
@NgModule({
imports: [
CommonModule,
ProcessAppsRoutingModule,
/* Common App imports (Angular Core and Material, ADF Core, Content, and Process */
AppCommonModule
],
declarations: [ProcessAppsPageComponent, ProcessAppsListPageComponent, ProcessAppsDetailsPageComponent]
})
export class ProcessAppsModule { }
Here we import the AppCommonModule, which gives us access to the <adf-apps component, which is part of the @alfresco/adf-process-services package. At the same time we also get access the Angular Material components and the ADF Core components.
We are configuring the process app list layoutType to LIST so we will see a view such as in this example:
Note that you will see other apps in your list depending on what you have previously created in APS. Or you will just see the Task App if you have not yet created any Process Apps.
It is also possible to use the more familiar, at least if you have used APS for a bit, grid view. Setting [layoutType]="'GRID'" will display as:
Choose whichever layout type you like, I will stick with LIST in this article.
You can control what process apps that are displayed via the filtersAppId property. In this case it is set to class variable processAppsFilter, add this variable to the class as follows:
export class ProcessAppsListPageComponent implements OnInit {
processAppsFilter = [ { tenantId: 1 } ];
constructor() { }
ngOnInit() {
}
}
This means that a Process App needs to be associated with tenant 1 to be included. When we skip this filter we also see the out-of-the-box Task App. We don’t want the Task App included as it isn't a Process App in the sense that it contains Process Definitions. And we are also going to build our own “My Tasks” view where we can take part in active workflows and complete tasks.
The last configuration for the Process App list is an event to capture when an app in the list is clicked. The onAppClick method is then called so we can navigate to the process app details page. Open up the src/app/process-apps/process-apps-list-page/process-apps-list-page.component.ts file and update it as follows:
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { AppDefinitionRepresentationModel } from '@alfresco/adf-process-services';
@Component({
selector: 'app-process-apps-list-page',
templateUrl: './process-apps-list-page.component.html',
styleUrls: ['./process-apps-list-page.component.scss']
})
export class ProcessAppsListPageComponent implements OnInit {
processAppsFilter = [ { tenantId: 1 } ];
constructor(private router: Router) { }
ngOnInit() {
}
onAppClick(appDef: AppDefinitionRepresentationModel) {
console.log('Navigating to App Definition : ', appDef);
this.router.navigate(['/process-apps', appDef.id]);
}
}
When an app is clicked in the list the event handler method is passed an object of type AppDefinitionRepresentationModel. It looks like this:
export class AppDefinitionRepresentationModel {
defaultAppId: string;
deploymentId: string;
name: string;
description: string;
theme: string;
icon: string;
id: number;
modelId: number;
tenantId: number;
So we can see that we already at this point know a lot of information about the process app. Although we only need the id to pass on as URL parameter when we navigate to the Process App Details page.
This finishes off the Process App list page. Let’s move on with the details page.
When the user clicks on one of the Process Apps a detail page such as the following should be displayed:
The page will have a new blue toolbar with two buttons, one for displaying the process definitions that are associated with the app, and one for closing the details view. Under the toolbar the process app properties are displayed in an ADF Card View.
Let’s start with the page template, open up the src/app/process-apps/process-apps-details-page/process-apps-details-page.component.html file and update it so it looks like this:
<adf-toolbar [color]="'accent'" *ngIf="appDef">
<adf-toolbar-title>
<span>{{ appDef.name }}</span>
</adf-toolbar-title>
<button mat-icon-button
matTooltip="Show Process Definitions included in app"
(click)="onShowProcDefs($event)">
<mat-icon>device_hub</mat-icon>
</button>
<adf-toolbar-divider></adf-toolbar-divider>
<button mat-icon-button
class="adf-viewer-close-button"
matTooltip="Close and go back to Process App list"
(click)="onGoBack($event)"
aria-label="Close">
<mat-icon>close</mat-icon>
</button>
</adf-toolbar>
<mat-card class="adf-card-container" *ngIf="appDef">
<mat-card-content>
<adf-card-view
[properties]="properties"
[editable]="false">
</adf-card-view>
</mat-card-content>
</mat-card>
We start by using the ADF specific toolbar (<adf-toolbar), which will have a title and two buttons. One button to navigate to the associated Process Definitions and one two navigate back to the Process App list. The mat-icon value is as usual taken from the list of available Material Design icons.
We then use some of the Angular Material components (i.e. all tags starting with mat-) to build the Process App Details view. These components are already imported via the AppCommonModule. To present the Process App Details we use the ADF Card View component (<adf-card-view), which is part of the @alfresco/adf-core package. The Card view can be used to present a list of properties in an easy way.
If you need a more sophisticated approach for your properties layout, then have a look at the ADF Form component. It is described in the Content Management App article.
The backing component class is implemented in the src/app/process-apps/process-apps-details-page/process-apps-details-page.component.ts file and looks like this:
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { AppsProcessService, CardViewItem, CardViewTextItemModel } from '@alfresco/adf-core';
import { AppDefinitionRepresentation } from 'alfresco-js-api';
@Component({
selector: 'app-process-apps-details-page',
templateUrl: './process-apps-details-page.component.html',
styleUrls: ['./process-apps-details-page.component.scss']
})
export class ProcessAppsDetailsPageComponent implements OnInit {
appDef: AppDefinitionRepresentation;
properties: Array<CardViewItem>;
constructor(private router: Router,
private activatedRoute: ActivatedRoute,
private processAppService: AppsProcessService) {
this.properties = new Array<CardViewItem>();
}
ngOnInit() {
const appId: number = +this.activatedRoute.snapshot.params['process-app-id'];
console.log('Showing app details for: ', appId);
this.processAppService.getApplicationDetailsById(appId).subscribe(
(appDef: AppDefinitionRepresentation) => {
console.log('App details: ', appDef);
this.appDef = appDef;
this.setupProps(this.appDef);
},
(error) => {
console.log('Error: ', error);
});
}
private setupProps(appDef: AppDefinitionRepresentation) {
console.log('setupProps: ', appDef.id);
// Properties that are always available
const idProp = new CardViewTextItemModel({label: 'Id:', value: appDef.id, key: 'appId'});
const defaultAppIidProp = new CardViewTextItemModel({label: 'Default App Id:', value: appDef.defaultAppId, key: 'defaultAppId'});
const tenantIdProp = new CardViewTextItemModel({label: 'Tenant Id:', value: appDef.tenantId, key: 'tenantId'});
const deploymentIdProp = new CardViewTextItemModel({label: 'Deployment Id:', value: appDef.deploymentId, key: 'deploymentId'});
const modelIdProp = new CardViewTextItemModel({label: 'Model Id:', value: appDef.modelId, key: 'modelId'});
const nameProp = new CardViewTextItemModel({label: 'Name:', value: appDef.name, key: 'appName'});
const descProp = new CardViewTextItemModel({label: 'Description:', value: appDef.description, key: 'appDesc'});
const iconProp = new CardViewTextItemModel({label: 'Icon:', value: appDef.icon, key: 'appIcon'});
this.properties.push(idProp);
this.properties.push(defaultAppIidProp);
this.properties.push(tenantIdProp);
this.properties.push(deploymentIdProp);
this.properties.push(modelIdProp);
this.properties.push(nameProp);
this.properties.push(descProp);
this.properties.push(iconProp);
}
onGoBack($event: Event) {
this.navigateBack2AppList();
}
private navigateBack2AppList() {
this.router.navigate(['../'],
{
relativeTo: this.activatedRoute
});
}
onShowProcDefs($event: Event) {
}
}
The component class defines the appDef and properties variables that are referred to in the template. The appId is passed in via the URL, which looks like http://localhost:4200/process-apps/<appId>. We extract the appId in the ngOnInit method via the activedRoute object. Important to note here is that the getApplicationDetailsById function expects the type of the appId to be a number. So we need to make sure it is a number and do this.appId = +this.activatedRoute.snapshot.params.
When we have the App ID we use the ADF AppsProcessService to fetch an AppDefinitionRepresentation object representing the info about the Process App. We then use this object to set up the properties that backs the ADF Card View. Properties are displayed in the order that they are added to the property array. There are a number of CardViewItem subclasses that we can use depending on the type of property, such as CardViewTextItemModel and CardViewDateItemModel. For more info see card view docs.
We implement the navigate back handler by just stepping up to the parent route. We don’t need to pass anything back to the Process App list as it always just displays all apps.
The onShowProcDefs is left empty at the moment and will be implemented in the next section.
That should do it, you can now try out the Process App list and details pages.
Each Process App can have many different Process Definitions associated with it. So when a user is given access to the Process App they can start process instances based on any of these Process Definitions.
To display Process Definitions for an app we are going to need a Process Definitions List page, it will look something like this:
The user will be able to click on the Three Dots menu to the right to navigate to a details page for the process definition:
The Process Definitions Details page will look like this, showing the Properties tab by default:
Clicking on the Preview tab displays a preview of the Process Definition diagram:
These components will all be part of the ProcessAppsModule.
We are going to need two page components, one for the Process Definition List and one for the Process Definition Details page component. We don't need a new module as these pages will be child pages of the Process Apps parent page.
As usual, we can easily create the needed components with the Angular CLI tool:
Martins-MacBook-Pro:adf-workbench-process20 mbergljung$ cd src/app/process-apps/
Martins-MacBook-Prorocess-apps mbergljung$ ng g component process-definitions-list-page
create src/app/process-apps/process-definitions-list-page/process-definitions-list-page.component.css (0 bytes)
create src/app/process-apps/process-definitions-list-page/process-definitions-list-page.component.html (48 bytes)
create src/app/process-apps/process-definitions-list-page/process-definitions-list-page.component.spec.ts (771 bytes)
create src/app/process-apps/process-definitions-list-page/process-definitions-list-page.component.ts (358 bytes)
update src/app/process-apps/process-apps.module.ts (1352 bytes)
Martins-MacBook-Prorocess-apps mbergljung$ ng g component process-definitions-details-page
create src/app/process-apps/process-definitions-details-page/process-definitions-details-page.component.css (0 bytes)
create src/app/process-apps/process-definitions-details-page/process-definitions-details-page.component.html (51 bytes)
create src/app/process-apps/process-definitions-details-page/process-definitions-details-page.component.spec.ts (792 bytes)
create src/app/process-apps/process-definitions-details-page/process-definitions-details-page.component.ts (370 bytes)
update src/app/process-apps/process-apps.module.ts (1528 bytes)
This creates a Process Definitions list page component where we will display the available process definitions for the selected Process App. It also creates a Process Definitions details page where we can display more detailed information for a selected process definition.
We need to define two new route configurations for the list page and details page. Let’s add it in the src/app/process-apps/process-apps-routing.module.ts file as follows:
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { ProcessAppsPageComponent } from './process-apps-page/process-apps-page.component';
import { ProcessAppsListPageComponent } from './process-apps-list-page/process-apps-list-page.component';
import { ProcessAppsDetailsPageComponent } from './process-apps-details-page/process-apps-details-page.component';
import { ProcessDefinitionsDetailsPageComponent } from './process-definitions-details-page/process-definitions-details-page.component';
import { ProcessDefinitionsListPageComponent } from './process-definitions-list-page/process-definitions-list-page.component';
import { AuthGuardBpm } from '@alfresco/adf-core';
const routes: Routes = [ {
path: 'process-apps',
component: ProcessAppsPageComponent,
canActivate: [AuthGuardBpm],
data: {
title: 'Process Apps',
icon: 'apps',
hidden: false,
needBpmAuth: true,
isLogin: false
},
children: [
{ path: '', component: ProcessAppsListPageComponent, canActivate: [AuthGuardBpm] },
{ path: ':process-app-id', component: ProcessAppsDetailsPageComponent, canActivate: [AuthGuardBpm] },
{ path: ':process-app-id/procdef-list', component: ProcessDefinitionsListPageComponent, canActivate: [AuthGuardBpm] },
{ path: ':process-app-id/procdef-details/:process-def-id', component: ProcessDefinitionsDetailsPageComponent, canActivate: [AuthGuardBpm] }
]
}];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class ProcessAppsRoutingModule { }
The route to get to the Process Definitions for a Process App is http://localhost:4200/process-apps/<process-app-id>/procdef-list. To get to the details page for a Process Definition we will use the http://localhost:4200/process-apps/<process-app-id>/procdef-details/<process-def-id> URL. You can try these URLs right now if you like by typing them in manually.
We configure the routes to these page components to be children of the process app parent component route. This means we can use the same router-outlet.
Now we can move on and implement the Process Definitions List component that will provide a list of the Process Definitions that the current app has access to.
There’s no ADF Process Definition List component, so we have to implement the process definition list view ourselves. We will do this with an ADF Data Table component, which means that we will get familiar with how to work with the ADF Data Table component, which is good.
Let’s start with the component template for the process definition list. Open up the src/app/process-apps/process-definitions-list-page/process-definitions-list-page.component.html file and replace what is there with this:
<adf-toolbar [color]="'accent'">
<adf-toolbar-title>
<span *ngIf="appName">{{ appName }} > Process Definitions</span>
</adf-toolbar-title>
<adf-toolbar-divider></adf-toolbar-divider>
<button mat-icon-button
class="adf-viewer-close-button"
matTooltip="Close and go back to process app list"
(click)="onGoBack($event)"
aria-label="Close">
<mat-icon>close</mat-icon>
</button>
</adf-toolbar>
<adf-datatable
[data]="data"
[loading]="isLoading"
[actions]="true"
[actionsPosition]="'right'"
[contextMenu]="true"
[showHeader]="true"
(showRowActionsMenu)="onShowThreeDotsMenu($event)"
(executeRowAction)="onExecuteThreeDotsMenuItem($event)"
(showRowContextMenu)="onShowDropDownMenu($event)"
(rowClick)="onRowClick($event)">
<data-columns>
<data-column title="Id" key="id" ></data-column>
<data-column title="Name" key="name" class="full-width name-column"></data-column>
<data-column title="Key" key="key" ></data-column>
<data-column title="Version" key="version"></data-column>
</data-columns>
<loading-content-template>
<!-- Show a spinner if loading is going on -->
<ng-template>
<mat-progress-spinner
class="adf-process-list-loading-margin"
[color]="'primary'"
[mode]="'indeterminate'">
</mat-progress-spinner>
</ng-template>
</loading-content-template>
<no-content-template>
<!-- Show a message if there are no process definitions associated with process app -->
<ng-template>
<div class="no-content-message">
{{ 'PPROCDEF_LIST.NONE' | translate }}
</div>
</ng-template>
</no-content-template>
</adf-datatable>
<adf-context-menu-holder></adf-context-menu-holder>
We can divide the template into 2 main sections. The top section contains the ADF toolbar that displays the title for the Process Definitions page. The toolbar also contains a close button that will take you back to the Process App Details page. The second section contains the <adf-datatable> that will display the process definitions.
The ADF Data Table can be configured with a number of properties and event handlers. They have the following meaning (default value inside parenthesis):
The next thing we need to configure in the ADF Data Table component is the <data-columns> that should be displayed, and link them to the data properties. Columns can be defined either declarative, as we do here, or programmatically from the component class. We link each column to the data via the key property, which need to match a property in the ObjectDataTableAdapter. We will load it with ProcessDefinitionRepresentation objects that looks like this:
export class ProcessDefinitionRepresentation {
id: string;
name: string;
description: string;
key: string;
category: string;
version: number;
deploymentId: string;
tenantId: string;
metaDataValues: any[];
hasStartForm: boolean;
So we can see that the data column keys we use match properties in the ProcessDefinitionRepresentation. After the data column definitions we got one template for what should be displayed while data table is being loaded and one template for what should be displayed when there is no data to display.
The last thing in the template is the <adf-context-menu-holder> element that is a holder for where the drop down context menu should be output/displayed.
Now let’s implement the process definition list component class. Open up the src/app/process-apps/process-definitions-list-page/process-definitions-list-page.component.ts file and update it as follows:
import { Component, OnInit } from '@angular/core';
import {ActivatedRoute, Router} from '@angular/router';
import {Subject} from 'rxjs/Subject';
import {
AppsProcessService, DataCellEvent, DataRowActionEvent, DataRowEvent, ObjectDataRow,
ObjectDataTableAdapter
} from '@alfresco/adf-core';
import { ProcessDefinitionRepresentation, ProcessService } from '@alfresco/adf-process-services';
@Component({
selector: 'app-process-definitions-list-page',
templateUrl: './process-definitions-list-page.component.html',
styleUrls: ['./process-definitions-list-page.component.scss']
})
export class ProcessDefinitionsListPageComponent implements OnInit {
appId: number;
appName: string;
data: ObjectDataTableAdapter;
isLoading = true;
detailsMenuActionId = 'details';
showDetailsMenuAction = {
title: 'Show Details',
subject: new Subject(),
// your custom metadata needed for onExecuteThreeDotsMenuItem
id: this.detailsMenuActionId
};
constructor(private router: Router,
private activatedRoute: ActivatedRoute,
private processAppService: AppsProcessService,
private processService: ProcessService) {
this.data = new ObjectDataTableAdapter();
}
ngOnInit() {
this.showDetailsMenuAction.subject.subscribe(item => this.onExecuteDropDownMenuItem(item));
this.appId = +this.activatedRoute.snapshot.params['process-app-id'];
console.log('Showing process definitions for app: ', this.appId);
this.processAppService.getApplicationDetailsById(this.appId).subscribe(
(appDef: any) => {
this.appName = appDef.name;
},
(error) => {
console.log('Error: ', error);
});
this.processService.getProcessDefinitions(this.appId).subscribe(
(procDefs: ProcessDefinitionRepresentation[]) => {
this.isLoading = true;
const procDefRows = this.createDataRows(procDefs);
this.data.setRows(procDefRows);
this.isLoading = false;
},
(error) => {
console.log('Error: ', error);
});
}
onShowThreeDotsMenu(event: DataCellEvent) {
event.value.actions = [
this.showDetailsMenuAction
];
}
onShowDropDownMenu(event: DataCellEvent) {
const args = event.value;
const procDefId = args.row.getValue('id');
event.value.actions = [
this.showDetailsMenuAction
];
event.value.actions =
event.value.actions.map(a => {
return {
title: a.title,
subject: a.subject,
id: a.id,
procDefId: procDefId, // Inject the context (i.e. Proc Def Id)
};
});
}
onExecuteThreeDotsMenuItem(event: DataRowActionEvent) {
const args = event.value;
const menuActionId = args.action.id;
const procDefId = args.row.getValue('id'); // Something like ReviewaFile:2:2506
console.log(`ThreeDots menu item invoked: action = ${menuActionId} (${args.action.title}) procDefId = ${procDefId}`);
this.navigate2ProcDetails(menuActionId, procDefId);
}
onExecuteDropDownMenuItem(menuItem) {
const menuActionId = menuItem.id;
const procDefId = menuItem.procDefId; // Something like ReviewaFile:2:2506
console.log(`DropDown menu item invoked: action = ${menuActionId} (${menuItem.title}) procDefId = ${procDefId}`);
this.navigate2ProcDetails(menuActionId, procDefId);
}
onRowClick(event: DataRowEvent) {
// Not currently doing anything when row is clicked
}
onGoBack($event: Event) {
this.navigateBack2AppList();
}
private navigateBack2AppList() {
this.router.navigate(['../'],
{
relativeTo: this.activatedRoute
});
}
private navigate2ProcDetails(menuActionId: string, procDefId: string) {
if (menuActionId === this.detailsMenuActionId) {
this.router.navigate(['../procdef-details', procDefId ],
{
relativeTo: this.activatedRoute
});
}
}
private createDataRows(procDefs: ProcessDefinitionRepresentation[]): ObjectDataRow[] {
const procDefRows: ObjectDataRow[] = [];
procDefs.forEach((procDef) => {
procDefRows.push(new ObjectDataRow(procDef));
});
return procDefRows;
}
}
The first thing we do in the class is to define the appId, appName, data, and isLoading properties that are used in the template definition. The Drop Down menu and the Three Dots menu should both have one menu item called “Details”. We define it with the showDetailsMenuAction object. If you don’t plan on having a drop down menu, then you don’t need the subject property.
The Process Details list page is accessed via a URL that looks like http://localhost:4200/process-apps/1/procdef-list, so at the beginning of the ngOnInit method we extract the process-app-id. We can then use the Process App ID to fetch the Process App details and all the associated Process Definitions. To fetch the Process Definitions we use a new service called ProcessService from the @alfresco/adf-process-services package.
Note here that the getProcessDefinitions function and the getApplicationDetailsById function wants the Process App id as a number. So we convert to number when extracting the parameter.
When the async getProcessDefinitions call returns we create the data rows based on the ProcessDefinitionRepresentation array. Each ObjectDataRow is basically a ProcessDefinitionRepresentation, so the column keys match.
The rest of the code has a lot to do with the Drop Down menu and the Three Dots menu. The onShowThreeDotsMenu and onShowDropDownMenu functions are used to configure what menu items to display for each menu. The onShowThreeDotsMenu just sets the actions (i.e. menu items) to what we configured in the beginning. When an item is clicked in the Three Dots menu, the context will be automatically available (i.e. what Process Definition that was clicked). However, with the Drop Down menu you don't get context when it is clicked, we need to inject it when creating the menu.
When the Details menu item is clicked in either menu we navigate to the Process Definitions Details page, which we will implement in the next section.
However, before we implement the details page we should fix the navigation to the Process Definition list from the Process App details page. Open up the src/app/process-apps/process-apps-details-page/process-apps-details-page.component.ts file and implement the onShowProcDefs event handler as follows:
onShowProcDefs($event: Event) {
console.log('Navigate to process definitions for app: ', this.appId);
this.router.navigate(['procdef-list'],
{
relativeTo: this.activatedRoute
});
}
Now we can move on and implement the Process Definition Detail component that will display both the properties for the process definition and a diagram of the process model. To implement the Process Definition details page start with the template. Open up the src/app/process-apps/process-definitions-details-page/process-definitions-details-page.component.html file and make it look like this:
<adf-toolbar [color]="'accent'" *ngIf="appDef && procDef">
<adf-toolbar-title>
<span>{{ appDef.name }} > {{ procDef.name }}</span>
</adf-toolbar-title>
<adf-toolbar-divider></adf-toolbar-divider>
<button mat-icon-button
class="adf-viewer-close-button"
matTooltip="Close and go back to Process Def list"
(click)="onGoBack($event)"
aria-label="Close">
<mat-icon>close</mat-icon>
</button>
</adf-toolbar>
<mat-tab-group *ngIf="appDef && procDef">
<mat-tab label="Properties">
<mat-card class="adf-card-container">
<mat-card-content>
<adf-card-view
[properties]="properties"
[editable]="false">
</adf-card-view>
</mat-card-content>
</mat-card>
</mat-tab>
<mat-tab label="Preview">
<adf-diagram
[processDefinitionId]="procDef.id">
</adf-diagram>
</mat-tab>
</mat-tab-group>
Here we use a layout that is very similar to the one we used for the Repository Details page in the Building a Content Management app article. We got the familiar ADF toolbar at the top and then we got a tabbed view that is implemented with Angular Material components (i.e. mat-). The first tab will contain the process definition details/properties and the second tab will contain a preview of the process model/diagram.
To display the process model diagram we use a new ADF component called <adf-diagram> that is part of the @alfresco/adf-insight package. This component takes only the Process Definition ID and then draws a diagram with the help of Raphael, a cross-browser vector graphics package.
The properties tab is implemented with the <adf-card-view, which we have seen before in both this article and in the Building a Content Management app article.
If you need a more sophisticated approach for your properties layout, then have a look at the ADF Form component. It is described in the Content Management App article.
The component backing class is implemented in the src/app/process-apps/process-definitions-details-page/process-definitions-details-page.component.ts file and looks like this:
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { AppsProcessService, CardViewItem, CardViewTextItemModel } from '@alfresco/adf-core';
import { ProcessDefinitionRepresentation, ProcessService } from '@alfresco/adf-process-services';
import {AppDefinitionRepresentation} from 'alfresco-js-api';
@Component({
selector: 'app-process-definitions-details-page',
templateUrl: './process-definitions-details-page.component.html',
styleUrls: ['./process-definitions-details-page.component.scss']
})
export class ProcessDefinitionsDetailsPageComponent implements OnInit {
appDef: AppDefinitionRepresentation;
procDef: ProcessDefinitionRepresentation;
properties: Array<CardViewItem>;
constructor(private router: Router,
private activatedRoute: ActivatedRoute,
private processAppService: AppsProcessService,
private processService: ProcessService) {
this.properties = new Array<CardViewItem>();
}
ngOnInit() {
const appId: number = +this.activatedRoute.snapshot.params['process-app-id'];
const procDefId: string = this.activatedRoute.snapshot.params['process-def-id'];
console.log('Showing process definition for app: ', appId, ' and process def ID ', procDefId);
this.processAppService.getApplicationDetailsById(appId).subscribe(
(appDef: any) => {
this.appDef = appDef;
},
(error) => {
console.log('Error: ', error);
});
this.processService.getProcessDefinitions(appId).subscribe(
(procDefs: ProcessDefinitionRepresentation[]) => {
for (const pd of procDefs) {
if (procDefId === pd.id) {
this.procDef = pd;
this.setupProps(this.procDef);
}
}
},
(error) => {
console.log('Error: ', error);
});
}
onGoBack($event: Event) {
this.navigateBack2AppList();
}
private navigateBack2AppList() {
this.router.navigate(['../../procdef-list'],
{
relativeTo: this.activatedRoute
});
}
private setupProps(procDef: ProcessDefinitionRepresentation) {
console.log('setupProps: ', procDef.id);
const idProp = new CardViewTextItemModel({label: 'Id:', value: procDef.id, key: 'procDefId'});
const nameProp = new CardViewTextItemModel({label: 'Name:', value: procDef.name, key: 'procDefName'});
const descProp = new CardViewTextItemModel({label: 'Description:', value: procDef.description, key: 'procDefDesc'});
const keyProp = new CardViewTextItemModel({label: 'Key:', value: procDef.key, key: 'procDefKey'});
const categoryProp = new CardViewTextItemModel({label: 'Category:', value: procDef.category, key: 'procDefCategory'});
const verProp = new CardViewTextItemModel({label: 'Version:', value: procDef.version, key: 'procDefVer'});
const deploymentIdProp = new CardViewTextItemModel({label: 'Deployment Id:', value: procDef.deploymentId, key: 'deploymentId'});
const tenantIdProp = new CardViewTextItemModel({label: 'Tenant Id:', value: procDef.tenantId, key: 'tenantId'});
const hasStartFormProp = new CardViewTextItemModel({label: 'Has start form:', value: procDef.hasStartForm, key: 'hasStartForm'});
this.properties.push(idProp);
this.properties.push(nameProp);
this.properties.push(descProp);
this.properties.push(keyProp);
this.properties.push(categoryProp);
this.properties.push(verProp);
this.properties.push(deploymentIdProp);
this.properties.push(tenantIdProp);
this.properties.push(hasStartFormProp);
if (procDef.metaDataValues) {
let metadataCount = 0;
for (const metaDataValue of procDef.metaDataValues) {
this.properties.push(new CardViewTextItemModel({
label: 'Metadata (' + metadataCount + '):',
value: metaDataValue,
key: 'metadata' + metadataCount }));
metadataCount++;
}
}
}
}
As usual the class starts by defining the properties appDef, procDef, and properties that are referred to in the template. The ngOnInit function is very similar to what we did for the Process Definition list component class. There’s no API available for fetching just one Process Definition, so we fetch all and pick the one that matches passed in process-def-id.
It is then just a matter of setting up the ADF Card View properties, which will match what we got in the ProcessDefinitionRepresentation object.
That’s pretty much it.
Now that we got a list of process definitions it is most likely that we want to start process instances based on them. The easiest way to do this is actually to use an ADF component called <adf-start-process. This component is part of the @alfresco/adf-process-services package.
The Start Process component takes a Process App ID as input, so it make sense to use it when we already got a Process App ID, which we do in for example the Process App List page or the Process App Details page. Let’s add a Start Process button to the toolbar in the Process App Details page, between the Show Process Definitions button and the Close button, it should look something like this:
This button will trigger a navigation to a new page that we will call Start Process, here is how it will look like:
This Start Process form is automatically generated when we use the <adf-start-process> component. The Select Process drop down will be pre populated with the Process Definitions that have been associated with the Process App:
Clicking one of the Process Definitions, such as Invoice Approval Process in this case, immediately shows the associated Start Form:
Here we fill in the specific fields for the Process Definition's start form. Then click the Start Process button at the bottom of the form. If the process instance is started successfully we will display a message at the bottom of the screen. If the process instance could not be started, then we display an error message at the bottom of the screen.
The Start Process page component will all be part of the ProcessAppsModule. As usual, we can easily create the needed component with the Angular CLI tool:
Martins-MacBook-Pro:adf-workbench-process20 mbergljung$ cd src/app/process-apps/
Martins-MacBook-Prorocess-apps mbergljung$ ng g component start-process-page
create src/app/process-apps/start-process-page/start-process-page.component.css (0 bytes)
create src/app/process-apps/start-process-page/start-process-page.component.html (37 bytes)
create src/app/process-apps/start-process-page/start-process-page.component.spec.ts (700 bytes)
create src/app/process-apps/start-process-page/start-process-page.component.ts (315 bytes)
update src/app/process-apps/process-apps.module.ts (1910 bytes)
This creates a Start Process page component where a list of process definitions will be displayed so the user can start a Process Instance from one of them.
We need to set up a new route configuration with a child route for the Start Process page. Let’s add it in the src/app/process-apps/process-apps-routing.module.ts file as follows:
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { ProcessAppsPageComponent } from './process-apps-page/process-apps-page.component';
import { ProcessAppsListPageComponent } from './process-apps-list-page/process-apps-list-page.component';
import { ProcessAppsDetailsPageComponent } from './process-apps-details-page/process-apps-details-page.component';
import { ProcessDefinitionsDetailsPageComponent } from './process-definitions-details-page/process-definitions-details-page.component';
import { ProcessDefinitionsListPageComponent } from './process-definitions-list-page/process-definitions-list-page.component';
import { StartProcessPageComponent } from './start-process-page/start-process-page.component';
import { AuthGuardBpm } from '@alfresco/adf-core';
const routes: Routes = [ {
path: 'process-apps',
component: ProcessAppsPageComponent,
canActivate: [AuthGuardBpm],
data: {
title: 'Process Apps',
icon: 'apps',
hidden: false,
needBpmAuth: true,
isLogin: false
},
children: [
{ path: '', component: ProcessAppsListPageComponent, canActivate: [AuthGuardBpm] },
{ path: ':process-app-id', component: ProcessAppsDetailsPageComponent, canActivate: [AuthGuardBpm] },
{ path: ':process-app-id/procdef-list', component: ProcessDefinitionsListPageComponent, canActivate: [AuthGuardBpm] },
{ path: ':process-app-id/procdef-details/:process-def-id', component: ProcessDefinitionsDetailsPageComponent, canActivate: [AuthGuardBpm] },
{ path: ':process-app-id/start-process', component: StartProcessPageComponent, canActivate: [AuthGuardBpm] }
]
}];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class ProcessAppsRoutingModule { }
We configure the route to the new page component to be a child of the Process App parent component route. This means we can use the same router-outlet as before.
The user will be able to navigate to the Start Process page from the Process App Details page. A new button should be added called Start Process. Open up the src/app/process-apps/process-apps-details-page/process-apps-details-page.component.html file and add it as follows:
<adf-toolbar [color]="'accent'" *ngIf="appDef">
<adf-toolbar-title>
<span>{{ appDef.name }}</span>
</adf-toolbar-title>
<button mat-icon-button
matTooltip="Show Process Definitions included in app"
(click)="onShowProcDefs($event)">
<mat-icon>device_hub</mat-icon>
</button>
<button mat-icon-button
matTooltip="Start Process based on Process Definitions for App"
(click)="onStartProcess($event)">
<mat-icon>launch</mat-icon>
</button>
<adf-toolbar-divider></adf-toolbar-divider>
<button mat-icon-button
class="adf-viewer-close-button"
matTooltip="Close and go back to Process App list"
(click)="onGoBack($event)"
aria-label="Close">
<mat-icon>close</mat-icon>
</button>
</adf-toolbar>
<mat-card class="adf-card-container" *ngIf="appDef">
<mat-card-content>
<adf-card-view
[properties]="properties"
[editable]="false">
</adf-card-view>
</mat-card-content>
</mat-card>
Implement the onStartProcess handler as follows in the src/app/process-apps/process-apps-details-page/process-apps-details-page.component.ts file:
onStartProcess($event: Event) {
console.log('Navigate to start process for app: ', this.appDef.id);
this.router.navigate(['start-process'],
{
relativeTo: this.activatedRoute
});
}
Now we can move on and implement the Start Process page component. Start with the template, open up the src/app/process-apps/start-process-page/start-process-page.component.html file and replace whatever is there with:
<adf-toolbar [color]="'accent'" *ngIf="appDef">
<adf-toolbar-title>
<span >{{ appDef.name }} > Start a process</span>
</adf-toolbar-title>
<adf-toolbar-divider></adf-toolbar-divider>
<button mat-icon-button
matTooltip="Close and go back to Process App list"
(click)="onGoBack($event)"
aria-label="Close">
<mat-icon>close</mat-icon>
</button>
</adf-toolbar>
<adf-start-process *ngIf="appDef"
[appId]="appDef.id"
(start)="onStartProcessInstance($event)"
(cancel)="onCancelProcessInstance()"
(error)="onStartError($event)">
</adf-start-process>
So we have the usual ADF toolbar at the top with a page title and Close button. Then under it we use the ADF Start Process component. It takes just the Process App ID and then it provides three event handlers for doing stuff depending on if it was successful to start the process or not.
The backing component class goes into the src/app/process-apps/start-process-page/start-process-page.component.ts file and looks like this:
import { Component, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { ProcessInstance, StartProcessInstanceComponent } from '@alfresco/adf-process-services';
import { AppsProcessService, NotificationService } from '@alfresco/adf-core';
import { AppDefinitionRepresentation } from 'alfresco-js-api';
@Component({
selector: 'app-start-process-page',
templateUrl: './start-process-page.component.html',
styleUrls: ['./start-process-page.component.scss']
})
export class StartProcessPageComponent implements OnInit {
appDef: AppDefinitionRepresentation;
@ViewChild(StartProcessInstanceComponent)
startProcessForm: StartProcessInstanceComponent;
constructor(private router: Router,
private activatedRoute: ActivatedRoute,
private processAppService: AppsProcessService,
private notificationService: NotificationService) { }
ngOnInit() {
const appId: number = +this.activatedRoute.snapshot.params['process-app-id'];
console.log('Start Process for app: ', appId);
this.processAppService.getApplicationDetailsById(appId).subscribe(
(appDef: AppDefinitionRepresentation) => {
this.appDef = appDef;
},
(error) => {
console.log('Error: ', error);
});
}
onGoBack($event: Event) {
this.navigateBack2AppList();
}
private navigateBack2AppList() {
this.router.navigate(['../'],
{
relativeTo: this.activatedRoute
});
}
onStartProcessInstance(procInstance: ProcessInstance) {
console.log('Started process instance: ', procInstance.id);
this.notificationService.openSnackMessage(
`Successfully started process instance ('${procInstance.id}') for Process App ${this.appDef.name}`,
4000);
this.startProcessForm.reset();
}
onCancelProcessInstance() {
console.log('Starting Process was cancelled.');
this.startProcessForm.reset();
}
onStartError(error: any) {
console.log('There was an error starting process: ', error);
this.notificationService.openSnackMessage(
`Failed to start process instance for Process App ${this.appDef.name} error = ${error}`,
4000);
}
}
The ngOnInit function is where we extract the Process App ID and then fetch the Process App data for it, mainly to get to the Process App name, so we can display it in toolbar. The event handler implementations are quite straightforward. Except that we need to reset the Start Form after we have started a new process instance, and also if the user cancels the form. We can get our hands on the start form by using the @ViewChild decorator.
When a Process Instance is started successfully a message will be displayed with the ADF Notification Service. There will also be a console log with the Process Instance ID.
Now try this out and start a few process instances so you have them available when continuing the tutorial, process instances and task instances are necessary for the rest of the article.
This part of the application is about interacting with ongoing tasks. This means looking at task instances for different process instances. There are a number of ADF components that can be used for this. What task instances that are displayed depend on what user that is logged in.
We will have a new menu item called My Tasks in the left navigation menu that will display a page looking something like this:
This page displays the Process Applications that the logged in user has access to:
The user then need to click on one of the Apps to see any tasks instances assigned to them:
By default all tasks that are assigned to the user will be displayed. If a task has a checklist (i.e sub-tasks), then those will be displayed without process instance ID as in above list.
If the process instances contains pooled tasks (i.e. candidate tasks that need to be claimed before worked on) then the user has to click on the Open (Pooled) radio button to see if any exist that the logged in user can claim.
If the user wants to see tasks that he or she has completed, then click on the Completed radio button. To manage a task the user clicks on the row, which will display a detail page with options to manage the task, or just view details.
We are going to need a My Tasks parent page component. It will contain two child page components, one for the Process Apps List and one for the Process Apps Details page component. Plus a module to keep everything organized.
As usual, we can easily create a new module and the needed components with the Angular CLI tool, standing in the adf-workbench-process20 directory do:
Martins-MacBook-Pro:adf-workbench-process20 mbergljung$ ng g module my-tasks --flat false --routing
create src/app/my-tasks/my-tasks-routing.module.ts (250 bytes)
create src/app/my-tasks/my-tasks.module.ts (284 bytes)
Martins-MacBook-Pro:adf-workbench-process mbergljung$ cd src/app/my-tasks/
Martins-MacBook-Pro:my-tasks mbergljung$ ng g component my-tasks-page
create src/app/my-tasks/my-tasks-page/my-tasks-page.component.css (0 bytes)
create src/app/my-tasks/my-tasks-page/my-tasks-page.component.html (32 bytes)
create src/app/my-tasks/my-tasks-page/my-tasks-page.component.spec.ts (665 bytes)
create src/app/my-tasks/my-tasks-page/my-tasks-page.component.ts (295 bytes)
update src/app/my-tasks/my-tasks.module.ts (384 bytes)
Martins-MacBook-Pro:my-tasks mbergljung$ ng g component my-tasks-list-page
create src/app/my-tasks/my-tasks-list-page/my-tasks-list-page.component.css (0 bytes)
create src/app/my-tasks/my-tasks-list-page/my-tasks-list-page.component.html (37 bytes)
create src/app/my-tasks/my-tasks-list-page/my-tasks-list-page.component.spec.ts (694 bytes)
create src/app/my-tasks/my-tasks-list-page/my-tasks-list-page.component.ts (314 bytes)
update src/app/my-tasks/my-tasks.module.ts (504 bytes)
Martins-MacBook-Pro:my-tasks mbergljung$ ng g component my-tasks-details-page
create src/app/my-tasks/my-tasks-details-page/my-tasks-details-page.component.css (0 bytes)
create src/app/my-tasks/my-tasks-details-page/my-tasks-details-page.component.html (40 bytes)
create src/app/my-tasks/my-tasks-details-page/my-tasks-details-page.component.spec.ts (715 bytes)
create src/app/my-tasks/my-tasks-details-page/my-tasks-details-page.component.ts (326 bytes)
update src/app/my-tasks/my-tasks.module.ts (636 bytes)
This creates a My Tasks module with routing, a My Tasks parent page, and a My Tasks list page component where we will display the available task instances for the user. It also creates a My Tasks details page where we can display more detailed information about a selected task, such as management view, properties, comments etc.
We need to set up a new route configuration with a parent route that has two child routes for the list page and details page. Let’s add it in the src/app/my-tasks/my-tasks-routing.module.ts file as follows:
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { MyTasksDetailsPageComponent } from './my-tasks-details-page/my-tasks-details-page.component';
import { MyTasksListPageComponent } from './my-tasks-list-page/my-tasks-list-page.component';
import { MyTasksPageComponent } from './my-tasks-page/my-tasks-page.component';
import { AuthGuardBpm } from '@alfresco/adf-core';
const routes: Routes = [ {
path: 'my-tasks',
component: MyTasksPageComponent,
canActivate: [AuthGuardBpm],
data: {
title: 'My Tasks',
icon: 'assignment',
hidden: false,
needBpmAuth: true,
isLogin: false
},
children: [
{ path: '', component: MyTasksListPageComponent, canActivate: [AuthGuardBpm] },
{ path: ':task-id', component: MyTasksDetailsPageComponent, canActivate: [AuthGuardBpm] }
]
}];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class MyTasksRoutingModule { }
The parent page route is http://localhost:4200/my-tasks, and if we don’t specify any other URL path elements, then the My Tasks List child page will be displayed automatically as it has path ''. The Task Details page is accessed via the http://localhost:4200/my-tasks/<task-id> URL.
We also need to make the new pages and routes known to the main app. Open up the src/app/app.module.ts file and add as follows:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { AppCommonModule } from './app-common/app-common.module';
import { AppLoginRoutingModule } from './app-login/app-login-routing.module';
import { AppLoginModule } from './app-login/app-login.module';
import { AppMenuService } from './app-menu/app-menu.service';
import { ProcessAppsModule } from './process-apps/process-apps.module';
import { ProcessAppsRoutingModule } from './process-apps/process-apps-routing.module';
import { MyTasksModule } from './my-tasks/my-tasks.module';
import { MyTasksRoutingModule } from './my-tasks/my-tasks-routing.module';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
AppRoutingModule,
AppCommonModule,
AppLoginModule,
AppLoginRoutingModule,
ProcessAppsModule,
ProcessAppsRoutingModule,
MyTasksModule,
MyTasksRoutingModule
],
providers: [AppMenuService],
bootstrap: [AppComponent]
})
export class AppModule { }
This section makes heavy use of the Activiti Task List module and Angular Material components. These components are not yet known to the My Tasks module. Import as follows in the src/app/my-tasks/my-tasks.module.ts file:
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MyTasksRoutingModule } from './my-tasks-routing.module';
import { MyTasksPageComponent } from './my-tasks-page/my-tasks-page.component';
import { MyTasksListPageComponent } from './my-tasks-list-page/my-tasks-list-page.component';
import { MyTasksDetailsPageComponent } from './my-tasks-details-page/my-tasks-details-page.component';
import { AppCommonModule } from '../app-common/app-common.module';
@NgModule({
imports: [
CommonModule,
MyTasksRoutingModule,
/* Common App imports (Angular Core and Material, ADF Core, Content, and Process */
AppCommonModule
],
declarations: [MyTasksPageComponent, MyTasksListPageComponent, MyTasksDetailsPageComponent]
})
export class MyTasksModule { }
Here we import the AppCommonModule, which gives us access to the Process App component and many task list and task details components. This also gives us access to the ADF Core module and the Angular Material components.
The output from the My Tasks list and details page need to go somewhere. Update the main My Tasks page template in the src/app/my-tasks/my-tasks-page/my-tasks-page.component.html file as follows, replace whatever markup is already there:
<router-outlet></router-outlet>
This finishes off the setup of the my tasks module.
Now we can move on and implement the My Tasks List component that initially provides a list of the applications that the logged in user has access to. The user can then click on one of the Apps and the related assigned task instances will be displayed in a list. The user will also have the option to display pooled and completed tasks via radio buttons.
To do this we are going to use a new ADF component called <adf-tasklist that is part of the @alfresco/adf-process-services package. This package is already installed so we are ready to start implementing the component.
Let’s start with the page template for the Task List. Open up the src/app/my-tasks/my-tasks-list-page/my-tasks-list-page.component.html file and replace whatever is there with the following:
<adf-toolbar [color]="'accent'">
<adf-toolbar-title>
<span *ngIf="appDef">{{ appDef.name }} > My Tasks </span>
<span *ngIf="!appDef">==>> Select an App to see tasks </span>
</adf-toolbar-title>
</adf-toolbar>
<adf-apps
[layoutType]="'LIST'"
[filtersAppId]="processAppsFilter"
(appClick)="onAppClick($event)">
</adf-apps>
<mat-divider></mat-divider>
<div class="margin10" *ngIf="appDef">
Show tasks that are:
<mat-radio-group (change)="onSelectTaskType($event.value)" class="margin10">
<mat-radio-button value="openAssigned" checked="true">Open (Assigned)</mat-radio-button>
<mat-radio-button value="openPooled">Open (Pooled)</mat-radio-button>
<mat-radio-button value="completed">Completed</mat-radio-button>
</mat-radio-group>
</div>
<mat-tab-group>
<mat-tab [label]="taskTypeName">
<adf-tasklist *ngIf="appDef"
[appId]="appDef.id"
[state]="taskState"
[assignment]="taskAssignment"
(rowClick)="onTaskClick($event)">
<data-columns>
<data-column key="processInstanceId" title="Process Id"></data-column>
<data-column key="id" title="Task Id"></data-column>
<data-column key="name" title="Name" class="full-width name-column"></data-column>
<data-column key="created" title="Created" type="date"></data-column>
</data-columns>
</adf-tasklist>
</mat-tab>
</mat-tab-group>
We are using familiar ADF components for the toolbar to display the page title. Then, to display the Process Apps, we use the <adf-apps component that we got familiar with in the beginning of this article. After that we got the three radio buttons that allow us to switch between Assigned, Pooled, and Completed tasks for the currently logged in user.
As you can see, we use Angular Material components (i.e. mat-) to display the radio button group. These Angular Material components have already been imported and provided via our AppCommonModule that we created in the beginning.
Now to the new ADF component <adf-tasklist, it takes the following properties and event handlers:
There are many more properties and events listed here than we use. So it is possible to customise the Task List quite a bit. As with all the lists that are based on the ADF Data table it is possible to customise what columns to display. Here we are doing that to be able to show process id, task id, and created date. The <data-column key need to match a value in the TaskDetailsModel:
export class TaskDetailsModel {
id: string;
name: string;
assignee: LightUserRepresentation;
priority: number;
adhocTaskCanBeReassigned: number;
category: string;
created: string;
description: string;
dueDate: string;
duration: string;
endDate: string;
executionId: string;
formKey: string;
initiatorCanCompleteTask: boolean;
managerOfCandidateGroup: boolean;
memberOfCandidateGroup: boolean;
memberOfCandidateUsers: boolean;
involvedPeople: LightUserRepresentation [];
parentTaskId: string;
parentTaskName: string;
processDefinitionCategory: string;
processDefinitionDeploymentId: string;
processDefinitionDescription: string;
processDefinitionId: string;
processDefinitionKey: string;
processDefinitionName: string;
processDefinitionVersion: number = 0;
processInstanceId: string;
processInstanceName: string;
processInstanceStartUserId: string;
taskDefinitionKey: string;
The last configuration for the task list is an event to capture when a task in the list is clicked. The onTaskClick method is then called so we can navigate to the Task Details page.
The template uses a CSS class called margin10, which we need to add. Let's add it to the global app stylesheet in the src/styles.scss file so we can use it in any component template:
.margin10 {
margin: 10px;
}
The template is now finished so we can move on and implement the backing component class. Open up the src/app/my-tasks/my-tasks-list-page/my-tasks-list-page.component.ts file and update it as follows:
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { AppDefinitionRepresentationModel } from '@alfresco/adf-process-services';
@Component({
selector: 'app-my-tasks-list-page',
templateUrl: './my-tasks-list-page.component.html',
styleUrls: ['./my-tasks-list-page.component.scss']
})
export class MyTasksListPageComponent implements OnInit {
appDef: AppDefinitionRepresentationModel;
taskTypeName: string;
taskState: string;
taskAssignment: string;
/* Create a filter that will exclude the Task List App and include only new custom apps */
processAppsFilter = [ { tenantId: 1 } ];
constructor(private router: Router) {
this.taskTypeName = 'Open (Assigned)';
this.taskState = 'active';
this.taskAssignment = 'assignee';
}
ngOnInit() {
}
onSelectTaskType(displayTaskType: string) {
console.log('Changed task type to show : ', displayTaskType);
if (displayTaskType === 'openAssigned') {
this.taskTypeName = 'Open (Assigned)';
this.taskState = 'active';
this.taskAssignment = 'assignee';
} else if (displayTaskType === 'openPooled') {
this.taskTypeName = 'Open (Pooled)';
this.taskState = 'active';
this.taskAssignment = 'candidate';
} else if (displayTaskType === 'completed') {
this.taskTypeName = 'Completed';
this.taskState = 'completed';
this.taskAssignment = '';
}
}
onAppClick(appDef: AppDefinitionRepresentationModel) {
console.log('Task state: ' , this.taskState, ' assignment : ', this.taskAssignment, ' Selected App : ', appDef);
if (appDef) {
this.appDef = appDef;
}
}
onTaskClick(id: string) {
console.log('Navigating to Task Details : ', id);
this.router.navigate(['/my-tasks', id]);
}
}
As usual, we start of by defining the appDef, taskTypeName, taskState, and taskAssignment template variables. In the constructor we set up what type of tasks we want to display by default, in this case tasks assigned to current user. The onSelectTaskType function is triggered whenever we click one of the radio buttons. We then set what type of tasks to display. The underlying task list is automatically updated as these are @Input variables.
No tasks are displayed unless a Process App is selected. The radio buttons are not visible either. When an App is selected the onAppClick function is called from which we set the internal app ID and name.
When a task row is clicked the onTaskClick function is called, which takes the user to the Task Details page.
This finishes off the task list page. Let’s move on with the task details page.
When the user clicks on one of the Task Instance rows in the Task List a details page such as the following should be displayed:
The page will have a blue toolbar with a title and a close button. Under the toolbar will be a tabbed view of the Task Details. The first tab, which is called Manage, will display the Task Instance form from which the user can interact with the task and complete it. In this case the Invoice has to be approved or rejected via the APPROVE or REJECT buttons at the bottom of the form (i.e. the outcomes).
Let’s start with the details page template, open up the src/app/my-tasks/my-tasks-details-page/my-tasks-details-page.component.html file and update it so it looks like this:
<div class="task-details-view">
<adf-toolbar [color]="'accent'">
<adf-toolbar-title>
<span *ngIf="taskDetails">Task Details > {{ taskDetails.name }} ({{ taskDetails.id }})</span>
</adf-toolbar-title>
<adf-toolbar-divider></adf-toolbar-divider>
<button mat-icon-button
matTooltip="Close and go back to task list"
(click)="onGoBack($event)"
aria-label="Close">
<mat-icon>close</mat-icon>
</button>
</adf-toolbar>
<mat-tab-group *ngIf="taskDetails">
<mat-tab label="Manage">
<adf-task-details
[taskId]="taskDetails.id"
[showHeader]="false"
[showHeaderContent]="false"
[showInvolvePeople]="false"
[showComments]="false"
[showChecklist]="false"
[showFormTitle]="true"
[showFormCompleteButton]="true"
[showFormSaveButton]="true"
[showFormRefreshButton]="false">
</adf-task-details>
</mat-tab>
<mat-tab label="Properties">
<adf-task-header
[taskDetails]="taskDetails">
</adf-task-header>
</mat-tab>
<mat-tab label="Attachments">
<adf-task-attachment-list
[taskId]="taskDetails.id"
(attachmentClick)="onAttachmentClick()">
</adf-task-attachment-list>
</mat-tab>
<mat-tab label="Comments">
<adf-comments
[taskId]="taskDetails.id"
[readOnly]="false">
</adf-comments>
</mat-tab>
<mat-tab label="Checklist (Sub-tasks)">
<adf-checklist *ngIf="taskDetails.assignee"
[readOnly]="false"
[taskId]="taskDetails.id"
[assignee]="taskDetails.assignee.id">
</adf-checklist>
</mat-tab>
</mat-tab-group>
</div>
This page has the usual ADF Toolbar at the top with task title and identifier. Under the toolbar is an Angular Material tabbed view. Each tab uses an ADF component to display different types of details for the task.
The first tab, called Manage, presents a task view where you can interact with the task instance. This means that you can complete the task from and advance the associated process instance accordingly. To do this we use the ADF component called <adf-task-details. This component is very powerful, you could be fine with just using it and no other components. You can see that I have turned off a lot of features by setting a number of show* variables to false. You feed this component with the task id. I have also enabled the Complete and Save buttons so the user can interact with the task. Enabling the Complete button means that you will see all configured outcomes as buttons, such as the APPROVE and REJECT buttons in the screenshot earlier on.
Note that if the task is a pooled task (candidate task), which means you need to claim it before you can interact with it, then a message will tell you to claim it before you can manage it. You can claim it under the Properties tab, which is next.
Note. It would be possible to also use <adf-form and give it task id. That is actually what is underneath an <adf-task-details component. However, it would not take care of tasks without a form, such as checklist sub-tasks and others. Using <adf-task-details takes care of it and just displays a Complete button if there is no associated form for the task instance.
The second tab is called Properties and displays a lot of the details for the task. It is implemented with the <adf-task-header component, which sounds like it would just display a row with for example the task title. But it actually displays a list of relevant task properties. You feed it with an object of type TaskDetailsModel:
The bottom of the Properties tab view could have a Requeue button present if it is a claimed pooled task. You can use it to put back the task in the pool so somebody else can claim it. If the pooled task is not yet claimed, then there will be a Claim button.
The Claim and Requeue buttons does not seem to have any effect....
The next tab, which is called Attachments, displays any uploaded files that are attached to the task instance. We do this with the <adf-task-attachment-list component. You feed this component with the task instance id:
If a task should offer the user the opportunity to upload more files, then you could also use the <adf-create-task-attachment component or configure drag-and-drop via the <adf-upload-drag-area component.
The following tab, which is called Comments, will display any comments made by users regarding this task instance. We do this with the <adf-comments component. You feed this component with the task instance id. If you want the user to be able to add more comments, then set the readOnly property to false:
The last tab is called Checklist and will display any sub-tasks created for this task instance. Sometimes it is useful to divide a task into multiple steps and make sure that they have been completed. We do this with the <adf-checklist component. You feed this component with the task instance id. It also need to be fed with the assignee that we should display sub-tasks for, which would be the current user:
The backing component class is implemented in the src/app/my-tasks/my-tasks-details-page/my-tasks-details-page.component.ts file and looks like this:
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { TaskDetailsModel, TaskListService } from '@alfresco/adf-process-services';
@Component({
selector: 'app-my-tasks-details-page',
templateUrl: './my-tasks-details-page.component.html',
styleUrls: ['./my-tasks-details-page.component.scss']
})
export class MyTasksDetailsPageComponent implements OnInit {
taskDetails: TaskDetailsModel;
constructor(private router: Router,
private activatedRoute: ActivatedRoute,
private taskListService: TaskListService) { }
ngOnInit() {
const taskId = this.activatedRoute.snapshot.params['task-id'];
console.log('Showing task details for : ', taskId);
this.taskListService.getTaskDetails(taskId).subscribe(
(taskDetails: TaskDetailsModel) => {
this.taskDetails = taskDetails;
},
(error) => {
console.log('Error: ', error);
});
}
onGoBack($event: Event) {
this.navigateBack2AppList();
}
private navigateBack2AppList() {
this.router.navigate(['../'],
{
relativeTo: this.activatedRoute
});
}
onAttachmentClick() {
}
}
The component class is very straightforward, it basically just fetches the Task Details for passed in ID. The task-id is passed in via the URL, which looks like http://localhost:4200/my-tasks/<task-id>. We extract the task-id in the ngOnInit method via the activedRoute object.
When we have the ID we use the ADF TaskListService to fetch a TaskDetailsModel object representing the details about the task.
We implement the navigate back handler as previously done by just stepping up to the parent route. We don’t need to pass anything back to the task list as it always justy displays all apps and then the user selects an app again.
That should do it, you can now try out the task list and details pages.
This part of the application is about interacting with ongoing Process Instances. There are a number of ADF components that can be used for this. What process instances that are displayed depend on what user that is logged in.
We will have new menu item called My Processes in the left navigation menu that will display a page looking something like this:
This page displays the Process Applications that the logged in user has access to. The user then need to click on one of the Apps to see the Process Instances related to that app. By default running process instances will be displayed. Clicking on the Completed radio button switches to display completed process instances that are no longer running.
We are going to need a My Processes parent page component. It will contain two child page components, one for the Process Instance List and one for the Process Instance Details page component. Plus a module to keep everything organised.
As usual, we can easily create a new module and the needed components with the Angular CLI tool, standing in the
adf-workbench-process directory do:
Martins-MacBook-Pro:adf-workbench-process20 mbergljung$ ng g module my-processes --flat false --routing
create src/app/my-processes/my-processes-routing.module.ts (254 bytes)
create src/app/my-processes/my-processes.module.ts (300 bytes)
Martins-MacBook-Pro:adf-workbench-process20 mbergljung$ cd src/app/my-processes/
Martins-MacBook-Pro:my-processes mbergljung$ ng g component my-processes-page
create src/app/my-processes/my-processes-page/my-processes-page.component.css (0 bytes)
create src/app/my-processes/my-processes-page/my-processes-page.component.html (36 bytes)
create src/app/my-processes/my-processes-page/my-processes-page.component.spec.ts (693 bytes)
create src/app/my-processes/my-processes-page/my-processes-page.component.ts (311 bytes)
update src/app/my-processes/my-processes.module.ts (416 bytes)
Martins-MacBook-Pro:my-processes mbergljung$ ng g component my-processes-list-page
create src/app/my-processes/my-processes-list-page/my-processes-list-page.component.css (0 bytes)
create src/app/my-processes/my-processes-list-page/my-processes-list-page.component.html (41 bytes)
create src/app/my-processes/my-processes-list-page/my-processes-list-page.component.spec.ts (722 bytes)
create src/app/my-processes/my-processes-list-page/my-processes-list-page.component.ts (330 bytes)
update src/app/my-processes/my-processes.module.ts (552 bytes)
Martins-MacBook-Pro:my-processes mbergljung$ ng g component my-processes-details-page
create src/app/my-processes/my-processes-details-page/my-processes-details-page.component.css (0 bytes)
create src/app/my-processes/my-processes-details-page/my-processes-details-page.component.html (44 bytes)
create src/app/my-processes/my-processes-details-page/my-processes-details-page.component.spec.ts (743 bytes)
create src/app/my-processes/my-processes-details-page/my-processes-details-page.component.ts (342 bytes)
update src/app/my-processes/my-processes.module.ts (700 bytes)
This creates a My Processes module with routing, a My Processes parent page, and a My Processes list page component where we will display the available process instances for the user. It also creates a My Processes details page where we can display more detailed information about a selected process, such as properties, comments, and diagram.
We need to set up a new route configuration with a parent route that has two child routes for the list page and details page. Let’s add it in the src/app/my-processes/my-processes-routing.module.ts file as follows:
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { MyProcessesDetailsPageComponent } from './my-processes-details-page/my-processes-details-page.component';
import { MyProcessesListPageComponent } from './my-processes-list-page/my-processes-list-page.component';
import { MyProcessesPageComponent } from './my-processes-page/my-processes-page.component';
import { AuthGuardBpm } from '@alfresco/adf-core';
const routes: Routes = [{
path: 'my-processes',
component: MyProcessesPageComponent,
canActivate: [AuthGuardBpm],
data: {
title: 'My Processes',
icon: 'settings',
hidden: false,
needBpmAuth: true,
isLogin: false
},
children: [
{ path: '', component: MyProcessesListPageComponent, canActivate: [AuthGuardBpm] },
{ path: ':process-id', component: MyProcessesDetailsPageComponent, canActivate: [AuthGuardBpm] }
]
}];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class MyProcessesRoutingModule { }
The parent page route is http://localhost:4200/my-processes, and if we don’t specify any other URL path elements, then the the My Processes List child page will be displayed automatically as it has path ''.
We also need to make the new pages and routes known to the main app. Open up the src/app/app.module.ts file and add as follows:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { AppCommonModule } from './app-common/app-common.module';
import { AppLoginRoutingModule } from './app-login/app-login-routing.module';
import { AppLoginModule } from './app-login/app-login.module';
import { AppMenuService } from './app-menu/app-menu.service';
import { ProcessAppsModule } from './process-apps/process-apps.module';
import { ProcessAppsRoutingModule } from './process-apps/process-apps-routing.module';
import { MyTasksModule } from './my-tasks/my-tasks.module';
import { MyTasksRoutingModule } from './my-tasks/my-tasks-routing.module';
import { MyProcessesModule } from './my-processes/my-processes.module';
import { MyProcessesRoutingModule } from './my-processes/my-processes-routing.module';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
AppRoutingModule,
AppCommonModule,
AppLoginModule,
AppLoginRoutingModule,
ProcessAppsModule,
ProcessAppsRoutingModule,
MyTasksModule,
MyTasksRoutingModule,
MyProcessesModule,
MyProcessesRoutingModule
],
providers: [AppMenuService],
bootstrap: [AppComponent]
})
export class AppModule { }
This section makes heavy use of the ADF components and the Angular Material components. These components are not yet known to the my processes module. Import as follows in the src/app/my-processes/my-processes.module.ts file:
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MyProcessesRoutingModule } from './my-processes-routing.module';
import { MyProcessesPageComponent } from './my-processes-page/my-processes-page.component';
import { MyProcessesListPageComponent } from './my-processes-list-page/my-processes-list-page.component';
import { MyProcessesDetailsPageComponent } from './my-processes-details-page/my-processes-details-page.component';
import { AppCommonModule } from '../app-common/app-common.module';
@NgModule({
imports: [
CommonModule,
MyProcessesRoutingModule,
/* Common App imports (Angular Core and Material, ADF Core, Content, and Process */
AppCommonModule
],
declarations: [MyProcessesPageComponent, MyProcessesListPageComponent, MyProcessesDetailsPageComponent]
})
export class MyProcessesModule { }
Here we import the AppCommonModule that we implemented earlier on. It gives us access to the ADF Core module, the ADF Content module, and the ADF Process module, plus the Angular Material components.
The output from the my processes list and details pages need to go somewhere. Update the main my processes page template in the src/app/my-processes/my-processes-page/my-processes-page.component.html file as follows, replace whatever markup is already there:
<router-outlet></router-outlet>
This finishes off the setup of the my processes module.
Now we can move on and implement the My Processes List component that will provide a list of the active process instances that the logged in user has access to. The user will then be able to click on one of the processes and the related details page will be displayed. It will also be possible to switch between running and completed process instances. To do this we are going to use a new ADF component called <adf-process-instance-list.
Let’s start with the page template for the process list. Open up the src/app/my-processes/my-processes-list-page/my-processes-list-page.component.html file and replace whatever is there with the following:
<adf-toolbar [color]="'accent'">
<adf-toolbar-title>
<span *ngIf="appDef">My Processes > {{ appDef.name }}</span>
<span *ngIf="!appDef">==>> Select an App to see processes</span>
</adf-toolbar-title>
</adf-toolbar>
<adf-apps
[layoutType]="'LIST'"
[filtersAppId]="processAppsFilter"
(appClick)="onAppClick($event)">
</adf-apps>
<mat-divider></mat-divider>
<div class="margin10" *ngIf="appDef">
Show processes that are:
<mat-radio-group (change)="onSelectProcessType($event.value)" class="margin10">
<mat-radio-button value="running" checked="true">Running</mat-radio-button>
<mat-radio-button value="completed">Completed</mat-radio-button>
</mat-radio-group>
</div>
<mat-tab-group>
<mat-tab [label]="processTypeName">
<adf-process-instance-list *ngIf="appDef"
[appId]="appDef.id"
[state]="processState"
(rowClick)="onProcessClick($event)">
<data-columns>
<data-column key="id" title="Id"></data-column>
<data-column key="name" title="Name" class="full-width name-column"></data-column>
<data-column key="started" title="Started" type="date"></data-column>
</data-columns>
</adf-process-instance-list>
</mat-tab>
</mat-tab-group>
We are using familiar ADF components for the toolbar to display the page title. Then, to display the Process Apps, we use the <adf-apps component that we got familiar with in the beginning of this article. After that we got the two radio buttons that allow us to switch between Running and Completed processes for the currently logged in user.
As you can see, we use Angular Material components (i.e. md-) to display the radio button group. These Angular Material components have already been imported and provided via our App Common Module that we created in the beginning.
Now to the new ADF component <adf-process-instance-list, it takes the following properties and event handlers:
Here we describe all the properties and events that can be used with the Process Instance List component. As with all the lists that are based on the ADF Data table it is possible to customise what columns to display. Here we are doing that to be able to show process id, name, and started date. The <data-column key need to match a value in the ProcessInstance class:
export class ProcessInstance {
public businessKey: string;
public ended: any;
public graphicalNotationDefined: boolean;
public id: string;
public name: string;
public processDefinitionCategory: string;
public processDefinitionDeploymentId: string;
public processDefinitionDescription: string;
public processDefinitionId: string;
public processDefinitionKey: string;
public processDefinitionName: string;
public processDefinitionVersion: number;
public startFormDefined: boolean;
public started: string;
public startedBy: any;
public suspended: boolean;
public tenantId: string;
public variables: any;
The last configuration for the Process List is an event to capture when a process in the list is clicked. The onProcessClick function is then called so we can navigate to the Process Details page.
Note that the radio buttons and the Process Instance list is only displayed when the user selects a Process App.
The template is now finished so we can move on and implement the backing component class. Open up the src/app/my-processes/my-processes-list-page/my-processes-list-page.component.ts file and update it as follows:
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { AppDefinitionRepresentationModel } from '@alfresco/adf-process-services';
@Component({
selector: 'app-my-processes-list-page',
templateUrl: './my-processes-list-page.component.html',
styleUrls: ['./my-processes-list-page.component.scss']
})
export class MyProcessesListPageComponent implements OnInit {
appDef: AppDefinitionRepresentationModel;
processTypeName: string;
processState: string;
/* Create a filter that will exclude the Task List App and include only new custom apps */
processAppsFilter = [ { tenantId: 1 } ];
constructor(private router: Router) {
this.processTypeName = 'Running';
this.processState = 'running';
}
ngOnInit() {
}
onSelectProcessType(displayProcessType: string) {
console.log('Changed process type to show : ', displayProcessType);
if (displayProcessType === 'running') {
this.processTypeName = 'Running';
this.processState = 'running';
} else if (displayProcessType === 'completed') {
this.processTypeName = 'Completed';
this.processState = 'completed';
}
}
onAppClick(appDef: AppDefinitionRepresentationModel) {
console.log('Process state: ' , this.processState, ' Selected App : ', appDef);
if (appDef) {
this.appDef = appDef;
}
}
onProcessClick(id: string) {
console.log('Navigating to Process Instance Details : ', id);
this.router.navigate(['/my-processes', id]);
}
}
As usual, we start of by defining the appId, appName, processTypeName, and processState template variables. In the constructor we set up what type of processes we want to display by default, in this case running processes.
The onSelectProcessType function is triggered whenever we click on one of the radio buttons. We then set what type of processes to display. The underlying Process List is automatically updated as these are @Input variables.
No processes are displayed unless a Process App is selected. The radio buttons are not visible either. When an App is selected the onAppClick function is called from which we set the internal app ID and name.
When a process row is clicked the onProcessClick function is called, which will take the user to the process details page.
This finishes off the process list page. Let’s move on with the process details page.
When the user clicks one of the processes a details page such as the following should be displayed:
The page will have a blue toolbar with a title and a close button. Under the toolbar will be a tabbed view of the process details.
Let’s start with the details page template, open up the src/app/my-processes/my-processes-details-page/my-processes-details-page.component.html file and update it so it looks like this:
<div class="process-details-view">
<adf-toolbar [color]="'accent'">
<adf-toolbar-title>
<span *ngIf="processDetails">Process Details > '{{ processDetails.name }}' ({{ processDetails.id }})</span>
</adf-toolbar-title>
<adf-toolbar-divider></adf-toolbar-divider>
<button mat-icon-button
matTooltip="Close and go back to task list"
(click)="onGoBack($event)"
aria-label="Close">
<mat-icon>close</mat-icon>
</button>
</adf-toolbar>
<mat-tab-group *ngIf="processDetails">
<mat-tab label="Manage">
<mat-card>
<span> {{ getProcessNameOrDescription('medium') }}</span>
<div *ngIf="isRunning()">
<button mat-button type="button" (click)="cancelProcess()">{{ 'DETAILS.BUTTON.CANCEL' | translate }}</button>
</div>
</mat-card>
</mat-tab>
<mat-tab label="Properties">
<adf-process-instance-header
[processInstance]="processDetails">
</adf-process-instance-header>
</mat-tab>
<mat-tab label="Attachments">
<adf-process-attachment-list
[processInstanceId]="processDetails.id"
(attachmentClick)="onAttachmentClick()">
</adf-process-attachment-list>
</mat-tab>
<mat-tab label="Comments">
<adf-process-instance-comments
[processInstanceId]="processDetails.id"
[readOnly]="false">
</adf-process-instance-comments>
</mat-tab>
<mat-tab label="Tasks">
<adf-process-instance-tasks
[processInstanceDetails]="processDetails"
[showRefreshButton]="true">
</adf-process-instance-tasks>
</mat-tab>
<mat-tab label="Preview">
<adf-diagram
[processInstanceId]="processDetails.id">
</adf-diagram>
</mat-tab>
</mat-tab-group>
</div>
This page has the usual ADF Toolbar at the top with process title and identifier. Under the toolbar is an Angular Material tabbed view. Each tab uses an ADF component to display different types of details for the process instance.
The first tab, called Manage, presents a process view where you can interact with the Process Instance:
In this case you can only cancel the process, if it is running. This does not use an ADF component as there is none that can be used to just cancel a process. However, there is a powerful component called <adf-process-instance-details that we don’t use in this application, it could potentially replace all the tabs in this page, and it includes a cancel button. But here we look at each individual feature separately.
The second tab is called Properties and displays a lot of the details for the process:
It is implemented with the <adf-process-instance-header component, which sounds like it would just display a row with for example the process title. But it actually displays a list of relevant process properties. You feed it with an object of type ProcessInstance.
The next tab, which is called Attachments, displays any uploaded files that are attached to the process instance, including also files attached to task instances:
This is done with the <adf-process-attachment-list component. You feed this component with the process instance id. If a process should offer the user the opportunity to upload more files, then you could also use the <adf-create-process-attachment component or configure drag-and-drop via the <adf-upload-drag-area component.
The following tab, which is called Comments, will display any comments made by users regarding this process instance and any associated task instances:
We do this with the <adf-process-instance-comments component. You feed this component with the process instance id. If you want the user to be able to add more comments, then set the readOnly property to false.
Next tab, which is called Tasks, will display any associated task instances:
This is done with the <adf-process-instance-tasks component. You feed this component with the process instance.
The last tab is called Preview and will show the diagram of a running Process Instance with the activities highlighted according to their state (Active/Completed/Pending):
This is implemented with the <adf-diagram component. You feed this component with the process instance id.
There is a bug in the diagram component so the activity names are not displayed.
The backing component class is implemented in the src/app/my-processes/my-processes-details-page/my-processes-details-page.component.ts file and looks like this:
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { DatePipe } from '@angular/common';
import { ProcessInstance, ProcessService } from '@alfresco/adf-process-services';
@Component({
selector: 'app-my-processes-details-page',
templateUrl: './my-processes-details-page.component.html',
styleUrls: ['./my-processes-details-page.component.scss']
})
export class MyProcessesDetailsPageComponent implements OnInit {
processDetails: ProcessInstance;
constructor(private router: Router,
private activatedRoute: ActivatedRoute,
private processService: ProcessService) {
}
ngOnInit() {
const processId = this.activatedRoute.snapshot.params['process-id'];
console.log('Showing process details for : ', processId);
this.processService.getProcess(processId).subscribe(
(processDetails: ProcessInstance) => {
this.processDetails = processDetails;
},
(error) => {
console.log('Error: ', error);
});
}
onGoBack($event: Event) {
this.navigateBack2AppList();
}
private navigateBack2AppList() {
this.router.navigate(['../'],
{
relativeTo: this.activatedRoute
});
}
isRunning(): boolean {
return this.processDetails && !this.processDetails.ended;
}
cancelProcess() {
this.processService.cancelProcess(this.processDetails.id).subscribe(
(data) => {
console.log('Process instance was cancelled: ', this.processDetails.id);
this.processDetails = null;
}, (err) => {
console.log('Process instance could not be cancelled: ', this.processDetails.id, ', error: ', err);
});
}
onAttachmentClick() {
}
getProcessNameOrDescription(dateFormat): string {
let name = '';
if (this.processDetails) {
name = this.processDetails.name ||
this.processDetails.processDefinitionName + ' - ' + this.getFormatDate(this.processDetails.started, dateFormat);
}
return name;
}
private getFormatDate(value, format: string) {
const datePipe = new DatePipe('en-US');
try {
return datePipe.transform(value, format);
} catch (err) {
console.log(`Error parsing date ${value} to format ${format}`);
}
}
onTasksError(error: any) {
console.log(`Error loading tasks ${error} for process instance ${this.processDetails.id}`);
}
}
The component class is very straightforward, it basically just fetches the process instance details for passed in ID. The process-id is passed in via the URL, which looks like http://localhost:4200/my-processes/<process-id>. We extract the process-id in the ngOnInit method via the activedRoute object.
When we have the ID we use the ADF ProcessService to fetch a ProcessInstance object representing the details about the process instance. We use the ProcessService also when the user clicks the Cancel process button.
We implement the navigate back handler as previously done by just stepping up to the parent route. We don’t need to pass anything back to the process list as it always justy displays all apps and then the user selects an app again.
That should do it, you can now try out the process list and process details pages.
To work with version 2.0 of the Alfresco Application Development Framework (ADF) is a very rewarding journey. The extensive range of ADF components that are available means that you can build a process management application from scratch in no time, picking the pieces you need.
My goal was to build a process management application where I could login and browse the process applications, look at the process definitions, start process instances, interact with task instances and workflow instances, view the details for tasks and processes, and see a preview of the process definitions and instances. I was a bit sceptical in the beginning if it would be possible to do all that by just picking and choosing components, adding as I went along. But it really does work!
Next you might want to do the same on the ACS side, check out the Building a Content Management App with ADF 2.0.0 article.
Blog posts and updates about Application Development Framework (ADF).
By using this site, you are agreeing to allow us to collect and use cookies as outlined in Alfresco’s Cookie Statement and Terms of Use (and you have a legitimate interest in Alfresco and our products, authorizing us to contact you in such methods). If you are not ok with these terms, please do not use this website.