diff --git a/ng2-components/ng2-activiti-tasklist/src/assets/task-details.mock.ts b/ng2-components/ng2-activiti-tasklist/src/assets/task-details.mock.ts
new file mode 100644
index 0000000000..ef07e36a07
--- /dev/null
+++ b/ng2-components/ng2-activiti-tasklist/src/assets/task-details.mock.ts
@@ -0,0 +1,192 @@
+/*!
+ * @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.
+ */
+
+export var taskDetailsMock = {
+ 'id': '91',
+ 'name': 'Request translation',
+ 'description': null,
+ 'category': null,
+ 'assignee': {'id': 1001, 'firstName': 'Wilbur', 'lastName': 'Adams', 'email': 'wilbur@app.activiti.com'},
+ 'created': '2016-11-03T15:25:42.749+0000',
+ 'dueDate': null,
+ 'endDate': null,
+ 'duration': null,
+ 'priority': 50,
+ 'parentTaskId': null,
+ 'parentTaskName': null,
+ 'processInstanceId': '86',
+ 'processInstanceName': null,
+ 'processDefinitionId': 'TranslationProcess:2:8',
+ 'processDefinitionName': 'Translation Process',
+ 'processDefinitionDescription': null,
+ 'processDefinitionKey': 'TranslationProcess',
+ 'processDefinitionCategory': 'http://www.activiti.org/processdef',
+ 'processDefinitionVersion': 2,
+ 'processDefinitionDeploymentId': '5',
+ 'formKey': '4',
+ 'processInstanceStartUserId': '1001',
+ 'initiatorCanCompleteTask': false,
+ 'adhocTaskCanBeReassigned': false,
+ 'taskDefinitionKey': 'sid-DDECD9E4-0299-433F-9193-C3D905C3EEBE',
+ 'executionId': '86',
+ 'involvedPeople': [],
+ 'memberOfCandidateUsers': false,
+ 'managerOfCandidateGroup': false,
+ 'memberOfCandidateGroup': false
+};
+
+export var taskFormMock = {
+ 'id': 4,
+ 'name': 'Translation request',
+ 'processDefinitionId': 'TranslationProcess:2:8',
+ 'processDefinitionName': 'Translation Process',
+ 'processDefinitionKey': 'TranslationProcess',
+ 'taskId': '91',
+ 'taskName': 'Request translation',
+ 'taskDefinitionKey': 'sid-DDECD9E4-0299-433F-9193-C3D905C3EEBE',
+ 'tabs': [],
+ 'fields': [{
+ 'fieldType': 'ContainerRepresentation',
+ 'id': '1478093984155',
+ 'name': 'Label',
+ 'type': 'container',
+ 'value': null,
+ 'required': false,
+ 'readOnly': false,
+ 'overrideId': false,
+ 'colspan': 1,
+ 'placeholder': null,
+ 'minLength': 0,
+ 'maxLength': 0,
+ 'minValue': null,
+ 'maxValue': null,
+ 'regexPattern': null,
+ 'optionType': null,
+ 'hasEmptyValue': null,
+ 'options': null,
+ 'restUrl': null,
+ 'restResponsePath': null,
+ 'restIdProperty': null,
+ 'restLabelProperty': null,
+ 'tab': null,
+ 'className': null,
+ 'dateDisplayFormat': null,
+ 'layout': null,
+ 'sizeX': 2,
+ 'sizeY': 1,
+ 'row': -1,
+ 'col': -1,
+ 'visibilityCondition': null,
+ 'numberOfColumns': 2,
+ 'fields': {
+ '1': [{
+ 'fieldType': 'AttachFileFieldRepresentation',
+ 'id': 'originalcontent',
+ 'name': 'Original content',
+ 'type': 'upload',
+ 'value': [],
+ 'required': true,
+ 'readOnly': false,
+ 'overrideId': false,
+ 'colspan': 1,
+ 'placeholder': null,
+ 'minLength': 0,
+ 'maxLength': 0,
+ 'minValue': null,
+ 'maxValue': null,
+ 'regexPattern': null,
+ 'optionType': null,
+ 'hasEmptyValue': null,
+ 'options': null,
+ 'restUrl': null,
+ 'restResponsePath': null,
+ 'restIdProperty': null,
+ 'restLabelProperty': null,
+ 'tab': null,
+ 'className': null,
+ 'params': {
+ },
+ 'dateDisplayFormat': null,
+ 'layout': {'row': -1, 'column': -1, 'colspan': 1},
+ 'sizeX': 1,
+ 'sizeY': 1,
+ 'row': -1,
+ 'col': -1,
+ 'visibilityCondition': null,
+ 'metaDataColumnDefinitions': []
+ }],
+ '2': [{
+ 'fieldType': 'RestFieldRepresentation',
+ 'id': 'language',
+ 'name': 'Language',
+ 'type': 'dropdown',
+ 'value': 'Choose one...',
+ 'required': true,
+ 'readOnly': false,
+ 'overrideId': false,
+ 'colspan': 1,
+ 'placeholder': null,
+ 'minLength': 0,
+ 'maxLength': 0,
+ 'minValue': null,
+ 'maxValue': null,
+ 'regexPattern': null,
+ 'optionType': null,
+ 'hasEmptyValue': true,
+ 'options': [{'id': 'empty', 'name': 'Choose one...'}, {'id': 'fr', 'name': 'French'}, {
+ 'id': 'de',
+ 'name': 'German'
+ }, {'id': 'es', 'name': 'Spanish'}],
+ 'restUrl': null,
+ 'restResponsePath': null,
+ 'restIdProperty': null,
+ 'restLabelProperty': null,
+ 'tab': null,
+ 'className': null,
+ 'params': {'existingColspan': 1, 'maxColspan': 1},
+ 'dateDisplayFormat': null,
+ 'layout': {'row': -1, 'column': -1, 'colspan': 1},
+ 'sizeX': 1,
+ 'sizeY': 1,
+ 'row': -1,
+ 'col': -1,
+ 'visibilityCondition': null,
+ 'endpoint': null,
+ 'requestHeaders': null
+ }]
+ }
+ }],
+ 'outcomes': [],
+ 'javascriptEvents': [],
+ 'className': '',
+ 'style': '',
+ 'customFieldTemplates': {},
+ 'metadata': {},
+ 'variables': [],
+ 'gridsterForm': false,
+ 'globalDateFormat': 'D-M-YYYY'
+};
+
+export var tasksMock = {
+ data: [
+ taskDetailsMock
+ ]
+};
+
+export var noDataMock = {
+ data: []
+};
diff --git a/ng2-components/ng2-activiti-tasklist/src/components/activiti-task-details.component.css b/ng2-components/ng2-activiti-tasklist/src/components/activiti-task-details.component.css
index 07eaf92d80..67959204ff 100644
--- a/ng2-components/ng2-activiti-tasklist/src/components/activiti-task-details.component.css
+++ b/ng2-components/ng2-activiti-tasklist/src/components/activiti-task-details.component.css
@@ -1,3 +1,7 @@
:host {
width: 100%;
-}
\ No newline at end of file
+}
+
+.error-dialog h3 {
+ margin: 16px 0;
+}
diff --git a/ng2-components/ng2-activiti-tasklist/src/components/activiti-task-details.component.html b/ng2-components/ng2-activiti-tasklist/src/components/activiti-task-details.component.html
index e34583395e..dde8190e57 100644
--- a/ng2-components/ng2-activiti-tasklist/src/components/activiti-task-details.component.html
+++ b/ng2-components/ng2-activiti-tasklist/src/components/activiti-task-details.component.html
@@ -9,7 +9,7 @@
{{taskDetails.name}}
-
+
+
diff --git a/ng2-components/ng2-activiti-tasklist/src/components/activiti-task-details.component.spec.ts b/ng2-components/ng2-activiti-tasklist/src/components/activiti-task-details.component.spec.ts
new file mode 100644
index 0000000000..4e65534f66
--- /dev/null
+++ b/ng2-components/ng2-activiti-tasklist/src/components/activiti-task-details.component.spec.ts
@@ -0,0 +1,249 @@
+/*!
+ * @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 { NO_ERRORS_SCHEMA, 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 { ActivitiFormModule, FormModel, FormOutcomeEvent, FormOutcomeModel, FormService } from 'ng2-activiti-form';
+
+import { ActivitiTaskDetails } from './activiti-task-details.component';
+import { ActivitiTaskListService } from './../services/activiti-tasklist.service';
+import { ActivitiPeopleService } from './../services/activiti-people.service';
+import { TranslationMock } from './../assets/translation.service.mock';
+import { taskDetailsMock, taskFormMock, tasksMock, noDataMock } from './../assets/task-details.mock';
+
+describe('ActivitiTaskDetails', () => {
+
+ let componentHandler: any;
+ let service: ActivitiTaskListService;
+ let formService: FormService;
+ let component: ActivitiTaskDetails;
+ let fixture: ComponentFixture
;
+ let getTaskDetailsSpy: jasmine.Spy;
+ let getFormSpy: jasmine.Spy;
+ let getTasksSpy: jasmine.Spy;
+ let completeTaskSpy: jasmine.Spy;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ CoreModule,
+ ActivitiFormModule
+ ],
+ declarations: [
+ ActivitiTaskDetails
+ ],
+ providers: [
+ { provide: AlfrescoTranslationService, useClass: TranslationMock },
+ ActivitiTaskListService,
+ ActivitiPeopleService
+ ],
+ schemas: [ NO_ERRORS_SCHEMA ]
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+
+ fixture = TestBed.createComponent(ActivitiTaskDetails);
+ component = fixture.componentInstance;
+ service = fixture.debugElement.injector.get(ActivitiTaskListService);
+ formService = fixture.debugElement.injector.get(FormService);
+
+ getTaskDetailsSpy = spyOn(service, 'getTaskDetails').and.returnValue(Observable.of(taskDetailsMock));
+ getFormSpy = spyOn(formService, 'getTaskForm').and.returnValue(Observable.of(taskFormMock));
+ getTasksSpy = spyOn(service, 'getTasks').and.returnValue(Observable.of(tasksMock));
+ completeTaskSpy = spyOn(service, 'completeTask').and.returnValue(Observable.of({}));
+ spyOn(service, 'getTaskComments').and.returnValue(Observable.of(noDataMock));
+ spyOn(service, 'getTaskChecklist').and.returnValue(Observable.of(noDataMock));
+
+ componentHandler = jasmine.createSpyObj('componentHandler', [
+ 'upgradeAllRegistered',
+ 'upgradeElement'
+ ]);
+ window['componentHandler'] = componentHandler;
+ });
+
+ it('should load task details when taskId specified', () => {
+ component.taskId = '123';
+ fixture.detectChanges();
+ expect(getTaskDetailsSpy).toHaveBeenCalled();
+ });
+
+ it('should not load task details when no taskId is specified', () => {
+ fixture.detectChanges();
+ expect(getTaskDetailsSpy).not.toHaveBeenCalled();
+ });
+
+ it('should set a placeholder message when taskId not initialised', () => {
+ fixture.detectChanges();
+ expect(fixture.nativeElement.innerText).toBe('TASK_DETAILS.MESSAGES.NONE');
+ });
+
+ it('should display a form when the task has an associated form', async(() => {
+ component.taskId = '123';
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ fixture.detectChanges();
+ expect(fixture.debugElement.query(By.css('activiti-form'))).not.toBeNull();
+ });
+ }));
+
+ it('should not display a form when the task does not have an associated form', async((done) => {
+ component.taskId = '123';
+ taskDetailsMock.formKey = undefined;
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ fixture.detectChanges();
+ expect(fixture.debugElement.query(By.css('activiti-form'))).toBeNull();
+ });
+ }));
+
+ describe('change detection', () => {
+
+ let change = new SimpleChange('123', '456');
+ let nullChange = new SimpleChange('123', null);
+
+ beforeEach(async(() => {
+ component.taskId = '123';
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ getTaskDetailsSpy.calls.reset();
+ });
+ }));
+
+ it('should fetch new task details when taskId changed', () => {
+ component.ngOnChanges({ 'taskId': change });
+ expect(getTaskDetailsSpy).toHaveBeenCalledWith('456');
+ });
+
+ it('should NOT fetch new task details when empty changeset made', () => {
+ component.ngOnChanges({});
+ expect(getTaskDetailsSpy).not.toHaveBeenCalled();
+ });
+
+ it('should NOT fetch new task details when taskId changed to null', () => {
+ component.ngOnChanges({ 'taskId': nullChange });
+ expect(getTaskDetailsSpy).not.toHaveBeenCalled();
+ });
+
+ it('should set a placeholder message when taskId changed to null', () => {
+ component.ngOnChanges({ 'taskId': nullChange });
+ fixture.detectChanges();
+ expect(fixture.nativeElement.innerText).toBe('TASK_DETAILS.MESSAGES.NONE');
+ });
+ });
+
+ describe('Form events', () => {
+
+ beforeEach(async(() => {
+ component.taskId = '123';
+ fixture.detectChanges();
+ fixture.whenStable();
+ }));
+
+ it('should emit a save event when form saved', () => {
+ let emitSpy: jasmine.Spy = spyOn(component.formSaved, 'emit');
+ component.onFormSaved(new FormModel());
+ expect(emitSpy).toHaveBeenCalled();
+ });
+
+ it('should emit a outcome execution event when form outcome executed', () => {
+ let emitSpy: jasmine.Spy = spyOn(component.executeOutcome, 'emit');
+ component.onFormExecuteOutcome(new FormOutcomeEvent(new FormOutcomeModel(new FormModel())));
+ expect(emitSpy).toHaveBeenCalled();
+ });
+
+ it('should emit a complete event when form completed', () => {
+ let emitSpy: jasmine.Spy = spyOn(component.formCompleted, 'emit');
+ component.onFormCompleted(new FormModel());
+ expect(emitSpy).toHaveBeenCalled();
+ });
+
+ it('should load next task when form completed', () => {
+ component.onComplete();
+ expect(getTasksSpy).toHaveBeenCalled();
+ });
+
+ it('should show placeholder message if there is no next task', () => {
+ getTasksSpy.and.returnValue(Observable.of(noDataMock));
+ component.onComplete();
+ fixture.detectChanges();
+ expect(fixture.nativeElement.innerText).toBe('TASK_DETAILS.MESSAGES.NONE');
+ });
+
+ it('should emit an error event if an error occurs fetching the next task', () => {
+ let emitSpy: jasmine.Spy = spyOn(component.onError, 'emit');
+ getTasksSpy.and.returnValue(Observable.throw({}));
+ component.onComplete();
+ expect(emitSpy).toHaveBeenCalled();
+ });
+
+ it('should NOT load next task when form completed if showNextTask is false', () => {
+ component.showNextTask = false;
+ component.onComplete();
+ expect(getTasksSpy).not.toHaveBeenCalled();
+ });
+
+ it('should call service to complete task when complete button clicked', () => {
+ component.onComplete();
+ expect(completeTaskSpy).toHaveBeenCalled();
+ });
+
+ it('should emit a complete event when complete button clicked and task completed', () => {
+ let emitSpy: jasmine.Spy = spyOn(component.formCompleted, 'emit');
+ component.onComplete();
+ expect(emitSpy).toHaveBeenCalled();
+ });
+
+ it('should call service to load next task when complete button clicked', () => {
+ component.onComplete();
+ expect(getTasksSpy).toHaveBeenCalled();
+ });
+
+ it('should emit a load event when form loaded', () => {
+ let emitSpy: jasmine.Spy = spyOn(component.formLoaded, 'emit');
+ component.onFormLoaded(new FormModel());
+ expect(emitSpy).toHaveBeenCalled();
+ });
+
+ it('should emit an error event when form error occurs', () => {
+ let emitSpy: jasmine.Spy = spyOn(component.onError, 'emit');
+ component.onFormError({});
+ expect(emitSpy).toHaveBeenCalled();
+ });
+
+ it('should display a dialog to the user when a form error occurs', () => {
+ let dialogEl = fixture.debugElement.query(By.css('.error-dialog')).nativeElement;
+ let showSpy: jasmine.Spy = spyOn(dialogEl, 'showModal');
+ component.onFormError({});
+ expect(showSpy).toHaveBeenCalled();
+ });
+
+ it('should close error dialog when close button clicked', () => {
+ let dialogEl = fixture.debugElement.query(By.css('.error-dialog')).nativeElement;
+ let closeSpy: jasmine.Spy = spyOn(dialogEl, 'close');
+ component.onFormError({});
+ component.closeErrorDialog();
+ expect(closeSpy).toHaveBeenCalled();
+ });
+
+ });
+
+});
diff --git a/ng2-components/ng2-activiti-tasklist/src/components/activiti-task-details.component.ts b/ng2-components/ng2-activiti-tasklist/src/components/activiti-task-details.component.ts
index 5cd482dc12..e6c73c9b47 100644
--- a/ng2-components/ng2-activiti-tasklist/src/components/activiti-task-details.component.ts
+++ b/ng2-components/ng2-activiti-tasklist/src/components/activiti-task-details.component.ts
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-import { Component, Input, OnInit, ViewChild, Output, EventEmitter, TemplateRef, OnChanges, SimpleChanges } from '@angular/core';
+import { Component, Input, OnInit, ViewChild, Output, EventEmitter, TemplateRef, OnChanges, SimpleChanges, DebugElement } from '@angular/core';
import { AlfrescoTranslationService, AlfrescoAuthenticationService } from 'ng2-alfresco-core';
import { ActivitiTaskListService } from './../services/activiti-tasklist.service';
import { TaskDetailsModel } from '../models/task-details.model';
@@ -37,6 +37,9 @@ export class ActivitiTaskDetails implements OnInit, OnChanges {
@ViewChild('activitichecklist')
activitichecklist: any;
+ @ViewChild('errorDialog')
+ errorDialog: DebugElement;
+
@Input()
taskId: string;
@@ -82,8 +85,10 @@ export class ActivitiTaskDetails implements OnInit, OnChanges {
/**
* Constructor
- * @param auth
- * @param translate
+ * @param auth Authentication service
+ * @param translate Translation service
+ * @param activitiForm Form service
+ * @param activitiTaskList Task service
*/
constructor(private auth: AlfrescoAuthenticationService,
private translate: AlfrescoTranslationService,
@@ -114,9 +119,9 @@ export class ActivitiTaskDetails implements OnInit, OnChanges {
}
/**
- * Reset the task detail to undefined
+ * Reset the task details
*/
- reset() {
+ private reset() {
this.taskDetails = null;
}
@@ -138,7 +143,7 @@ export class ActivitiTaskDetails implements OnInit, OnChanges {
* Load the activiti task details
* @param taskId
*/
- loadDetails(taskId: string) {
+ private loadDetails(taskId: string) {
this.taskPeople = [];
this.taskFormName = null;
if (taskId) {
@@ -154,10 +159,7 @@ export class ActivitiTaskDetails implements OnInit, OnChanges {
this.taskPeople.push(new User(user));
});
}
- }
- );
- } else {
- this.reset();
+ });
}
}
@@ -166,7 +168,7 @@ export class ActivitiTaskDetails implements OnInit, OnChanges {
* @param processInstanceId
* @param processDefinitionId
*/
- loadNextTask(processInstanceId: string, processDefinitionId: string) {
+ private loadNextTask(processInstanceId: string, processDefinitionId: string) {
let requestNode = new TaskQueryRequestRepresentationModel(
{
processInstanceId: processInstanceId,
@@ -187,11 +189,11 @@ export class ActivitiTaskDetails implements OnInit, OnChanges {
}
/**
- * Complete the activiti task
+ * Complete button clicked
*/
onComplete() {
this.activitiTaskList.completeTask(this.taskId).subscribe(
- (res) => this.formCompleted.emit(null)
+ (res) => this.onFormCompleted(null)
);
}
@@ -215,10 +217,15 @@ export class ActivitiTaskDetails implements OnInit, OnChanges {
}
onFormError(error: any) {
+ this.errorDialog.nativeElement.showModal();
this.onError.emit(error);
}
- onExecuteFormOutcome(event: FormOutcomeEvent) {
+ onFormExecuteOutcome(event: FormOutcomeEvent) {
this.executeOutcome.emit(event);
}
+
+ closeErrorDialog(): void {
+ this.errorDialog.nativeElement.close();
+ }
}
diff --git a/ng2-components/ng2-activiti-tasklist/src/i18n/en.json b/ng2-components/ng2-activiti-tasklist/src/i18n/en.json
index f55ebd1fb9..96642c6c57 100644
--- a/ng2-components/ng2-activiti-tasklist/src/i18n/en.json
+++ b/ng2-components/ng2-activiti-tasklist/src/i18n/en.json
@@ -33,6 +33,11 @@
},
"CHECKLIST": {
"NONE": "No checklist."
+ },
+ "ERROR": {
+ "TITLE": "Something went wrong",
+ "DESCRIPTION": "Could not complete the specified action. Please try again or check that you have permission.",
+ "CLOSE": "Close"
}
},
"TASK_FILTERS": {