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

- +
+ +
+

{{'TASK_DETAILS.ERROR.TITLE'|translate}}

+

{{'TASK_DETAILS.ERROR.DESCRIPTION'|translate}}

+ +
+
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": {