Alfresco: How to create an Angular Component for ADF

We have develop some Open Source addons for Alfresco in the last years from keensoft. All the source code is available at https://github.com/keensoft.

Since Alfresco is moving from Share web application (based in Aikau) to ADF (based in Angular 5), every addon including an UI part has to be reviewed. I started with Alfresco Esign Cert, our addon to perform electronic signatures using client certificates from every device and operative systems.

Current Share UI is designed to load the content of the document in the browser and to launch a client app called AutoFirma to perform the signature operation. After that, the result is saved again in Alfresco Repository.

This work is based in the blog post Building an Angular Library with the Angular CLI and ng-packagr, but it has been adapted to produce and Alfresco ADF Component.

All the source code is available at https://github.com/keensoft/alfresco-esign-cert/tree/master/esign-cert-ng2

Creating the boilterplate

We need some default files to start, so we created a Signature module and also a Signature component.

$ ng new esign-cert-ng2
$ cd esign-cert-ng2
$ ng generate module modules/signature
$ ng generate component modules/signature

Different files were created in src/app/modules/signature, but only three were relevant for this project:

  • signature.component.html to include the view
  • signature.component.ts to develop the Component
  • signature.module.ts to develop the Module

 Adding the ADF dependencies

ADF 2.2.0 components and services were required, so I played for a while with package.json file to find the right combination.

  "dependencies": {
    "@alfresco/adf-content-services": "2.2.0",
    "@alfresco/adf-core": "2.2.0",
    "@angular/animations": "5.1.1",
    "@angular/cdk": "5.0.1",
    "@angular/common": "5.1.1",
    "@angular/compiler": "5.1.1",
    "@angular/core": "5.1.1",
    "@angular/flex-layout": "2.0.0-beta.12",
    "@angular/forms": "5.1.1",
    "@angular/http": "5.1.1",
    "@angular/material": "5.0.1",
    "@angular/material-moment-adapter": "5.0.1",
    "@angular/platform-browser": "5.1.1",
    "@angular/platform-browser-dynamic": "5.1.1",
    "@angular/router": "5.1.1",
    "@mat-datetimepicker/core": "1.0.1",
    "@mat-datetimepicker/moment": "1.0.1",
    "@ngx-translate/core": "9.1.0",
    "alfresco-js-api": "2.2.0",
    "core-js": "^2.5.4",
    "moment-es6": "^1.0.0",
    "npm": "^5.8.0",
    "rxjs": "^5.5.6",
    "zone.js": "^0.8.19"
  },

Configuring the packaging tool

There are other tools (like Nrwl) that can be using for this purpose, but I chose the classic ng-packagr. Some operations are required to install and configure this program.

$ npm install ng-packagr --save-dev

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

$ cat public_api.ts
export * from './src/app/modules/signature/signature.module';

After that, package.json had to be modified to include the made the tool available by command line.

  "scripts": {
    "ng": "ng",
    "start": "ng serve",
    "build": "ng build --prod",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e",
    "packagr": "ng-packagr -p ng-package.json"
  },
  "private": false,

Producing packages for local development

Some tools like npm-link can be used for development purposes, but generating a package always is a must.

$ npm run packagr
$ cd dist
$ npm pack

So this Component package could be imported from the testing project.

$ npm install ../alfresco-esign-cert/esign-cert-ng2/dist/keensoft-esign-cert-ng2-1.0.0.tgz

Developing

From this point, development environment was set, so I could start to code some features.

I created a SignatureService class to include invocations to custom Web Scripts consumed from esign-cert-repo artifact. The more relevant part was the use of Alfresco API to get a valid session ticket and the use of Config Service to guess Alfresco repository address.

import { AlfrescoApiService, AppConfigService } from '@alfresco/adf-core';
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';

@Injectable()
export class SignatureService {

    // Alfresco host from 'ecmHost' configuration entry, like http://localhost:8080
    ecmHost: string;

    constructor(
        private http: HttpClient,
        private alfrescoApi: AlfrescoApiService,
        private appConfig: AppConfigService) {
            this.ecmHost = this.appConfig.get<string>('ecmHost');
        }

    // Get signature options from alfresco-global.properties (Custom REST API)
    public getSignatureOptions(): Observable<any> {

        const ticket = this.alfrescoApi.getInstance().getTicketEcm();

        return this.http.get(
            this.ecmHost + '/alfresco/s/keensoft/sign/signature-params?alf_ticket=' + ticket
        );

    }

As the electronic signature software relies on an old-styled JavaScript, there were required some tricks in the Component to load the script from assets folder and to manage the callback.

import { NodesApiService } from '@alfresco/adf-core';
import { Component, Input, OnInit, ViewEncapsulation } from '@angular/core';
import { MinimalNodeEntryEntity } from 'alfresco-js-api';

import { SignatureOptions } from './signature.model';
import { SignatureService } from './signature.service';

// miniapplet.js loaded from index.html
declare var MiniApplet: any;

@Component({
    selector: 'app-signature',
    templateUrl: './signature.component.html',
    encapsulation: ViewEncapsulation.None
})

// Launch client signature program for an Alfresco node
export class SignatureComponent implements OnInit {

// ...

    // Sign button: launch local AutoFirma program to perform and attached (PAdES for PDF) or detached (CAdES for other mimetypes) signature
    sign() {
        const signedCallback = this.saveSigned.bind(this);
        const signedError = this.showError.bind(this);
        if (this.mimeType === 'pdf') {
            const paramsPades = this.options.paramsPades.split('\t').join('\n');
            MiniApplet.sign(this.dataToSign, this.options.signatureAlg, 'PAdES', paramsPades, signedCallback, signedError);
        } else {
            const paramsCades = this.options.paramsCades.split('\t').join('\n');
            MiniApplet.sign(this.dataToSign, this.options.signatureAlg, 'CAdES', paramsCades, signedCallback, signedError);
        }
    }
}

Finally, the Module was updated to include every public service and component required from integrating apps.

import { CommonModule } from '@angular/common';
import { HttpClient, HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core';

import { SignatureComponent } from './signature.component';
import { SignatureService } from './signature.service';

export { SignatureComponent };

@NgModule({
  imports: [
    CommonModule,
    HttpClientModule
  ],
  providers: [
    HttpClient,
    SignatureService
  ],
  declarations: [
    SignatureComponent
  ],
  exports: [
    SignatureComponent
  ]
})
export class SignatureModule { }

Testing the integration

As this Component was designed to be used from an ADF application, I downloaded Alfresco Content Application to test if everything worked fine.

After importing the package…

$ npm install ../alfresco-esign-cert/esign-cert-ng2/dist/keensoft-esign-cert-ng2-1.0.0.tgz

… I started adding the dependency to app.module.ts

import { SignatureModule } from '@keensoft/esign-cert-ng2';

// ...

@NgModule({
    imports: [
        BrowserModule,
        RouterModule.forRoot(APP_ROUTES, {
            useHash: true,
            enableTracing: false // enable for debug only
        }),
        AdfModule,
        CommonModule,
        MaterialModule,
        SignatureModule // <--
    ],

The Component used an external JavaScript, so I created a folder src/assets/js/ and copied required files: miniapplet.js and miniapplet-full_1.6.2.jar. These files are used from root index.html file.

  <head>
     <!-- ... -->
     <script type="text/javascript" src="assets/js/miniapplet.js"></script>
  </head>

I created a simple dialog view to inject the node into SignatureComponent identified by selector app-signature.


<header mat-dialog-title>SIGNATURE</header>

<section mat-dialog-content>
    <app-signature [node]="node"></app-signature>
</section>

<footer mat-dialog-actions fxLayout="row" fxLayoutAlign="end center">
    <button mat-button (click)="close()">CLOSE</button>
</footer>

And also the component part was developed.

import { Component, Inject, ViewEncapsulation } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
import { MinimalNodeEntryEntity } from 'alfresco-js-api';

@Component({
  templateUrl: './signature-dialog.component.html',
  encapsulation: ViewEncapsulation.None
})
export class SignatureDialogComponent {

  public node: MinimalNodeEntryEntity;

  constructor(
    @Inject(MAT_DIALOG_DATA) data: any,
    private containingDialog?: MatDialogRef<SignatureDialogComponent>) {
    this.node = data.entry;
  }

  close() {
    this.containingDialog.close();
  }
}

The feature worked as expected, so I moved to the next step.

Publishing on npm

With the NPM account configured, it’s easy to publish a component.

$ npm login
$ npm publish

And after that, the module was available in the repository: https://www.npmjs.com/package/alfresco-esign-cert-ng2

Recap

Development and packaging environment is still far away from Maven based solutions like Alfresco SDK: creating the boilerplate, managing dependencies, upgrading and testing are features to be improved. Probably is too early to start porting all the old stuff from Share to ADF, but this first experience provides some hints for future developers.

Anuncios

My experience adding a new component to Alfresco Content Application 1.1.0

Alfresco is still working on the new ADF framework that is going to replace Alfresco Share for UI development in the Digital Business Platform. ADF is based in Angular 5 (at the moment I’m writing) and provides a catalog of components to be integrated easily with custom ADF applications.

Alfresco is also providing a reference application named Alfresco Content Application to teach and experiment how to use these ADF Components on the real world.

Below I’m describing my experience on including an ADF Component ready-to-use in ACA (Alfresco Content Application).  I thought that this should be a task to be executed in less than one hour, but it was a bit longer in the end.

Reading component documentation

I started reading Version Manager Component documentation, available at GitHub:

https://github.com/Alfresco/alfresco-ng2-components/blob/master/docs/version-manager.component.md

After that, I thought that just including a simple HTML tag should do the trick.


<adf-version-manager [node]="aMinimalNodeEntryEntity"></adf-version-manager>

Understanding where to include the code in ACA

I identified from HTML source how an action was declared in this reference application.

https://github.com/Alfresco/alfresco-content-app/blob/v1.1.0/src/app/components/files/files.component.html#L55

And I included my button in the same way:

<button mat-menu-item [app-version-node]="documentList.selection">
    <mat-icon>history</mat-icon>
    <span>{{ 'APP.ACTIONS.VERSION' | translate }}</span>
</button>                

As actions are developed as individual directives in ACA, I created a new directive name NodeVersionDirective to deal with “app-version-node” tags.

mport { Directive, HostListener, Input } from '@angular/core';

import { MinimalNodeEntity } from 'alfresco-js-api';

@Directive({
    selector: '[app-version-node]'
})
export class NodeVersionDirective {

    @Input('app-version-node')
    selection: MinimalNodeEntity[];

    @HostListener('click')
    onClick() {
        alert("Hello!");
    }

    constructor(
        private dialog: MatDialog
    ) {}

}

At this point, I have no real link with ADF component documentation, as I couldn’t use adf-version-manager in any way.

Understanding how the ADF component works

Luckily, I remembered that Marting Bergljung is writing and excellent series of posts to document how the components can be used in detail. In this case, I searched the one for the release of the component: 2.0.0

https://community.alfresco.com/community/application-development-framework/blog/2017/12/15/building-a-content-management-app-with-adf-200

After studying carefully these instructions, I realised that I need to build some other pieces to include default Version Manager in ACA:

  • A new component to manage form interaction
  • An HTML template for the form

I copied the component…

import { Component, Inject, ViewEncapsulation } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';

import { MinimalNodeEntryEntity } from 'alfresco-js-api';

@Component({
  templateUrl: './version-manager-dialog.component.html',
  encapsulation: ViewEncapsulation.None
})
export class VersionManagerDialogComponent {
  public nodeName: string;
  public nodeEntry: MinimalNodeEntryEntity;

  constructor(@Inject(MAT_DIALOG_DATA) data: any,
              private containingDialog?: MatDialogRef<VersionManagerDialogComponent>) {
    this.nodeEntry = data.entry;
    this.nodeName = this.nodeEntry.name;
  }

  close() {
    this.containingDialog.close();
  }
}

… and the HTML template

<header mat-dialog-title>{{'APP.VERSION.DIALOG.TITLE' | translate}}</header>

<section mat-dialog-content>
    <adf-version-manager [node]="nodeEntry"></adf-version-manager>
</section>

<footer mat-dialog-actions fxLayout="row" fxLayoutAlign="end center">
    <button mat-button (click)="close()">{{'APP.VERSION.DIALOG.CLOSE' | translate}}</button>
</footer>

Back to ACA development

Completing NodeVersionDirective was easier after having the low level pieces ready.

import { Directive, HostListener, Input } from '@angular/core';

import { MinimalNodeEntity } from 'alfresco-js-api';

import { MatDialog } from '@angular/material';
import { VersionManagerDialogComponent } from '../../components/versions/version-manager-dialog.component';

@Directive({
    selector: '[app-version-node]'
})
export class NodeVersionDirective {

    @Input('app-version-node')
    selection: MinimalNodeEntity[];

    @HostListener('click')
    onClick() {
        this.dialog.open(VersionManagerDialogComponent, {
            data: this.selection[0],
            panelClass: 'adf-version-manager-dialog',
            width: '630px'
        });
    }

    constructor(
        private dialog: MatDialog
    ) {}

}

Later I included the directive and the component in global resources:

And I finished the tasks with i18n English resources at en.json

Testing

The action was added to document list page and it worked as expected. However, I realised that there were other views including the same actions source code. So I had to copy-paste the same lines in following resources.

<button mat-menu-item [app-version-node]="documentList.selection">
    <mat-icon>history</mat-icon>
    <span>{{ 'APP.ACTIONS.VERSION' | translate }}</span>
</button>

In the end, all the code seemed to be on the right place. Unfortunately, this component is still not ready for production use, as it’s required for the new file uploaded to have exactly the same name of the previous version. Probably there are any other bugs hidden also.

Sharing with the Community

This is not a polished feature, but I felt that it was right to send a pull request to the team. At least for their evaluation. In my opinion is a good sample on how much work is required to add a simple feature to a real app.

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

Recap

My experience was harder than expected and I would fail without Martin’s blog post. So probably there is still a long way to improve ADF documentation and development experience. In fact, I’m still considering if this framework is ready to start a real project or not.

How Alfresco is helping non-profit organizations

I’ve received an email from a research institute focused on finding natural methods to reduce diseases impact. They thanked me about some suggestions I made to solve a few issues in their Alfresco Community server.

Few days ago I also got back in touch again with another non profit organization  dedicated to preserve the food in the planet. And I also helped them with an Alfresco problem.

From my point of view, such companies have to focus their efforts in achieving their goals. So any selfless collaboration is always much appreciated.

Alfresco Community is freely available and offer powerful capabilities for non-critical business process or content needs. And it can be maintained by many organizations with a small help from tech enthusiasts.

This fact makes Alfresco different from any other product in the market. And even it makes us willing to collaborate with others.

I hope last changes in Alfresco do not make the company to lose the focus from this founding motto