diff --git a/ng2-components/ng2-activiti-processlist/src/components/activiti-process-attachment-list.component.css b/ng2-components/ng2-activiti-processlist/src/components/activiti-process-attachment-list.component.css
new file mode 100644
index 0000000000..170e76fd6c
--- /dev/null
+++ b/ng2-components/ng2-activiti-processlist/src/components/activiti-process-attachment-list.component.css
@@ -0,0 +1,17 @@
+alfresco-datatable >>> .column-header {
+ color: #232323;
+ font-size: 15px;
+}
+
+alfresco-datatable >>> .data-cell {
+ cursor: pointer !important;
+}
+
+.no-attachment-message {
+ border: 1px solid rgb(224, 224, 224);
+ background: #fff;
+ text-align: left;
+ border-top: none;
+ padding: 10px;
+ text-align: center;
+}
diff --git a/ng2-components/ng2-activiti-processlist/src/components/activiti-process-attachment-list.component.html b/ng2-components/ng2-activiti-processlist/src/components/activiti-process-attachment-list.component.html
new file mode 100644
index 0000000000..3613ae89f9
--- /dev/null
+++ b/ng2-components/ng2-activiti-processlist/src/components/activiti-process-attachment-list.component.html
@@ -0,0 +1,15 @@
+
+ No Attachments Found
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ng2-components/ng2-activiti-processlist/src/components/activiti-process-attachment-list.component.spec.ts b/ng2-components/ng2-activiti-processlist/src/components/activiti-process-attachment-list.component.spec.ts
new file mode 100644
index 0000000000..cc0c99e4bb
--- /dev/null
+++ b/ng2-components/ng2-activiti-processlist/src/components/activiti-process-attachment-list.component.spec.ts
@@ -0,0 +1,210 @@
+/*!
+ * @license
+ * Copyright 2016 Alfresco Software, Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { SimpleChange } from '@angular/core';
+import { ComponentFixture, TestBed, async } from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+import { Observable } from 'rxjs/Rx';
+
+import { AlfrescoTranslationService, CoreModule } from 'ng2-alfresco-core';
+import { ActivitiContentService } from 'ng2-activiti-form';
+import { DataTableModule, ObjectDataRow, DataRowEvent, ObjectDataTableAdapter, DataSorting } from 'ng2-alfresco-datatable';
+
+import { ActivitiProcessAttachmentListComponent } from './activiti-process-attachment-list.component';
+import { TranslationMock } from './../assets/translation.service.mock';
+
+describe('Activiti Process Instance Attachment List', () => {
+
+ let componentHandler: any;
+ let service: ActivitiContentService;
+ let component: ActivitiProcessAttachmentListComponent;
+ let fixture: ComponentFixture;
+ let getProcessRelatedContentSpy: jasmine.Spy;
+ let deleteContentSpy: jasmine.Spy;
+ let getFileRawContentSpy: jasmine.Spy;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ CoreModule.forRoot(),
+ DataTableModule.forRoot()
+ ],
+ declarations: [
+ ActivitiProcessAttachmentListComponent
+ ],
+ providers: [
+ { provide: AlfrescoTranslationService, useClass: TranslationMock },
+ ActivitiContentService
+ ]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+
+ fixture = TestBed.createComponent(ActivitiProcessAttachmentListComponent);
+ component = fixture.componentInstance;
+ service = fixture.debugElement.injector.get(ActivitiContentService);
+
+ getProcessRelatedContentSpy = spyOn(service, 'getProcessRelatedContent').and.returnValue(Observable.of(
+ {
+ 'size': 2,
+ 'total': 2,
+ 'start': 0,
+ 'data': [{
+ 'id': 4001,
+ 'name': 'Invoice01.pdf',
+ 'created': '2017-05-12T12:50:05.522+0000',
+ 'createdBy': {
+ 'id': 1,
+ 'firstName': 'Apps',
+ 'lastName': 'Administrator',
+ 'email': 'admin@app.activiti.com',
+ 'company': 'Alfresco.com',
+ 'pictureId': 3003
+ },
+ 'relatedContent': true,
+ 'contentAvailable': true,
+ 'link': false,
+ 'mimeType': 'application/pdf',
+ 'simpleType': 'pdf',
+ 'previewStatus': 'created',
+ 'thumbnailStatus': 'created'
+ },
+ {
+ 'id': 4002,
+ 'name': 'Invoice02.pdf',
+ 'created': '2017-05-12T12:50:05.522+0000',
+ 'createdBy': {
+ 'id': 1,
+ 'firstName': 'Apps',
+ 'lastName': 'Administrator',
+ 'email': 'admin@app.activiti.com',
+ 'company': 'Alfresco.com',
+ 'pictureId': 3003
+ },
+ 'relatedContent': true,
+ 'contentAvailable': true,
+ 'link': false,
+ 'mimeType': 'application/pdf',
+ 'simpleType': 'pdf',
+ 'previewStatus': 'created',
+ 'thumbnailStatus': 'created'
+ }]
+ }));
+
+ deleteContentSpy = spyOn(service, 'deleteRelatedContent').and.returnValue(Observable.of({successCode: true}));
+
+ let blobObj = new Blob();
+ getFileRawContentSpy = spyOn(service, 'getFileRawContent').and.returnValue(Observable.of(
+ blobObj
+ ));
+
+ componentHandler = jasmine.createSpyObj('componentHandler', [
+ 'upgradeAllRegistered',
+ 'upgradeElement'
+ ]);
+ window['componentHandler'] = componentHandler;
+ });
+
+ it('should load attachments when processInstanceId specified', () => {
+ let change = new SimpleChange(null, '123', true);
+ component.ngOnChanges({ 'processInstanceId': change });
+ expect(getProcessRelatedContentSpy).toHaveBeenCalled();
+ });
+
+ it('should emit an error when an error occurs loading attachments', () => {
+ let emitSpy = spyOn(component.error, 'emit');
+ getProcessRelatedContentSpy.and.returnValue(Observable.throw({}));
+ let change = new SimpleChange(null, '123', true);
+ component.ngOnChanges({ 'processInstanceId': change });
+ expect(emitSpy).toHaveBeenCalled();
+ });
+
+ it('should not attach when no processInstanceId is specified', () => {
+ fixture.detectChanges();
+ expect(getProcessRelatedContentSpy).not.toHaveBeenCalled();
+ });
+
+ it('should display attachments when the process has attachments', async(() => {
+ let change = new SimpleChange(null, '123', true);
+ component.ngOnChanges({ 'processInstanceId': change });
+
+ fixture.whenStable().then(() => {
+ fixture.detectChanges();
+ expect(fixture.debugElement.queryAll(By.css('alfresco-datatable tbody tr')).length).toBe(2);
+ });
+ }));
+
+ it('should not display attachments when the process has no attachments', async(() => {
+ component.processInstanceId = '123';
+ getProcessRelatedContentSpy.and.returnValue(Observable.of({
+ 'size': 0,
+ 'total': 0,
+ 'start': 0,
+ 'data': []
+ }));
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ fixture.detectChanges();
+ expect(fixture.debugElement.queryAll(By.css('alfresco-datatable tbody tr')).length).toBe(0);
+ });
+ }));
+
+ describe('change detection', () => {
+
+ let change = new SimpleChange('123', '456', true);
+ let nullChange = new SimpleChange('123', null, true);
+
+ beforeEach(async(() => {
+ component.processInstanceId = '123';
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ getProcessRelatedContentSpy.calls.reset();
+ });
+ }));
+
+ it('should fetch new attachments when processInstanceId changed', () => {
+ component.ngOnChanges({ 'processInstanceId': change });
+ expect(getProcessRelatedContentSpy).toHaveBeenCalledWith('456');
+ });
+
+ it('should NOT fetch new attachments when empty changeset made', () => {
+ component.ngOnChanges({});
+ expect(getProcessRelatedContentSpy).not.toHaveBeenCalled();
+ });
+
+ it('should NOT fetch new attachments when processInstanceId changed to null', () => {
+ component.ngOnChanges({ 'processInstanceId': nullChange });
+ expect(getProcessRelatedContentSpy).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('Delete attachments', () => {
+
+ beforeEach(async(() => {
+ component.processInstanceId = '123';
+ fixture.detectChanges();
+ fixture.whenStable();
+ }));
+
+ it('should display a dialog to the user when the Add button clicked', () => {
+ expect(true).toBe(true);
+ });
+
+ });
+
+});
diff --git a/ng2-components/ng2-activiti-processlist/src/components/activiti-process-attachment-list.component.ts b/ng2-components/ng2-activiti-processlist/src/components/activiti-process-attachment-list.component.ts
new file mode 100644
index 0000000000..16139580e1
--- /dev/null
+++ b/ng2-components/ng2-activiti-processlist/src/components/activiti-process-attachment-list.component.ts
@@ -0,0 +1,163 @@
+/*!
+ * @license
+ * Copyright 2016 Alfresco Software, Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Component, OnChanges, Input, SimpleChanges, Output, EventEmitter } from '@angular/core';
+import { AlfrescoTranslationService, ContentService } from 'ng2-alfresco-core';
+import { ActivitiContentService } from 'ng2-activiti-form';
+
+@Component({
+ selector: 'activiti-process-attachment-list',
+ styleUrls: ['./activiti-process-attachment-list.component.css'],
+ templateUrl: './activiti-process-attachment-list.component.html'
+})
+export class ActivitiProcessAttachmentListComponent implements OnChanges {
+
+ @Input()
+ processInstanceId: string;
+
+ @Output()
+ attachmentClick = new EventEmitter();
+
+ @Output()
+ error: EventEmitter = new EventEmitter();
+
+ attachments: any[] = [];
+
+ constructor(private translateService: AlfrescoTranslationService,
+ private activitiContentService: ActivitiContentService,
+ private contentService: ContentService) {
+
+ if (translateService) {
+ translateService.addTranslationFolder('ng2-activiti-processlist', 'node_modules/ng2-activiti-processlist/src');
+ }
+ }
+
+ ngOnChanges(changes: SimpleChanges) {
+ if (changes['processInstanceId'] && changes['processInstanceId'].currentValue) {
+ this.processInstanceId = changes['processInstanceId'].currentValue;
+ this.loadAttachmentsByProcessInstanceId(this.processInstanceId);
+ }
+ }
+
+ reset () {
+ this.attachments = [];
+ }
+
+ private loadAttachmentsByProcessInstanceId(processInstanceId: string) {
+ if (processInstanceId) {
+ this.reset();
+ this.activitiContentService.getProcessRelatedContent(processInstanceId).subscribe(
+ (res: any) => {
+ res.data.forEach(content => {
+ this.attachments.push({
+ id: content.id,
+ name: content.name,
+ created: content.created,
+ createdBy: content.createdBy.firstName + ' ' + content.createdBy.lastName,
+ icon: this.activitiContentService.getMimeTypeIcon(content.mimeType)
+ });
+ });
+
+ },
+ (err) => {
+ this.error.emit(err);
+ });
+ }
+ }
+
+ private deleteAttachmentById(contentId: string) {
+ if (contentId) {
+ this.activitiContentService.deleteRelatedContent(contentId).subscribe(
+ (res: any) => {
+ this.attachments = this.attachments.filter(content => {
+ return content.id !== contentId;
+ });
+ },
+ (err) => {
+ this.error.emit(err);
+ });
+ }
+ }
+
+ isEmpty(): boolean {
+ return this.attachments && this.attachments.length === 0;
+ }
+
+ onShowRowActionsMenu(event: any) {
+ let viewAction = {
+ title: 'View',
+ name: 'view'
+ };
+
+ let removeAction = {
+ title: 'Remove',
+ name: 'remove'
+ };
+
+ let downloadAction = {
+ title: 'Download',
+ name: 'download'
+ };
+
+ event.value.actions = [
+ viewAction,
+ removeAction,
+ downloadAction
+ ];
+ }
+
+ onExecuteRowAction(event: any) {
+ let args = event.value;
+ let action = args.action;
+ if (action.name === 'view') {
+ this.emitDocumentContent(args.row.obj);
+ } else if (action.name === 'remove') {
+ this.deleteAttachmentById(args.row.obj.id);
+ } else if (action.name === 'download') {
+ this.downloadContent(args.row.obj);
+ }
+ }
+
+ openContent(event: any): void {
+ let content = event.value.obj;
+ this.emitDocumentContent(content);
+ }
+
+ emitDocumentContent(content: any) {
+ this.activitiContentService.getFileRawContent(content.id).subscribe(
+ (blob: Blob) => {
+ content.contentBlob = blob;
+ this.attachmentClick.emit(content);
+ },
+ (err) => {
+ this.error.emit(err);
+ }
+ );
+ }
+
+ /**
+ * Invoke content download.
+ */
+ downloadContent(content: any): void {
+ this.activitiContentService.getFileRawContent(content.id).subscribe(
+ (blob: Blob) => this.contentService.downloadBlob(blob, content.name),
+ (err) => {
+ this.error.emit(err);
+ }
+ );
+ }
+}