Skip navigation
All Places > Application Development Framework > Blog

Are you a developer and would like to contribute to the Alfresco's Angular Open Source framework?

Do you plan to do a Pull Request on the ADF source code or the JS-API source code?

 

If this is the case, we are very glad to remind you that the entire ADF project is Open Source and its contributions are welcome. The ADF Team is happy to receive your contributions: could be an issue found during the development or an experimentation, could be a bug fix, could be anything else will make the project better for our eco-system of developers.

 

To thank you for the active contributions, we want to give you back something. We want to recognise your help with something small, but meaningful. To everyone that will do a Pull Request, and will be accepted, we will send a t-shirt or a similar gift as a reward.

 

 

We want the ADF project to be an open supported framework by the broad community of Alfresco Developers.

 

References of the ADF project

 

Below you can find a brief list of references to help you understand the most relevant resources for the ADF Project.

 

Official GitHub Project - alfresco-ng2-components

Getting started guides 

Gitter channel on ADF 

ADF Component Catalog 

Alfresco-JS-API

ADF App Generator 

 

How to contribute to the ADF project

 

Code contribution acceptance criteria · Alfresco/alfresco-ng2-components Wiki · GitHub 

Working with the development branch · Alfresco/alfresco-ng2-components Wiki · GitHub 

 

How to get in touch

 

Please refer to the Alfresco issue tracker for the known issues you might be interested to work on.

If you have any questions, please contact us using gitter .

SIn the version 2.3.0 of ADF has been added a new Sidenav Layout Component : 

 

Sidenav Layout Component

In the next version of the generator this component will be by default present in your new generated app, but what if you want to add it to your current application?

 

In this tutorial, we are going to add this new component in a previous version of an ADF application. If you need more details about the sidenav layout component after the tutorial please check the documentation:

alfresco-ng2-components/sidenav-layout.component.md at master · Alfresco/alfresco-ng2-components · GitHub 

 

If you want to give a look at the example that I made, you can find it here:

Dev eromano sidebar by eromano · Pull Request #23 · Alfresco/adf-examples · GitHub 

 

Step 1

 

Update your current application package.json ADF dependencies to 2.3.0:

 

"@alfresco/adf-content-services" : "2.3.0"
"@alfresco/adf-process-services" : "2.3.0"
"@alfresco/adf-core" : "2.3.0"
"@alfresco/adf-insights" : "2.3.0"
"alfresco-js-api": "2.3.0"

 

Step 2

 

Generate the app-layout component:

ng generate component app-layout

Modify app-layout.component.html 

 

Note the route and the element in the HTML below are just for example, you need to feed this template with your own routes and elements:

 

<adf-sidenav-layout [sidenavMin]="70" [sidenavMax]="200" [stepOver]="780" [hideSidenav]="false" [expandedSidenav]="false">

    <adf-sidenav-layout-header>
        <ng-template let-toggleMenu="toggleMenu">
            <mat-toolbar color="primary" class="adf-app-layout-toolbar mat-elevation-z6">
                <button mat-icon-button (click)="toggleMenu()">
                    <mat-icon>menu</mat-icon>
                </button>

                <span fxFlex="1 1 auto" fxShow fxHide.lt-sm="true">ADF</span>

                <div class="adf-app-layout-menu-spacer"></div>

            </mat-toolbar>
        </ng-template>
    </adf-sidenav-layout-header>

    <adf-sidenav-layout-navigation>
        <ng-template let-isMenuMinimized="isMenuMinimized">
            <mat-nav-list class="adf-sidenav-linklist">
                <a mat-list-item class="adf-sidenav-link" routerLink="/login">
                    <mat-icon matListIcon class="sidenav-menu-icon">vpn_key</mat-icon>
                    <div class="sidenav-menu-label" *ngIf="!isMenuMinimized()">Login</div>
                </a>
                <a mat-list-item class="adf-sidenav-link" routerLink="/apps">
                    <mat-icon matListIcon class="sidenav-menu-icon">device_hub</mat-icon>
                    <div class="sidenav-menu-label" *ngIf="!isMenuMinimized()">Apps</div>
                </a>
                <a mat-list-item class="adf-sidenav-link" routerLink="/documentlist">
                    <mat-icon matListIcon class="sidenav-menu-icon">folder_open</mat-icon>
                    <div class="sidenav-menu-label" *ngIf="!isMenuMinimized()">Documents</div>
                </a>
                <a mat-list-item adf-logout class="adf-sidenav-link">
                    <mat-icon matListIcon class="sidenav-menu-icon">exit_to_app</mat-icon>
                    <div class="sidenav-menu-label" *ngIf="!isMenuMinimized()">Logout</div>
                </a>
            </mat-nav-list>
        </ng-template>
    </adf-sidenav-layout-navigation>

    <adf-sidenav-layout-content>
        <ng-template>
            <router-outlet></router-outlet>
        </ng-template>
    </adf-sidenav-layout-content>
</adf-sidenav-layout>

 

Modify app-layout.component.scss 

 

In this SCSS file should be present only the router outlet

adf-sidenav-layout {
    height: 100%;
}

 

Modify app.component.html 

 

In this HTML file should be present only the router outlet

<router-outlet></router-outlet> 

 

Step 3

 

Modify app.routes.ts

Now let's add the AppLayoutComponent as the main route of the router and move all  yours previous route as children of the AppLayoutComponent as in this example:

 

export const appRoutes: Routes = [
  {
    path: '',
    component: AppLayoutComponent,
    children: [
      {
        path: '',
        component: HomeComponent
      },
     {.....},
     {.....},
     {.....},
     {.....},
     {.....}
    ]
  };

 

If you have more questions, please reply here or contact me using  gitter .

Often after you have to build your app for a different reason you need to track it.The analytics collected can be used for multiple purposes:

  • Track possible error in your software
  • New functionality engagement
  • Understand how the user interacts with your product

 

In my career, I saw multiple time create this kind of tracking systems implemented duplicating the code everywhere in the app. What I want to propose to you today is a simple way to archive this result taking advantages of the message log bus introduced in ADF 2.3.0. 

If you want to give a look at the example that I made, you can find it here:

 

logservice message bus example by eromano · Pull Request #22 · Alfresco/adf-examples · GitHub 

 

Step 1 Generate your application:

 

First, install yeoman:

npm install -g yo

Then the Alfresco Application Generator:

npm install -g generator-alfresco-adf-app

Then create the app

yo alfresco-adf-app

Step 2 create an account on Mixpanel

 

Go to https://mixpanel.com and create your account. Once you have your account, you can get your Mixpanel token from the account information.

 

Step 3 add the log to your generated app

 

With the logService you can add differents kind of type of logs: 

"ERROR|DEBUG|INFO|LOG|TRACE|WARN|ASSERT"

Once you decided which kind of log is the one that you need to add a similar piece of code in your application:

import { LogService } from '@alfresco/adf-core';

@Component({...})
export class AppComponent {

    constructor(logService: LogService) {
    }
   
    myMethod(){
      this.logService.error('My error');
      this.logService.trace('My trace')
      this.logService.debug('My debug')
      this.logService.info('My info')
      this.logService.warn('My warn')
    }
   
}

Please check the log service documentation for more details:  alfresco-ng2-components/log.service.md at master · Alfresco/alfresco-ng2-components · GitHub 

 

Step 4 Send the data to Mixapnel

 

In order to use Mixpanel we can take advantage of the Mixpanel javascript library:

Let's install it from npm:

npm install mixpanel

Now we need to redirect all the logService message to Mixpanel. The best way to archive this integration is adding a subscriber to the logService message bus:

 

import { Component } from '@angular/core';
import { LogService } from '@alfresco/adf-core';
import * as Mixpanel from 'mixpanel';

@Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.css']
})
export class AppComponent {

    constructor(public logService: LogService) {

        const mixpanel: any = Mixpanel.init('YOUR_MIXPANEL_TOKEN');
        mixpanel.set_config({ debug: true });

        logService.onMessage.subscribe((message) => {
            mixpanel.track(message.text, {
                type: message.type
            });

        });
    }

}

 

If you have more questions, please reply here or contact me using  gitter .

 

 

Alfresco Application Development Framework (aka ADF) is a set of generic and customizable Angular components (and services), used into an application to build your own solution on top of Alfresco Content Services and Alfresco Process Services. In concrete use cases you might want to build your own library of components (and services, modules and whatever you will want) starting from the existing collection and extending it.

 

Thanks to Angular’s capabilities, the creation of new components and their organisation in a library, is something possible and straightforward. In this tutorial we are going to describe how to do it, using the powerful Angular CLI, in a concrete example you can try into your development environment.

 

The approach described here is the same used by the ADF Team to develop the official packages and uses the ng-packagr by David Herges. This tutorial is mainly based on this blog post, suggested for a standard Angular application and still valid also in case of Alfresco Application Development Framework.

 

For further details about the Alfresco ADF, please refer to the alfresco-ng2-components GitHub project. For requests of support, questions and feedback, don’t forget to participate to the Community of Developers dedicated to the ADF or to the Gitter space dedicated to the Alfresco Angular components.

 

Prerequisites

Before jumping into the tutorial, let’s check your environment to be sure you have the right prerequisites. As described above, Angular CLI and Node.js as a consequence, is assumed to be already installed. To check if they are correctly installed into your environment, run the following commands in a terminal.

 

node -v
ng --version

 

All this tutorial has been developed and tested using Node.js version 8.11.1 and Angular CLI version 1.7.3 into an Ubuntu Operating System version 16.04 LTS.

 

Any variation to the listed versions and tools could affect the success of the tutorial, even if the involved technologies and tasks have been defined so as not to depend on any specific context or platform. Please let us know of any issue or problem. The best way to provide feedback is through the Community Portal dedicated to the Alfresco ADF or using the Gitter space dedicated to the Alfresco Angular components.

Goals (what we are going to develop)

The goal of this tutorial is very simple: developing an Angular project implementing a library composed by one component only (a hello-world component). Once done, you will see how to transpile the library into an Angular Package Format using the ng-packagr. Then you will see how to release it in a public repository (the npm registry) or as compressed archive. Last but not least, you will see how to reuse the published package into an ADF application (more in particular we will use the Alfresco Example Content app).

 

Once done, you as a developer, should be more familiar on developing your own library of components to be used in a single project or defining a collection of components for more projects and customers. In both cases, this approach should clarify how to create a reusable library of components, released in the public npm registry to be imported and installed in third party applications by other developers.

 

Following the principle of “simple is better”, the goal of this tutorial is to show a very basic example, enabling the developers on something they might find useful in their day-by-day development. Using this approach in most complex use cases and scenarios, should be straightforward enough to be a good solution in developing complex applications using Alfresco Application Development Framework.

Creating the library

As first practical task of this tutorial, we are going to develop (from scratch) an Angular project implementing a library composed by one component only (a hello-world component). To reach that goal we are going to use the Angular CLI and split the task in two sub-tasks: the first about creating an Angular project hosting the library and the second adding a custom component named hello-world.

Creating an Angular project

The creation of an Angular project from scratch, is pretty straightforward using the Angular CLI. In our example we are going to name the library my-component-library, but you can choose your preferred name according with your needs. From a terminal, execute the following command and Angular CLI will do all the rest for you.

 

ng new my-component-library

 

We won’t detail here the anatomy of the basic Angular project, but all you have to know is that your brand new project is all available into the my-component-library folder. if you want to see it in action, cd in the my-component-library folder and run ng serve. Opening the browser to the URL http://localhost:4200, this is what you will see.

Adding a component

Once the very basic my-component-library app is created, the next task is to add the simplest possible component, named hello-world. From the terminal, being in the my-component-library folder, execute the following commands.

 

ng generate module modules/hello-world
ng generate component modules/hello-world

 

The two commands will add an Angular module named HelloWorldModule and an Angular component named HelloWorldComponent. Once done, edit the hello-world.component.html file into the src/app/modules/hello-world folder, and replace the entire content with the following.

 

<h1>
 Hello World!
</h1>

 

Now edit the hello-world.module.ts (in the same folder) and add the following export item. This change will instruct the HelloWorldModule module to export the new component into the hosting application, when it will be used into a third party app.

 

declarations: [
 HelloWorldComponent
],
exports: [
 HelloWorldComponent // <-- this!
]

 

Once done, edit the app.component.html file into the src/app folder replacing the content with the following content. This change will replace the home page of the my-component-library application displaying the hello-world component only.

 

<app-hello-world></app-hello-world>

 

To make the hello-world component usable into the my-component-library application, edit the app.module.ts file into the src/app folder and add the following source code.

 

// import our module 
import { HelloWorldModule } from './modules/hello-world/hello-world.module';

...

imports: [
 BrowserModule,
 HelloWorldModule // <-- this!
],

 

That’s all! To see how the my-component-library application looks like, run the ng serve command from the root of the project and open http://localhost:4200 into your browser. Below how the home page looks like.

Transpiling the library into an Angular Package Format

Now that the my-component-library project is defined with the hello-world component hosted into it, let’s see how to make it reusable by third party applications. The principle guiding this task is to transpile the library into the so called Angular Package Format.

 

Thanks to the ng-packagr project (by David Herges) the task is straightforward and will let the developer able to publish the package into the public npm registry or distribute it as a tarball archive. In the following sections we are going to see how to develop each task, starting with the installation of the ng-packagr into the project and discussing its usage.

Installing the ng-packagr

To prepare the my-component-library project to be “packaged” in Angular Package Format, let’s install the ng-packagr. With this purpose in mind, open a terminal directly in the root of the project and run the following command.

 

npm install ng-packagr --save-dev

 

To setup the configuration, create a new file named ng-package.json directly in the root of the project and add the following content into it.

 

{
 "$schema": "./node_modules/ng-packagr/ng-package.schema.json",
 "lib": {
   "entryFile": "public_api.ts"
 }
}

 

As alternative It is also possible to store the same content in the regular package.json, if using the "ngPackage": { … } syntax. This apart, to complete the setup, create the file named public_api.ts in the root of the project, with the following content.

 

export * from './src/app/modules/hello-world/hello-world.module'

 

As last task, edit the package.json file in the root of the project and change the following parts.

 

"scripts": {
 "packagr": "ng-packagr -p ng-package.json" // <-- this!
},
"private": false // <-- change this too!

 

Your my-component-library project is now ready to be pushed on GitHub or every other subversion system you might want to use in your development lifecycle.

Creating the distribution package

Starting from here, the creation of a distribution package is now an easy task to complete. To prepare the project, edit the package.json file in the root of the project and change the dependencies item in peerDependencies.

 

Open a terminal and run npm run packagr. Below you can see the screenshot showing how the result should look like.


Starting from here, you’ll find a dist folder in your project root together with a dist.tgz file. This is your component library, fully self sufficient, packaged according to best practices and ready to be reused in every other project or application.

Creating an alternative distribution archive

If you’d prefer to pack your library for a local development, you might be interested to know about an alternative approach, creating a tarball archive of the dist folder. To complete the task, open a terminal into the dist folder and run npm pack. After the end of the execution, a file called my-component-library-0.0.0.tgz will be created. The 0.0.0 part comes from the package.json configuration.

 

From other Angular applications on your system that require your component library, you can run npm install ./path/my-component-library-0.0.0.tgz and your library of components will be installed locally (more precisely into the node_modules folder).

Publishing the Angular Package

Now that you know how to pack your library in Angular Package Format, let’s see here how to publish the distribution archive into the public npm registry. As you probably know, the npm registry is the world's largest software registry, with over 600,000 JavaScript packages (building blocks of code). The npm registry is the right place to publish the my-component-library project, if you want it to be used into others applications from npm with npm install my-component-library.

 

To publish the library, be sure you are logged in npm by executing npm login in a terminal. If you don’t have a valid login to npm, be sure you completed the registration first. Once logged in, you can publish your component library executing npm publish dist from the root of your application.

 

Be sure that you have a unique package name (my-component-library may be taken) and remember that you cannot publish an older version of your package or publish again the same version.

 

Once executed with success the npm publish dist, a short message will appear into your terminal and the library of components will be available to be consumed by you or other developers.

Consuming an Angular Package

In this section we are going to discuss how to consume the components developed in the my-component-library, starting for the Angular Package published into the npm registry.

 

As an example, we are going to add (and use) the hello-world component into the Alfresco Example Content app developed using the Alfresco Application Development Framework components.

 

To prepare the Alfresco Example Content app, let’s open a terminal and run the following commands.

 

git clone https://github.com/Alfresco/alfresco-content-app.git
cd alfresco-content-app/
npm install

 

This is all you need to have the Alfresco Example Content app ready to be run into your environment. Once done, let’s install the library into the ADF application with the following command.

 

npm install my-component-library

 

Of course, in your case you will replace the my-component-library with the name of your published package. Once done, edit the app.module.ts file into the src/app folder and change the following content. This will add the HelloWorldModule, together with the HelloWorldComponent, to the ADF application.

 

// import our module.
import { HelloWorldModule } from 'my-component-library';

@NgModule({
 imports: [
     ...
     HelloWorldModule // <-- This!
 ],

 

To consume the hello-world component, edit the login.component.html in the src/app/components/login folder as described below. This will add the hello-world component to the login page, more in particular will show the Hello World message on top of the page.

 

<app-hello-world></app-hello-world> <!-- Add this! -->
<adf-login ...>
</adf-login>

 

Once done, run the npm start and below you can see the resulting page in your browser as an example.

Congratulations! You succeed in developing, publishing and using your custom library of components into a third party ADF application.

Conclusion

In this tutorial you saw how to create an Angular project containing one component (but could be a collection), package it in Angular Package Format using ng-packagr, publish it in the npm registry and consume it into every ADF application. This content should help the developers in developing a set of reusable components and maybe publish them for others benefit.

 

If you experience any problem on executing this tutorial or want to share a feedback, feel free to leave a comment or raise a question in the Community Portal dedicated to the Alfresco ADF or in the Gitter space dedicated to the Alfresco Angular components. We are always happy to discuss about technical topics and receive feedback from our ecosystem of developers.

Introduction

Thi is my second article on how to use Docker with ADF. In the previous article ADF Docker practical guide I described how is possible start the demo shell or the example content app using Docker.

What if you want use docker in your generated ADF app? From the version 2.1.0 of our generator, we have added some new capability to help you in this task. Let's give a look together how to do it.

 

Prerequisites

Generate an ADF app 

If you already have the version 2.1.0 or major of the alfresco generator app you can skip the two initial step.

 

  1. install Yeoman:
    npm install -g yo
  2. install lfresco Application Generator:
    npm install -g generator-alfresco-adf-app
  3. move to the folder where you want to create your project
    yo alfresco-adf-app
 After the  3 steps above, you should have your ADF scaffolder app ready.

If you need more information about our app generator please visit this repository: GitHub - Alfresco/generator-ng2-alfresco-app: Yeoman Generator Angular 2 Alfresco Application  

Let's test it! to start the app you need to run the command

Before npm start if you didn't select to install your dependencies. The command : "npm install" is necessary

npm start

Open in your browser http://localhost:4200/login to reach your generated app

Generate a Docker image

After generating the ADF app, you will find in your folder app a markdown file docker.md that will help you but don't worry, I will bring you through all the necessary steps:

 

Publish the generated app on docker hub

  1. First of all, if you do not have a Docker Hub account, you should create an account here: https://hub.docker.com/, the registration is absolutely free.
  2. From the folder of your generated app, build your app:
    npm run build
    The build command will create your dist folder

    If you want add some customization in the app.config.json you need to do it before the build

  3. Now you can build your Docker mage:
    docker image build -t myaccount/my_app_name:1.0 .
    Replace myaccount with your Docker Hub account name.
    Please note the ending "." symbol at the end of the command. It instructs the Docker to take current folder where the `Dockerfile` is located.
  4. .Is the moment to publish our newly created image in docker hub:

    docker push myaccount/my_app_name:1.0

 

Congratulations! at this point, your first app is on docker hub!

You can access your docker hub profile and even add some documentation to it visiting  https://hub.docker.com/.

 

Start the docker container

Now that your image is on docker hub you can quickly test and run it in any environment using the following command:

docker container run -p 80:80 --rm myaccount/my_app_name:1.0

--rm options will cleanup the container and image data once you stop the process.

At this point open in your browser http://localhost/login your running docker app!

 

Travis integration

All the generated app are provided with a Travis configuration file. This file can help you to initialize your CI on Travis.

if you what publish an image for any build of your ADF app you need to uncomment the relative part in the .travis.yml file:

#Uncomment this part if you want publish your docker image
#  - export BRANCH=$(if [ "$TRAVIS_PULL_REQUEST" == "false" ]; then echo $TRAVIS_BRANCH; else echo $TRAVIS_PULL_REQUEST_BRANCH; fi)
#  - echo "TRAVIS_BRANCH=$TRAVIS_BRANCH, PR=$PR, BRANCH=$BRANCH"
#  - export TAG=`if [ "$BRANCH" == "master" ]; then echo "latest"; else echo $BRANCH ; fi`
#  - docker build -t $DOCKER_REPO:$TAG .
#  # Publish extra image based on Travis build number
#  - docker tag $DOCKER_REPO:$TAG $DOCKER_REPO:travis-$TRAVIS_BUILD_NUMBER
#  - docker login -u "$DOCKER_USERNAME" -p "$DOCKER_PASSWORD"
#  - docker push $DOCKER_REPO

The email, username, and password used for login should be stored in the repository settings environment variables, which may be set up through the web or locally via the Travis CLI, e.g.:

travis env set DOCKER_USERNAME myusername travis env set DOCKER_PASSWORD secretsecr

For more information see also Pushing a Docker Image to a Registry

 

Conclusion

When we created the generator our purpose was to give to all the users a full scaffolder to start an Angular project using the ADF framework.

if you have more questions or you need help to start to use those technologies, please reply here or contact me using gitter.


First of all, if you never been in Lisbon go and visit it!

 

This is not a wrap up for the Devcon 2018, so I will be short on this topic: great community energy, amazing food, amazing people, amazing city, amazing weather, amazing talks and amazing organization!

 

My favourite things on this trip

 

Here you can find my slides

 

All the code from my Unified Javascript API talk

 

My favourite talks

 

Post Devcon conversation

For me, the best part of the Alfresco Devcon 2018 is the conversation that started after the conference and triggered by the following tweet made by Jeff Potts.
https://twitter.com/jeffpotts01/status/954052157480747013

 

I agree with him on the idea of coming all together to build a new and better application based on ADF, and I hope the content app could be a good base to start from

https://github.com/Alfresco/alfresco-content-app/

 

On this matter, we had Bindu kicking off the conversation on the Content app with this list of requirements https://github.com/Alfresco/alfresco-content-app/issues/179.

 

I personally invite everyone to join the conversation on GitHub and the community site.
In this phase, ideas and contributions are fundamental and most welcome because we are already discussing it and identifying which points can be covered by Alfresco engineers and on which points we are going to need the help of the community in order to achieve it.

 

Why do we need a new app?

  • Front end development has changed since Share was created and developing applications on top of Aikau and Surf requires different skills and tooling from modern web applications.
  • Using more recent technologies with wide adoption makes it easier to find people skilled in them, will help grow our community, and will incentivize collaboration across communities with the exchange of ideas and solutions.
  • Today extension and software update model are too complicated.
  • Accelerate time to value.

 

My proposal to collaborate on the content app (I'm happy to discuss it and improve with any advice)

  • Phase 1 - Submit proposals
  • Phase 2 - Internal review proposals in Alfresco
  • Phase 3 - Review proposals with the community
  • Phase 4 - PoC and investigating different ways of approaching extensibility in collaboration with the community

 

Is quite likely that as Alfresco we will not be able to cover all the requests from the community and we are going to need your help to achieve it, this doesn't mean that we will leave the community alone on these points, we will have Alfresco engineers supporting the community to achieve these goals.

 

If you have any idea on how to improve this proposal please feel free to comment.

Introduction

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.

 

Article Series

This article is part of series of articles covering ADF 2.0:

 

 

Prerequisites

This articles assumes that you are starting with an ADF application that has a menu, navigation, toolbar, and logout as per the Adding Navigation, Menu, Toolbar, and Logout to your ADF 2.0 App   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 mbergljungnpm install

Martins-Macbook-Pro:adf-workbench-process20 mbergljungnpm 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

If you are just cloning the source code from the article, please remember that you must have Node.js 8 and Angular CLI 1.5.x already installed. If you don't, then resort to the linked article for information about how to install these tools.

Source Code

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

 

Listing Process Apps and Process Definitions

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.

 

Generating the Process Apps module and page components

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-Pro:process-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-Pro:process-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-Pro:process-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 { }

 

Automatically route to the Process Apps List page after Login

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:

 

 

Implementing the Process Apps List page

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

 

Implementing the Process App Detail 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. 

 

Displaying Process Definitions for a Process App

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.

 

Generating the Process Definitions page components

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-Pro:process-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-Pro:process-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.

 

Implementing the Process Definitions List

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

 

  • data: holds the data that should be displayed in the table. It refers to a Data Table Adapter. There are many of those and you can implement your own feeding the table from whatever data source. For our implementation we will us an out-of-the-box ObjectDataTableAdapter.
  • showHeader: set to true to display data table column headers.
  • selectionMode (single): determines how many rows you can select at a time. Single row (default), multiple rows, or none.
  • multiselect (false): displays checkboxes if set to true. 
  • loading (false): set to true when the table is loading data.
  • actions: actions are menu items in the “three dots” menu that you can see to the right on each data table row. This property just controls if this menu should be available/visible or not. So set to true to display this menu.
    • (showRowActionsMenu): if actions=true, then this event handler function will be called just before the “three dots” menu is about to be displayed, so you get a chance to define the menu items to display.
    • actionsPosition: set to 'right' to display the “three dots” menu on the right side of the data row, or set to ‘left’.
    • (executeRowAction): event handler function is called when one of the “three dots” menu items are clicked on. Only relevant if actions=true, and the showRowActionsMenu handler has been implemented to provide a menu.
  • contextMenu: these are menu items in the “drop down” menu that you can see when right-clicking on a data table row. This property just controls if this menu should be available/visible or not. So set to true to display this menu when right clicking.
    • (showRowContextMenu): if contextMenu=true, then this event handler function will be called just before the “drop down” menu is about to be displayed, so you get a chance to define the menu items to display.
  • (rowClick): event handler function will be called if you click a data row

 

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

 

Displaying Process Definition Details

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. 

 

Starting Processes from a Process App

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.

 

Generating a Start Process page component

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-Pro:process-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. 

 

Implementing Navigation to the Start Process Page

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

 

Implementing the Start Process Page

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.

 

Managing My Tasks

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.

 

Generating the My Tasks module and page components

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.

 

Implementing the My Tasks List component

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:

 

  • Task Query Filters:
    • appId: the Process Application id. Task instances will only be displayed if they belong to process instances associated with this process app.
    • processInstanceId: the Process Instance ID. Task instances will only be displayed if they belong to this Process Instances.
    • state: task instances in this state will be included. Can be active or completed.  
    • assignment: task instances will be included if they have this type of assignment. Can be assignee (task is assigned to current user), candidate (this is a pooled task that current user can claim), empty (include all).
    • name: include task only if it matches this name. Name is from TaskDetailsModel below.
  • sort: sort the task list by this property. For example 'created-desc'. See TaskDetailsModel below.
  • landingTaskId: the task instance row that should be selected in the list.
  • data: the ObjectDataTableAdapter that is backing the Task List.
  • selectionMode (none): determines how many rows you can select at a time. single row, multiple rows, or none.
  • multiselect (false): displays checkboxes if set to true
  • page (0): what page to display if there are more tasks then the page size.
  • size (5): the page size, how many tasks should be displayed per page in the task list.
  • (rowClick): called when one of the task rows is clicked.
  • (rowSelected): called when one of the tasks rows is selected.
  • (onSuccess): called when a task has been successfully completed.
  • (onError): called when a task could NOT be successfully completed.

 

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.

 

Implementing the Task Details component

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.

 

Managing My Processes

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.

 

Generating the My Processes module and page components

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.

 

Implementing the My Processes List component

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:

 

  • Process query filters:
    • appId: the process application id. Process instances will only be displayed if they belong to this process app.
    • processDefinitionKey: the process definition to match by this key. Process instances will only be displayed if they have been created from this process definition.
    • name: include process only if it matches this name. Name is from ProcessInstance below.
  • state: process instances in this state will be included. Can be running or completed.  
  • sort: sort the process list by this property. For example 'started-desc'. See ProcessInstance below.
  • (rowClick): called when one of the processes in the list is clicked.
  • (onSuccess): called when the Process Instance list has been loaded successfully.
  • (onError): called when the Process Instance list failed to load.

 

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.

 

Implementing the Process Details component

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.

 

Summary

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.

Introduction

In this article we will look at how you can use the Alfresco Content Services (ACS) related ADF components to build a Content 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 content management application interface to look something like this:

 

This application should allow the user to browse the complete Alfresco Repository. While browsing the Repository the user will be able to execute some of the more common content actions, such as Download, Details, Copy, Move, etc. There will be file Preview available so the user doesn't have to download a file to look at the content. It will also be possible to look at just My Files and also to browse and work with files via Sites. The interface will provide Search in both metadata and content. 

 

Article Series

This article is part of series of articles covering ADF 2.0:

 

 

Prerequisites

This articles assumes that you are starting with an ADF application that has a menu, navigation, toolbar, and logout as per this  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-content20

 

This clones the starter project within a new directory called adf-workbench-content. Install all the packages for the project like this:

 

Martins-Macbook-Pro:ADF mbergljung$ cd adf-workbench-content20

Martins-Macbook-Pro:adf-workbench-content20 mbergljungnpm install

Martins-Macbook-Pro:adf-workbench-content20 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

If you are just cloning the source code from the article, please remember that you must have Node.js 8 and Angular CLI 1.5.x already installed. If you don't, then resort to the linked article for information about how to install these tools.

Source Code

While walking through this article it is a good idea to have the source code available. You can clone the source as follows:

 

Martins-Macbook-Pro:ADF mbergljung$ git clone https://github.com/gravitonian/adf-workbench-content20.git adf-workbench-content20-src

 

Listing files and folders in the Alfresco Repository

After we have logged in we most likely want to navigate around in the Alfresco Repository and look at folders and files. This can easily be done with the ADF Document List component. We will create a page that displays the content of the Repository top folder /Company Home. The implementation will actually be in the form of a Master-Detail pattern. So we prepare for the possibility to view details for a file or a folder.

 

Generating module and pages and setting up routing

As usual, we can easily create a new module and components with the Angular CLI tool. Standing in the adf-workbench-content20 directory do the following:

 

Martins-MacBook-Pro:adf-workbench-content20 mbergljung$ ng g module repository --flat false --routing

  create src/app/repository/repository-routing.module.ts (253 bytes)

  create src/app/repository/repository.module.ts (295 bytes)

 

Martins-MacBook-Pro:adf-workbench-content20 mbergljung$ cd src/app/repository/

 

Martins-MacBook-Pro:repository mbergljung$ ng g component repository-page

  create src/app/repository/repository-page/repository-page.component.css (0 bytes)

  create src/app/repository/repository-page/repository-page.component.html (34 bytes)

  create src/app/repository/repository-page/repository-page.component.spec.ts (685 bytes)

  create src/app/repository/repository-page/repository-page.component.ts (304 bytes)

  update src/app/repository/repository.module.ts (405 bytes)

 

Martins-MacBook-Pro:repository mbergljung$ ng g component repository-list-page

  create src/app/repository/repository-list-page/repository-list-page.component.css (0 bytes)

  create src/app/repository/repository-list-page/repository-list-page.component.html (39 bytes)

  create src/app/repository/repository-list-page/repository-list-page.component.spec.ts (714 bytes)

  create src/app/repository/repository-list-page/repository-list-page.component.ts (323 bytes)

  update src/app/repository/repository.module.ts (535 bytes)

 

Martins-MacBook-Pro:repository mbergljung$ ng g component repository-details-page

  create src/app/repository/repository-details-page/repository-details-page.component.css (0 bytes)

  create src/app/repository/repository-details-page/repository-details-page.component.html (42 bytes)

  create src/app/repository/repository-details-page/repository-details-page.component.spec.ts (735 bytes)

  create src/app/repository/repository-details-page/repository-details-page.component.ts (335 bytes)

  update src/app/repository/repository.module.ts (677 bytes)

 

This creates a repository module with routing and the following pages:

 

  • Repository Parent page - will contain just the <router-outlet>, and it has the following child pages:
    • Repository List Page - this is the Master view with the document list
    • Repository Details Page - this is the Details view for a folder or a file

 

Let’s configure the routing table, open up the src/app/repository/repository-routing.module.ts file and update it with the new route to the repository page:

 

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { RepositoryPageComponent } from './repository-page/repository-page.component';
import { RepositoryDetailsPageComponent } from './repository-details-page/repository-details-page.component';
import { RepositoryListPageComponent } from './repository-list-page/repository-list-page.component';

import { AuthGuardEcm } from '@alfresco/adf-core';


const routes: Routes = [
  {
    path: 'repository',
    component: RepositoryPageComponent,
    canActivate: [AuthGuardEcm],
    data: {
      title: 'Repository',
      icon: 'folder',

      hidden: false,
      needEcmAuth: true,
      isLogin: false
    },
    children: [
      { path: '', component: RepositoryListPageComponent, canActivate: [AuthGuardEcm] },
      { path: ':node-id', component: RepositoryDetailsPageComponent, canActivate: [AuthGuardEcm] }
    ]
  }
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class RepositoryRoutingModule {}

 

When we use the http://localhost:4200/repository we will hit the parent page component RepositoryPageComponent and as there is a child component RepositoryListPageComponent with empty path '' it will automatically be invoked and the document list displayed.

 

When one of the items in the document list is clicked, such as a file, and we select a Details content action from the 'Three Dots' menu, then the http://localhost:4200/repository/<node-id> URL will be invoked taking the user to the RepositoryDetailsPageComponent.

 

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 AuthGuardEcm ADF component makes sure that the route cannot be activated if the user is not authenticated with ACS. Which leads us to make sure that the app is set up to authenticate with ACS 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]="'ECM'"
[copyrightText]="'© 2017 Alfresco Training.'"
...

 

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 { RepositoryRoutingModule } from './repository/repository-routing.module';
import { RepositoryModule } from './repository/repository.module';

@NgModule({
declarations: [
   AppComponent
],
imports: [
   BrowserModule,
   AppRoutingModule,

   AppCommonModule,
   AppLoginModule,
   AppLoginRoutingModule,
   RepositoryModule,
   RepositoryRoutingModule
],
providers: [AppMenuService],
bootstrap: [AppComponent]
})
export class AppModule { }

 

Note how we also import the RepositoryModule with the page components. The order that we add the *RoutingModules in matter as that is the order in which they will be displayed in the side navigation.

 

Logging in should display the Repository link in the left navigation as follows (start the server with adf-workbench-content20 $ npm start):

 

 

We can now start to implement the list and details pages.

 

Implementing the Repository Parent Page

The parent page will just be a container for the content that is output from the child pages. Open up the src/app/repository/repository-page/repository-page.component.html file and update it so it contains the Router Outlet:

 

<router-outlet></router-outlet>

 

Whenever we navigate to the Repository List page (http://localhost:4200/repository) or the Repository Details page (http://localhost:4200/repository/123) their template view will now be output in this router outlet instead of in the main app page router outlet that is contained in the src/app/app.component.html template file.

 

 Clicking on the Repository link in the side navigation should now work and take you to the Repository List page:

 

 

Implementing the Repository List Page

To display the contents of the Alfresco Repository we can use the ADF Document List component that is available in the @alfresco/adf-content-services package/library (follow the link for full documentation of this component). We don't need to install this package as that has already been done in the initial articles in this series.

 

The ADF Document List component uses the ADF Datatable component under the covers. So whatever you know/learn about the ADF Datatable will be applicable also to the ADF Document List.

 

The basic usage of the ADF Document list is as follows:

 

<adf-document-list
   #documentList
   [currentFolderId]="'-my-'"
   [contextMenuActions]="true"
   [contentActions]="true">

</adf-document-list>

The documentList is the identifier we give this document list instance in the UI. We can choose another identifier if we like, but remember that the docs refer to this specific identifier from time to time.

 

The currentFolderId property is used to specify from where in the repository we want to display folders and files. The following list of constants can be used (also referred to as Data Sources):

 

  • -root- : Displays content from the root of the Repository, which means from the top folder called /Company Home.
  • -shared- : Displays shared files (This is the Shared Files menu item in Alfresco Share). Same as /Company Home/Shared.
  • -my- : Displays my files (This is the My Files menu item in Alfresco Share). Same as listing /Company Home/User Homes/<user id>.
  • -mysites-Displays a list of Alfresco Share sites that the current user is a member of. This is the content under /Company Home/Sites.
  • -trashcan- : Displays files that the user has soft deleted. (This is the User Profile | Trashcan menu item in Alfresco Share).
  • -sharedlinks- : Displays a list of shared content item links.
  • -sites- : Displays a list of Alfresco Share sites. This is the content under /Company Home/Sites.
  • -favorites- : Displays a list of content items that the user has marked as favorites.
  • -recent- : Displays a list of recently accessed content items.

 

Open up the Repository list page template file located in src/app/repository/repository-list-page/repository-list-page.component.html and define the template as follows:

 

<adf-document-list
#documentList
[navigationMode]="'click'"
[currentFolderId]="'-root-'"
[contextMenuActions]="true"
[contentActions]="true">

</adf-document-list>

 

Note that we changed to use -root- as the data source because we want to show folders and files from the top /Company Home folder. The navigationMode property was also set to click so we don’t have to double click all the time when navigating around in the Document List.

 

The adf-document-list tag will not be known to the application until we import the AppCommonModule. Open up the src/app/repository/repository.module.ts and add it:

 

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';

import { RepositoryRoutingModule } from './repository-routing.module';
import { RepositoryPageComponent } from './repository-page/repository-page.component';
import { RepositoryListPageComponent } from './repository-list-page/repository-list-page.component';
import { RepositoryDetailsPageComponent } from './repository-details-page/repository-details-page.component';

import { AppCommonModule } from '../app-common/app-common.module';

@NgModule({
  imports: [
    CommonModule,
    RepositoryRoutingModule,

    /* Common App imports (Angular Core and Material, ADF Core, Content, and Process */
    AppCommonModule
  ],
  declarations: [RepositoryPageComponent, RepositoryListPageComponent, RepositoryDetailsPageComponent]
})
export class RepositoryModule {}

 

The AppCommonModule will bring in all the Angular stuff, Google Material components and the ADF Content and Process components. So we should be all set to go.

 

You should see the following result if you login and then click on the Repository link in the side navigation:

 

 

Pretty amazing for a few lines of code!

 

So we got a header where we can sort ascending/descending, we got a default column layout, and we can click and navigate into a folder. We also got the context sensitive menu on each row but it is empty at the moment.

 

Let’s make some improvements.

 

After Successful Login navigate to Repository

When the user has successfully logged in it would be nice if the Repository List page was shown automatically. We can fix this with a minor update to the Login page component class. Open up the src/app/app-login/app-login-page/app-login-page.component.ts file and update it as follows:

 

What we do here is just uncomment the router.navigate code in the onLoginSuccess method and configure it to navigate to /repository.

 

Adding Pagination

As you might have noticed, the Document List does not have pagination built in (it used to in previous versions of ADF, but having it separate gives more freedom). The ADF component that we need to use is called <adf-pagination. Update the Repository page template file src/app/repository/repository-list-page/repository-list-page.component.html and add the component as follows:

 

<adf-document-list
  #documentList
  [navigationMode]="'click'"
  [currentFolderId]="'-root-'"
  [contextMenuActions]="true"
  [contentActions]="true">

</adf-document-list>

<adf-pagination
  [target]="documentList">

</adf-pagination>

 

The adf-pagination component has a property called target that needs to be set to point to the Document List component instance that we want to show pagination for. In this case documentList. So make sure the IDs match.

 

The application UI now displays a pagination bar at the bottom of the Document List:

 

 

The user can select how many folders and files to display per page via the "Items per page" drop down. It is also possible to navigate directly to a page via the "Page" drop down. To navigate between pages the user clicks on the left (<) and right (>) arrows. 

 

It is possible to inject your own code into the pagination logic. The following events (Outputs) are available:

 

  • (change): called for any change to the pagination bar.
  • (changePageNumber): called when the "Page" drop down is changed.
  • (changePageSize)called when the "Items per page" drop down is changed.
  • (nextPage): called when navigating to the next page
  • (prevPage): called when navigating to the previous page

 

There is also another pagination component called <adf-infinite-pagination that can be used if you want infinite scrolling.

 

Adding a Toolbar with Breadcrumb Navigation 

Adding a Toolbar with Breadcrumb navigation to the Repository view would be nice as then we could navigate in the folder hierarchy more freely. Luckily there are a couple of useful ADF components for just that. Update the Repository page template file src/app/repository/repository-list-page/repository-list-page.component.html and add the <adf-toolbar and <adf-breadcrumb components as follows:

 

<adf-toolbar [color]="'accent'">
  <adf-toolbar-title>
    <adf-breadcrumb
      root="Company Home"
      [target]="documentList"
      [folderNode]="documentList.folderNode">

    </adf-breadcrumb>
  </adf-toolbar-title>
</adf-toolbar>

<adf-document-list
  #documentList
  [navigationMode]="'click'"
  [currentFolderId]="'-root-'"
  [contextMenuActions]="true"
  [contentActions]="true">

</adf-document-list>

<adf-pagination
  [target]="documentList">

</adf-pagination>

 

The adf-breadcrumb component has a property called target that needs to be set to point to the Document List component instance that we want to show breadcrumbs for. In this case documentList. So make sure the IDs match.

The folderNode property should be set to the node that we are currently at. So we grab that directly from the document list instance with documentList.folderNode.

 

We enclose the breadcrumb component inside an adf-toolbar component so it sits nicely spaced from the document list and from the top toolbar. The document list toolbar will have a different color than the top toolbar, so we set the color property to 'accent', which is part of the Google Material theming.

 

The application UI now displays a breadcrumb for the Document List as follows:

 

 

Here I have navigated down in the folder hierarchy to the Email Templates folder. I can easily navigate back up to Company Home by clicking on that folder in the breadcrumbs toolbar.

 

Adding Drag-n-Drop File Upload

There is currently no way of dragging and dropping files into a folder. This is usually available in the Alfresco Share folder view so users would expect this feature to be available. We can add that via the ADF Upload Drag area component, here is how you use it:

 

<adf-upload-drag-area 
[parentId]="parent folder id where files should be uploaded"
(success)="onSuccess($event)">

 
       Component that you want to drag and drop into

</adf-upload-drag-area>

 

This component is part of the @alfresco/adf-content-services package and is available without any further configuration. So we can add the adf-upload-drag-area component directly in our template. Open up src/app/repository/repository-list-page/repository-list-page.component.html and add it as follows:

 

<adf-toolbar [color]="'accent'">
  <adf-toolbar-title>
    <adf-breadcrumb
      root="Company Home"
      [target]="documentList"
      [folderNode]="documentList.folderNode">

    </adf-breadcrumb>
  </adf-toolbar-title>
</adf-toolbar>

<adf-upload-drag-area
  [parentId]="documentList.currentFolderId || '-root-'"
  (success)="onDragAndDropUploadSuccess($event)">

  <adf-document-list
    #documentList
    [navigationMode]="'click'"
    [currentFolderId]="'-root-'"
    [contextMenuActions]="true"
    [contentActions]="true">

  </adf-document-list>
  <adf-pagination
    [target]="documentList">

  </adf-pagination>
</adf-upload-drag-area>

 

The most important thing here is to set the parentId property correctly, it should point to where we want the content files to be uploaded. In this case they should be uploaded to the current folder of the Document List, which we can access via the documentList.currentFolderId property. It is important here to give an alternative to the documentList.currentFolderId value when it is null, that's why we do || -root-. If we don't do this, then we will get Angular change detection errors. 

 

Also note that if parentId is not set properly, then files will be uploaded to the /Company Home (if you have permission) folder, which might not be what you want.

 

Implement the onDragAndDropUploadSuccess method, it will be called when content has been uploaded successfully. Do this as follows in the src/app/repository/repository-list-page/repository-list-page.component.ts class:

 

import { Component, OnInit, ViewChild } from '@angular/core';

import { DocumentListComponent } from '@alfresco/adf-content-services';

@Component({
  selector: 'app-repository-list-page',
  templateUrl: './repository-list-page.component.html',
  styleUrls: ['./repository-list-page.component.scss']
})
export class RepositoryListPageComponent implements OnInit {
  @ViewChild(DocumentListComponent)
  documentList: DocumentListComponent;

  constructor() { }

  ngOnInit() {  }

  onDragAndDropUploadSuccess($event: Event) {
    console.log('Drag and Drop upload successful!');

    // Refresh the page so you can see the new files
    this.documentList.reload();
  }
}

 

The onDragAndDropUploadSuccess method has to do a bit of work as the folder is not automatically refreshed after an upload. So you might think that the upload did not work as you don’t see the file(s). Fix this by accessing the underlying view/template Document List component instance with the Angular ViewChild decorator. It’s then an easy task of just reloading the document list for current folder after a successful upload.

 

We should now see the following if we navigate into an empty folder such as /Company Home/Shared:

 

Folders with existing content will not show this information but you can still drag and drop to them. If we drag and drop a file into the Shared folder we see the the log message in the Console window to the right and the file added to the folder and screen updated.

 

There are two more properties associated with the Drag-n-Drop component that is good to know about: 

  • disabled (boolean): can be used to disable the control (i.e. disable drag-n-drop), defaults to false.
  • versioning (boolean): controls if the uploaded files should be versioned, defaults to false.

 

Enable/Disable Upload based on User Permissions

So things are working nicely with the upload. However, if you create another user in ACS that does not have more than read permissions to the Content Repository, then you will get some errors in the console if you try an upload, here I'm trying an upload to the Data Dictionary that the user does not have access to:

 

 

 

The error spells it out “You do not have the appropriate permissions to perform this operation”. What we need is a way to disable the Drag-n-Drop upload functionality if the user doesn’t have permission to create stuff in a folder. We can do this with the adf-node-permission and adf-nodes properties:

 

<adf-toolbar [color]="'accent'">
  <adf-toolbar-title>
    <adf-breadcrumb
      root="Company Home"
      [target]="documentList"
      [folderNode]="documentList.folderNode">

    </adf-breadcrumb>
  </adf-toolbar-title>
</adf-toolbar>

<adf-upload-drag-area
  [parentId]="documentList.currentFolderId || '-root-'"
  [adf-node-permission]="'create'"
  [adf-nodes]="getNodesForPermissionCheck()"
  (success)="onDragAndDropUploadSuccess($event)">

  <adf-document-list
    #documentList
    [navigationMode]="'click'"
    [currentFolderId]="'-root-'"
    [contextMenuActions]="true"
    [contentActions]="true">

  </adf-document-list>
  <adf-pagination
    [target]="documentList">

  </adf-pagination>
</adf-upload-drag-area>

 

The permission we want the user to have on current folder is ‘create’. For more information see the docs. The adf-node-permission property can be used on any component that implements the NodePermissionSubject interface, so probably good to keep in mind when working with components that interact with the Content Repository. This functionality is part of the ADF Core Module, which we import in the RepositoryModule via the AppCommonModule.

 

Just specifying these properties on the component is not enough for it to work. You also need to supply the nodes that should be checked. In our case we want to supply current folder in the Document List. We can do this with the adf-nodes property, which gets its value via the getNodesForPermissionCheck method that is implemented as follows in the src/app/repository/repository-list-page/repository-list-page.component.ts class:

 

import { Component, OnInit, ViewChild } from '@angular/core';

import { DocumentListComponent } from '@alfresco/adf-content-services';
import { MinimalNodeEntity } from 'alfresco-js-api';

@Component({
  selector: 'app-repository-list-page',
  templateUrl: './repository-list-page.component.html',
  styleUrls: ['./repository-list-page.component.scss']
})
export class RepositoryListPageComponent implements OnInit {
  @ViewChild(DocumentListComponent)
  documentList: DocumentListComponent;

  constructor() { }

  ngOnInit() {
  }

  onDragAndDropUploadSuccess($event: Event) {
    console.log('Drag and Drop upload successful!');

    // Refresh the page so you can see the new files
    this.documentList.reload();
  }

  getNodesForPermissionCheck(): MinimalNodeEntity[] {
    if (this.documentList.folderNode) {
      return [{entry: this.documentList.folderNode}];
    } else {
      return [];
    }
  }
}

 

The method is expected to return an array of MinimalNodeEntity, which is a class that looks like this:

export class MinimalNodeEntity {
  constructor(obj?: any);

  entry?: MinimalNodeEntryEntity;
}

And looking at MinimalNodeEntryEntity we can see that this class extends the MinimalNode class, which in turn extends the Node class that looks like this:

 

export class Node {
  constructor(obj?: any);

  id?: string;
  name?: string;
  nodeType?: string;
  isFolder?: boolean;
  isFile?: boolean;
  isLocked?: boolean;
  modifiedAt?: Date;
  modifiedByUser?: UserInfo;
  adddAt?: Date;
  adddByUser?: UserInfo;
  parentId?: string;
  isLink?: boolean;
  content?: ContentInfo;
  aspectNames?: Array<string>;
  properties?: any;
  allowableOperations?: Array<string>;
  path?: PathInfo;
  permissions?: PermissionsInfo;
  createdAt?: Date;
  createdByUser?: UserInfo;
}

We can easily recognise the properties of the node as similar to what we have for a node in the Alfresco Content Repository. The documentList.folderNode property is actually of the MinimalNodeEntity type as well, which we can see if we look at the DocumentListComponent source code:

export class DocumentListComponent implements OnInit, OnChanges, AfterContentInit, PaginatedComponent {
...
    @Input()
    folderNode: MinimalNodeEntryEntity = null;
...
    selection = new Array<MinimalNodeEntity>();

We can also see that when you select something in the Document list you will get the selected nodes as MinimalNodeEntity.

 

If we test this now we will see that drag-n-drop functionality is disabled if we are logged in with a user that does not have create permissions to the folder where we are attempting the upload.

 

Adding a Delete Action supporting i18n

Now that we can upload new files to the Repository it would be nice to be able to delete folders and files. This can be done via content action configurations on the Document list component. The Delete action will look something like this when you click on the 'Three Dots' menu for an item in the Document List:

 

 

While implementing this we will also have a look at internationalisation (i18n) of UI labels, such as the ‘Delete’ label for this action. We will update the i18n resource file (i.e. src/assets/i18n/en.json) with the labels we need for the application. See the first two articles mentioned in the introduction for more information about translations.

 

But as usual, we start with the template update, open up the src/app/repository/repository-list-page/repository-list-page.component.html file and add delete actions as follows via the content-actions component:

 

<adf-toolbar [color]="'accent'">
  <adf-toolbar-title>
    <adf-breadcrumb
      root="Company Home"
      [target]="documentList"
      [folderNode]="documentList.folderNode">

    </adf-breadcrumb>
  </adf-toolbar-title>
</adf-toolbar>

<adf-upload-drag-area
  [parentId]="documentList.currentFolderId || '-root-'"
  [adf-node-permission]="'create'"
  [adf-nodes]="getNodesForPermissionCheck()"
  (success)="onDragAndDropUploadSuccess($event)">

  <adf-document-list
    #documentList
    [navigationMode]="'click'"
    [currentFolderId]="'-root-'"
    [contextMenuActions]="true"
    [contentActions]="true">

    <content-actions>
      <!-- Folder actions -->
      <content-action
        title="{{'DOCUMENT_LIST.ACTIONS.FOLDER.DELETE' | translate}}"
        [icon]="'delete'"
        [target]="'folder'"
        [permission]="'delete'"
        [disableWithNoPermission]="true"
        [handler]="'delete'"
        (permissionEvent)="onDeleteActionPermissionError($event)"
        (success)="onDeleteActionSuccess($event)">

      </content-action>
      <!-- File actions -->
      <content-action
        title="{{'DOCUMENT_LIST.ACTIONS.DOCUMENT.DELETE' | translate}}"
        [icon]="'delete'"
        [target]="'document'"
        [permission]="'delete'"
        [disableWithNoPermission]="true"
        [handler]="'delete'"
        (permissionEvent)="onDeleteActionPermissionError($event)"
        (success)="onDeleteActionSuccess($event)">

      </content-action>
    </content-actions>
  </adf-document-list>
  <adf-pagination
    [target]="documentList">

  </adf-pagination>
</adf-upload-drag-area>

 

The properties and events for each content-action have the following meaning (not all are used in the above configuration):

 

  • title - The title of the action as displayed in the UI menu, i18n resource ID in this case. Using the translate pipe from the ngx-translate/core library. Note that the bound property format (i.e [...]) is not used here as it is easier to use normal HTML attribute format when using the translate pipe. 
  • icon - a Google Material Design icon id.
  • target - Is this action for a ‘folder’ or for a ‘document’ (file).
  • permission - the name of the permission that the user has to have on the content item to be able to delete it. In this case the user needs ‘delete’ permission.
  • disableWithNoPermission - set this to true if you want this content action to be disabled in the menu (i.e. grayed out)  if the user does not have permission to delete the content item.
  • handler - this property is used when the action is available as an out-of-the-box action. Such as for example ‘delete’, ‘download’, ‘copy’, and ‘move’. For an up-to-date list of all available action handlers see the backing service class. If you want to call a custom action implementation use the execute event.
  • (permissionEvent) - this function is called if there is a problem with permissions during the delete of the content item.
  • (success) - this function is called after the content item has been successfully deleted.
  • (error) - this function is called when a content item could not be deleted.
  • (execute) - use this instead of handler when a custom content action function should be called.

 

Now when we have finished configuring the actions that we need it is time to add the required event handler functions. Open up the src/app/repository/repository-list-page/repository-list-page.component.ts file and add the following methods:

 

import { Component, OnInit, ViewChild } from '@angular/core';

import { NotificationService } from '@alfresco/adf-core';
import { DocumentListComponent } from '@alfresco/adf-content-services';
import { MinimalNodeEntity } from 'alfresco-js-api';

@Component({
  selector: 'app-repository-list-page',
  templateUrl: './repository-list-page.component.html',
  styleUrls: ['./repository-list-page.component.scss']
})
export class RepositoryListPageComponent implements OnInit {
  @ViewChild(DocumentListComponent)
  documentList: DocumentListComponent;

  constructor(private notificationService: NotificationService) { }

  ngOnInit() {
  }

  onDragAndDropUploadSuccess($event: Event) {
    console.log('Drag and Drop upload successful!');

    // Refresh the page so you can see the new files
    this.documentList.reload();
  }

  getNodesForPermissionCheck(): MinimalNodeEntity[] {
    if (this.documentList.folderNode) {
      return [{entry: this.documentList.folderNode}];
    } else {
      return [];
    }
  }

  onDeleteActionPermissionError(event: any) {
    this.notificationService.openSnackMessage(
      `You don't have the '${event.permission}' permission to do a '${event.action}' operation on the ${event.type}`,
      4000);
  }

  onDeleteActionSuccess(node) {
    console.log('Successfully deleted a node: ' + node);
  }
}

 

The onDeleteActionPermissionError method will display a message at the bottom of the screen with a text such as ‘You don't have the 'delete' permission to do a 'delete' operation on the content’. The onDeleteActionSuccess method will write a log message such as ‘Successfully deleted a node: 4fdf9fe4-c5fe-4313-bb50-9edbada9216b’. Note how this success method gives you back an Alfresco Node Reference for the deleted node, which might not be super useful...

 

Here we bring in another useful service from the ADF Core library. It is called the Notification Service and is implemented on top of the Angular 2 Material Design Snackbar. It can be used to display small messages on screen.

 

The final thing we need to take care of are the i18n resource identifiers that we have used in our content action configurations:

 

<content-action
       title="{{'DOCUMENT_LIST.ACTIONS.FOLDER.DELETE' | translate}}"

<content-action
       title="{{'DOCUMENT_LIST.ACTIONS.DOCUMENT.DELETE' | translate}}"

 

Update the src/assets/i18n/en.json file, it will be included in the Webpack bundle process. Replace whatever it contains with the following content:

 

{
  "DOCUMENT_LIST": {
    "ACTIONS": {
      "FOLDER": {
        "DELETE": "Delete"
      },
      "DOCUMENT": {
        "DELETE": "Delete"
      }
    }
  }
}

 

Note for Windows users: when copying JSON from this article it might be beneficial to have a JavaScript IDE as it will flag any characters that are not allowed in the document. Otherwise you will get problems with parsing.

You should now be able to try this out now. Note that the server need to be restarted as we updated stuff under /assets, which need to be repackaged by Webpack. Login first with the admin user and make sure that you can delete folders and files. Then login with a user that does not have delete permission and make sure the action is not available then.

 

Adding a Download File Content Action

Being able to download a file from the repository is one of the essential features that we are likely to need in most apps. Download is also one of the out-of-the-box supported ADF content actions, so it is easy to add. The action will appear in the 'Three Dots' menu as follows for files:

 

 

To add the Download content action open up the src/app/repository/repository-list-page/repository-list-page.component.html file and add a 'download' action as follows:

 

<adf-toolbar [color]="'accent'">
  <adf-toolbar-title>
    <adf-breadcrumb
      root="Company Home"
      [target]="documentList"
      [folderNode]="documentList.folderNode">

    </adf-breadcrumb>
  </adf-toolbar-title>
</adf-toolbar>

<adf-upload-drag-area
  [parentId]="documentList.currentFolderId || '-root-'"
  [adf-node-permission]="'create'"
  [adf-nodes]="getNodesForPermissionCheck()"
  (success)="onDragAndDropUploadSuccess($event)">

  <adf-document-list
    #documentList
    [navigationMode]="'click'"
    [currentFolderId]="'-root-'"
    [contextMenuActions]="true"
    [contentActions]="true">

    <content-actions>
      <!-- Folder actions -->
      <content-action
        title="{{'DOCUMENT_LIST.ACTIONS.FOLDER.DELETE' | translate}}"
        [icon]="'delete'"
        [target]="'folder'"
        [permission]="'delete'"
        [disableWithNoPermission]="true"
        [handler]="'delete'"
        (permissionEvent)="onDeleteActionPermissionError($event)"
        (success)="onDeleteActionSuccess($event)">

      </content-action>
      <!-- File actions -->
      <content-action
        title="{{'DOCUMENT_LIST.ACTIONS.DOCUMENT.DOWNLOAD' | translate}}"
        [icon]="'file_download'"
        [target]="'document'"
        [handler]="'download'">

      </content-action>
      <content-action
        title="{{'DOCUMENT_LIST.ACTIONS.DOCUMENT.DELETE' | translate}}"
        [icon]="'delete'"
        [target]="'document'"
        [permission]="'delete'"
        [disableWithNoPermission]="true"
        [handler]="'delete'"
        (permissionEvent)="onDeleteActionPermissionError($event)"
        (success)="onDeleteActionSuccess($event)">

      </content-action>
    </content-actions>
  </adf-document-list>
  <adf-pagination
    [target]="documentList">

  </adf-pagination>
</adf-upload-drag-area>

 

The important part of the new action configuration is the handler configuration. We us the download handler that are available out-of-the-box with ADF. We don't need to check permissions, because if the user can see the content item, then they have read permissions. 

 

There are no event handler methods to implement. Update the i18n resource file src/assets/i18n/en.json with the english Download translation:

 

{
  "DOCUMENT_LIST": {
    "ACTIONS": {
      "FOLDER": {
        "DELETE": "Delete"
      },
      "DOCUMENT": {
        "DOWNLOAD": "Download",
        "DELETE": "Delete"
      }
    }
  }
}

 

Now we should be ready to try out the Download action.

 

Adding a Download Folder Content Action

Being able to download a whole folder as a ZIP file from the repository is a useful feature that we can add to the application. Download a folder as a ZIP file is not one of the out-of-the-box supported ADF content actions, so we need to do a bit of work to get it going. The action will appear in the 'Three Dots' menu as follows for folders:

 

 

To add the Download as ZIP content action open up the src/app/repository/repository-list-page/repository-list-page.component.html file and add it as follows:

 

<adf-toolbar [color]="'accent'">
  <adf-toolbar-title>
    <adf-breadcrumb
      root="Company Home"
      [target]="documentList"
      [folderNode]="documentList.folderNode">

    </adf-breadcrumb>
  </adf-toolbar-title>
</adf-toolbar>

<adf-upload-drag-area
  [parentId]="documentList.currentFolderId || '-root-'"
  [adf-node-permission]="'create'"
  [adf-nodes]="getNodesForPermissionCheck()"
  (success)="onDragAndDropUploadSuccess($event)">

  <adf-document-list
    #documentList
    [navigationMode]="'click'"
    [currentFolderId]="'-root-'"
    [contextMenuActions]="true"
    [contentActions]="true">

    <content-actions>
      <!-- Folder actions -->
      <content-action
        title="{{'DOCUMENT_LIST.ACTIONS.FOLDER.DOWNLOAD_AS_ZIP' | translate}}"
        [icon]="'file_download'"
        [target]="'folder'"
        (execute)="onDownloadAsZip($event)">

      </content-action>
      <content-action
        title="{{'DOCUMENT_LIST.ACTIONS.FOLDER.DELETE' | translate}}"
        [icon]="'delete'"
        [target]="'folder'"
        [permission]="'delete'"
        [disableWithNoPermission]="true"
        [handler]="'delete'"
        (permissionEvent)="onDeleteActionPermissionError($event)"
        (success)="onDeleteActionSuccess($event)">

      </content-action>
      <!-- File actions -->
      <content-action
        title="{{'DOCUMENT_LIST.ACTIONS.DOCUMENT.DOWNLOAD' | translate}}"
        [icon]="'file_download'"
        [target]="'document'"
        [handler]="'download'">

      </content-action>
      <content-action
        title="{{'DOCUMENT_LIST.ACTIONS.DOCUMENT.DELETE' | translate}}"
        [icon]="'delete'"
        [target]="'document'"
        [permission]="'delete'"
        [disableWithNoPermission]="true"
        [handler]="'delete'"
        (permissionEvent)="onDeleteActionPermissionError($event)"
        (success)="onDeleteActionSuccess($event)">

      </content-action>
    </content-actions>
  </adf-document-list>
  <adf-pagination
    [target]="documentList">

  </adf-pagination>
</adf-upload-drag-area>

The important part of the new action configuration is the handler configuration. We us a custom function handler that is executed by implementing the execute event on the content action. We don't need to check permissions, because if the user can see the folder, then they have read permissions. 

 

Now, let's implement the event handler function onDownloadAsZip. Open up the src/app/repository/repository-list-page/repository-list-page.component.ts file and add the handler function and a helper function called downloadZip:

 

import { Component, OnInit, ViewChild } from '@angular/core';
import { MatDialog } from '@angular/material';

import { NotificationService } from '@alfresco/adf-core';
import { DocumentListComponent, DownloadZipDialogComponent } from '@alfresco/adf-content-services';
import { MinimalNodeEntity, MinimalNodeEntryEntity } from 'alfresco-js-api';

@