From 791051edee2327a970c3d69bfeb4d39c320162a8 Mon Sep 17 00:00:00 2001 From: Silviu Popa Date: Mon, 18 Mar 2019 14:34:08 +0200 Subject: [PATCH] [ADF-4128] ProcessCloud - add complete task directive (#4364) * [ADF-4128] ProcessCloud - add complete task directive * [ADF-4128] ProcessCloud - fix completion functionality and add documentation * [ADF-4128] ProcessCloud - PR changes * [ADF-4128] ProcessCloud - lint * [ADF-4148] ProcessServicesCloud - change layout * [ADF-4128] fix PR changes * [ADF-4128] - refractor complete task directive * [ADF-4128] - fix lint * [ADF-4128] - PR changes * [ADF-4128] - replace isCompleted method from service with model method * [ADF-4128] fix unit tests * [ADF-4128] - change travis yml * [ADF-4128] - fix travis.yml * [ADF-4128] - travis lint --- demo-shell/src/app/app.module.ts | 6 +- .../task-details-cloud-demo.component.html | 21 +- .../task-details-cloud-demo.component.scss | 20 ++ .../task-details-cloud-demo.component.ts | 38 +++- .../complete-task.directive.md | 26 +++ .../task-cloud.service.md | 57 ++++++ .../complete-task.directive.spec.ts | 181 ++++++++++++++++++ .../directives/complete-task.directive.ts | 82 ++++++++ .../task/directives/task-directive.module.ts | 29 +++ .../src/lib/task/public-api.ts | 1 + .../models/task-details-cloud.model.ts | 15 +- .../task-header-cloud.component.html | 33 ++-- .../task-header-cloud.component.scss | 6 - .../task-header-cloud.component.spec.ts | 14 +- .../components/task-header-cloud.component.ts | 27 ++- .../mocks/fake-complete-task.mock.ts | 31 +++ .../src/lib/task/task-header/public-api.ts | 2 + ...ice.spec.ts => task-cloud.service.spec.ts} | 81 +++++++- ...cloud.service.ts => task-cloud.service.ts} | 62 +++++- .../task-header/task-header-cloud.module.ts | 4 - 20 files changed, 654 insertions(+), 82 deletions(-) create mode 100644 docs/process-services-cloud/complete-task.directive.md create mode 100644 docs/process-services-cloud/task-cloud.service.md create mode 100644 lib/process-services-cloud/src/lib/task/directives/complete-task.directive.spec.ts create mode 100644 lib/process-services-cloud/src/lib/task/directives/complete-task.directive.ts create mode 100644 lib/process-services-cloud/src/lib/task/directives/task-directive.module.ts create mode 100644 lib/process-services-cloud/src/lib/task/task-header/mocks/fake-complete-task.mock.ts rename lib/process-services-cloud/src/lib/task/task-header/services/{task-header-cloud.service.spec.ts => task-cloud.service.spec.ts} (76%) rename lib/process-services-cloud/src/lib/task/task-header/services/{task-header-cloud.service.ts => task-cloud.service.ts} (71%) diff --git a/demo-shell/src/app/app.module.ts b/demo-shell/src/app/app.module.ts index adbb66ad7c..376d429c25 100644 --- a/demo-shell/src/app/app.module.ts +++ b/demo-shell/src/app/app.module.ts @@ -64,7 +64,7 @@ import { ContentModule } from '@alfresco/adf-content-services'; import { InsightsModule } from '@alfresco/adf-insights'; import { ProcessModule } from '@alfresco/adf-process-services'; import { AuthBearerInterceptor } from './services'; -import { ProcessServicesCloudModule, GroupCloudModule } from '@alfresco/adf-process-services-cloud'; +import { ProcessServicesCloudModule, GroupCloudModule, TaskDirectiveModule } from '@alfresco/adf-process-services-cloud'; import { TreeViewSampleComponent } from './components/tree-view/tree-view-sample.component'; import { CloudLayoutComponent } from './components/app-layout/cloud/cloud-layout.component'; import { AppsCloudDemoComponent } from './components/app-layout/cloud/apps-cloud-demo.component'; @@ -102,7 +102,9 @@ import { NestedMenuPositionDirective } from './components/app-layout/cloud/direc ThemePickerModule, ChartsModule, MonacoEditorModule.forRoot(), - GroupCloudModule + ProcessServicesCloudModule, + GroupCloudModule, + TaskDirectiveModule ], declarations: [ AppComponent, diff --git a/demo-shell/src/app/components/app-layout/cloud/task-details-cloud-demo.component.html b/demo-shell/src/app/components/app-layout/cloud/task-details-cloud-demo.component.html index c3f4a4d399..2a72d8d545 100644 --- a/demo-shell/src/app/components/app-layout/cloud/task-details-cloud-demo.component.html +++ b/demo-shell/src/app/components/app-layout/cloud/task-details-cloud-demo.component.html @@ -1,12 +1,13 @@ - - -

Simple page to show the taskId: {{ taskId }} of the app: {{ appName }}

- - +
+
+ + +
+ + + + +
\ No newline at end of file diff --git a/demo-shell/src/app/components/app-layout/cloud/task-details-cloud-demo.component.scss b/demo-shell/src/app/components/app-layout/cloud/task-details-cloud-demo.component.scss index e69de29bb2..e97ca949e4 100644 --- a/demo-shell/src/app/components/app-layout/cloud/task-details-cloud-demo.component.scss +++ b/demo-shell/src/app/components/app-layout/cloud/task-details-cloud-demo.component.scss @@ -0,0 +1,20 @@ + +.adf { + + &-task-detail-container { + display: flex; + } + + &-task-tiitle { + margin-left:15px; + } + + &-task-control { + width:70%; + } + + &-demop-card-container { + width:30%; + font-family: inherit; + } +} diff --git a/demo-shell/src/app/components/app-layout/cloud/task-details-cloud-demo.component.ts b/demo-shell/src/app/components/app-layout/cloud/task-details-cloud-demo.component.ts index c5ade39624..6838200b2e 100644 --- a/demo-shell/src/app/components/app-layout/cloud/task-details-cloud-demo.component.ts +++ b/demo-shell/src/app/components/app-layout/cloud/task-details-cloud-demo.component.ts @@ -15,20 +15,26 @@ * limitations under the License. */ -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; +import { TaskCloudService, TaskDetailsCloudModel } from '@alfresco/adf-process-services-cloud'; @Component({ templateUrl: './task-details-cloud-demo.component.html', styleUrls: ['./task-details-cloud-demo.component.scss'] }) -export class TaskDetailsCloudDemoComponent { +export class TaskDetailsCloudDemoComponent implements OnInit { + taskDetails: TaskDetailsCloudModel; taskId: string; appName: string; readOnly = false; - constructor(private route: ActivatedRoute, private router: Router) { + constructor( + private route: ActivatedRoute, + private router: Router, + private taskCloudService: TaskCloudService + ) { this.route.params.subscribe((params) => { this.taskId = params.taskId; }); @@ -37,8 +43,30 @@ export class TaskDetailsCloudDemoComponent { }); } - onGoBack() { - this.router.navigate([`/cloud/${this.appName}/`]); + ngOnInit() { + this.loadTaskDetailsById(this.appName, this.taskId); + } + loadTaskDetailsById(appName: string, taskId: string): any { + this.taskCloudService.getTaskById(appName, taskId).subscribe( + (taskDetails) => { + this.taskDetails = taskDetails; + }); + } + + isTaskValid() { + return this.appName && this.taskId; + } + + canCompleteTask() { + return this.taskDetails && this.taskCloudService.canCompleteTask(this.taskDetails); + } + + goBack() { + this.router.navigate([`/cloud/${this.appName}/`]); + } + + onCompletedTask(evt: any) { + this.goBack(); } } diff --git a/docs/process-services-cloud/complete-task.directive.md b/docs/process-services-cloud/complete-task.directive.md new file mode 100644 index 0000000000..0ad4ad5856 --- /dev/null +++ b/docs/process-services-cloud/complete-task.directive.md @@ -0,0 +1,26 @@ +--- +Title: Complete Cloud Task +Added: v3.1.0 +Status: Experimental +Last reviewed: 2019-02-28 +--- + +# [Complete task directive](../../lib/process-services-cloud/src/lib/task/task-header/directives/complete-task.directive.ts "Defined in complete-task.directive.ts") + +Complete a task + +## Basic Usage + +```html + +``` +## Class members + +### Properties + +| Name | Type | Default value | Description | +| ---- | ---- | ------------- | ----------- | +| taskId | `string` | empty |(Required) The id of the task. | +| appName | `string` | empty | (Required) The name of the application. | +| success | `EventEmitter` | empty | Emitted when the task is completed. | +| error | `EventEmitter` | empty | Emitted when the task cannot be completed. | \ No newline at end of file diff --git a/docs/process-services-cloud/task-cloud.service.md b/docs/process-services-cloud/task-cloud.service.md new file mode 100644 index 0000000000..3c4a8968ad --- /dev/null +++ b/docs/process-services-cloud/task-cloud.service.md @@ -0,0 +1,57 @@ +--- +Title: Task Cloud Service +Added: v3.1.0 +Status: Experimental +Last reviewed: 2019-02-28 +--- + +# [Task Cloud Service](../../lib/process-services-cloud/src/lib/task/task-header/services/task-cloud.service.ts "Defined in task-cloud.service.ts") + +Manage task cloud. + +## Class members + +### Methods + +- **completeTask**(appName: `string`, taskId: `string`)
+ Complete a task + - _appName:_ `string` - Name of the app + - _taskId:_ `string` - ID of the task to complete + +- **canCompleteTask**(taskDetails: [`TaskDetailsCloudModel`](../../lib/process-services-cloud/src/lib/task/start-filters/models/task-details-cloud.model.ts))
+ Validate if a task can be completed. + - _taskDetails:_ [`TaskDetailsCloudModel`](../../lib/process-services-cloud/src/lib/task/start-filters/models/task-details-cloud.model.ts) - Task details object + +- **claimTask**(appName: `string`, taskId: `string`, assignee: `string`): `any`
+ Claims a task for an assignee. + - _appName:_ `string` - Name of the app + - _taskId:_ `string` - ID of the task to claim + - _assignee:_ `string` - User to assign the task to + - **Returns** `any` - Details of the claimed task +- **getTaskById**(appName: `string`, taskId: `string`): [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`TaskDetailsCloudModel`](../../lib/process-services-cloud/src/lib/task/start-task/models/task-details-cloud.model.ts)`>`
+ Gets details of a task. + - _appName:_ `string` - Name of the app + - _taskId:_ `string` - ID of the task whose details you want + - **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`TaskDetailsCloudModel`](../../lib/process-services-cloud/src/lib/task/start-task/models/task-details-cloud.model.ts)`>` - Task details +- **unclaimTask**(appName: `string`, taskId: `string`): `any`
+ Un-claims a task. + - _appName:_ `string` - Name of the app + - _taskId:_ `string` - ID of the task to unclaim + - **Returns** `any` - Details of the task that was unclaimed +- **updateTask**(appName: `string`, taskId: `string`, updatePayload: `any`): `any`
+ Updates the details (name, description, due date) for a task. + - _appName:_ `string` - Name of the app + - _taskId:_ `string` - ID of the task to update + - _updatePayload:_ `any` - Data to update the task + - **Returns** `any` - Updated task details + +## Details + +The methods work in much the same way as the equivalent methods in the +[Tasklist service](../process-services/tasklist.service.md) +but they use the cloud variants of the classes for return values. See the +[Tasklist service](../process-services/tasklist.service.md) page for usage examples. + +## See also + +- [Tasklist service](../process-services/tasklist.service.md) \ No newline at end of file diff --git a/lib/process-services-cloud/src/lib/task/directives/complete-task.directive.spec.ts b/lib/process-services-cloud/src/lib/task/directives/complete-task.directive.spec.ts new file mode 100644 index 0000000000..0d1fdc413c --- /dev/null +++ b/lib/process-services-cloud/src/lib/task/directives/complete-task.directive.spec.ts @@ -0,0 +1,181 @@ +/*! + * @license + * Copyright 2019 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, ViewChild, ContentChildren } from '@angular/core'; +import { CompleteTaskDirective } from './complete-task.directive'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { CoreModule, setupTestBed } from '@alfresco/adf-core'; +import { RouterTestingModule } from '@angular/router/testing'; +import { of } from 'rxjs'; +import { TaskCloudService } from '../task-header/services/task-cloud.service'; +import { taskCompleteCloudMock } from '../task-header/mocks/fake-complete-task.mock'; + +describe('CompleteTaskDirective', () => { + + @Component({ + selector: 'adf-test-component', + template: `` + }) + class TestComponent { + + taskMock = 'test1234'; + appNameMock = 'simple-app'; + + @ViewChild(CompleteTaskDirective) + completeTaskDirective: CompleteTaskDirective; + + onCompleteTask(event: any) { + return event; + } + } + + let fixture: ComponentFixture; + let taskCloudService: TaskCloudService; + + setupTestBed({ + imports: [ + CoreModule.forRoot(), + RouterTestingModule + ], + declarations: [ + TestComponent, + CompleteTaskDirective + ] + }); + + beforeEach(() => { + taskCloudService = TestBed.get(TaskCloudService); + fixture = TestBed.createComponent(TestComponent); + fixture.detectChanges(); + }); + + it('should directive complete task', () => { + spyOn(taskCloudService, 'completeTask').and.returnValue(of(taskCompleteCloudMock)); + const button = fixture.nativeElement.querySelector('button'); + button.click(); + expect(taskCloudService.completeTask).toHaveBeenCalled(); + }); +}); + +describe('Complete Task Directive validation errors', () => { + + @Component({ + selector: 'adf-no-fields-validation-component', + template: '' + }) + class TestMissingInputDirectiveComponent { + + appName = 'simple-app'; + appNameUndefined = undefined; + appNameNull = null; + + @ContentChildren(CompleteTaskDirective) + completeTaskValidationDirective: CompleteTaskDirective; + + onCompleteTask(event: any) { + return event; + } + } + + @Component({ + selector: 'adf-no-taskid-validation-component', + template: '' + }) + class TestMissingTaskIdDirectiveComponent { + + appName = 'simple-app'; + + @ContentChildren(CompleteTaskDirective) + completeTaskValidationDirective: CompleteTaskDirective; + + onCompleteTask(event: any) { + return event; + } + } + + @Component({ + selector: 'adf-undefined-appname-component', + template: '' + }) + class TestInvalidAppNameUndefineddDirectiveComponent { + + appName = 'simple-app'; + taskMock = 'test1234'; + + @ContentChildren(CompleteTaskDirective) + completeTaskValidationDirective: CompleteTaskDirective; + + onCompleteTask(event: any) { + return event; + } + } + + @Component({ + selector: 'adf-null-appname-component', + template: '' + }) + class TestInvalidAppNameNulldDirectiveComponent { + + appName = 'simple-app'; + taskMock = 'test1234'; + + @ContentChildren(CompleteTaskDirective) + completeTaskValidationDirective: CompleteTaskDirective; + + onCompleteTask(event: any) { + return event; + } + } + + let fixture: ComponentFixture; + + setupTestBed({ + imports: [ + CoreModule.forRoot(), + RouterTestingModule + ], + declarations: [ + TestMissingTaskIdDirectiveComponent, + TestInvalidAppNameUndefineddDirectiveComponent, + TestInvalidAppNameNulldDirectiveComponent, + TestMissingInputDirectiveComponent, + CompleteTaskDirective + ] + }); + + beforeEach(() => { + fixture = TestBed.createComponent(TestMissingInputDirectiveComponent); + }); + + it('should throw error when missing input', () => { + expect(() => fixture.detectChanges()).toThrowError(); + }); + + it('should throw error when taskId is not set', () => { + fixture = TestBed.createComponent(TestMissingTaskIdDirectiveComponent); + expect( () => fixture.detectChanges()).toThrowError('Attribute taskId is required'); + }); + + it('should throw error when appName is undefined', () => { + fixture = TestBed.createComponent(TestInvalidAppNameUndefineddDirectiveComponent); + expect( () => fixture.detectChanges()).toThrowError('Attribute appName is required'); + }); + + it('should throw error when appName is null', () => { + fixture = TestBed.createComponent(TestInvalidAppNameUndefineddDirectiveComponent); + expect( () => fixture.detectChanges()).toThrowError('Attribute appName is required'); + }); +}); diff --git a/lib/process-services-cloud/src/lib/task/directives/complete-task.directive.ts b/lib/process-services-cloud/src/lib/task/directives/complete-task.directive.ts new file mode 100644 index 0000000000..7d604d5bc7 --- /dev/null +++ b/lib/process-services-cloud/src/lib/task/directives/complete-task.directive.ts @@ -0,0 +1,82 @@ +/*! + * @license + * Copyright 2019 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 { Directive, Input, HostListener, Output, EventEmitter, OnInit } from '@angular/core'; +import { TaskCloudService } from '../task-header/services/task-cloud.service'; + +@Directive({ + selector: '[adf-cloud-complete-task]' +}) +export class CompleteTaskDirective implements OnInit { + + /** (Required) The id of the task. */ + @Input() + taskId: string; + + /** (Required) The name of the application. */ + @Input() + appName: string; + + /** Emitted when the task is completed. */ + @Output() + success: EventEmitter = new EventEmitter(); + + /** Emitted when the task cannot be completed. */ + @Output() + error: EventEmitter = new EventEmitter(); + + invalidParams: string[] = []; + + constructor(private taskListService: TaskCloudService) {} + + ngOnInit() { + this.validateInputs(); + } + + validateInputs() { + + if (!this.isTaskValid()) { + this.invalidParams.push('taskId'); + } + if (!this.isAppValid()) { + this.invalidParams.push('appName'); + } + if (this.invalidParams.length) { + throw new Error(`Attribute ${this.invalidParams.join(', ')} is required`); + } + } + + isTaskValid() { + return this.taskId && this.taskId.length > 0; + } + + isAppValid() { + return this.appName && this.appName.length > 0; + } + + @HostListener('click') + async onClick() { + try { + const result = await this.taskListService.completeTask(this.appName, this.taskId).toPromise(); + if (result) { + this.success.emit(result); + } + } catch (error) { + this.error.emit(error); + } + + } +} diff --git a/lib/process-services-cloud/src/lib/task/directives/task-directive.module.ts b/lib/process-services-cloud/src/lib/task/directives/task-directive.module.ts new file mode 100644 index 0000000000..8280bb9385 --- /dev/null +++ b/lib/process-services-cloud/src/lib/task/directives/task-directive.module.ts @@ -0,0 +1,29 @@ +/*! + * @license + * Copyright 2019 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 { NgModule } from '@angular/core'; +import { CompleteTaskDirective } from './complete-task.directive'; + +@NgModule({ + declarations: [ + CompleteTaskDirective + ], + exports: [ + CompleteTaskDirective + ] +}) +export class TaskDirectiveModule { } diff --git a/lib/process-services-cloud/src/lib/task/public-api.ts b/lib/process-services-cloud/src/lib/task/public-api.ts index d9950d290b..c2b1682a4e 100644 --- a/lib/process-services-cloud/src/lib/task/public-api.ts +++ b/lib/process-services-cloud/src/lib/task/public-api.ts @@ -21,3 +21,4 @@ export * from './start-task/public-api'; export * from './task-header/public-api'; export * from './task-cloud.module'; +export * from './directives/task-directive.module'; diff --git a/lib/process-services-cloud/src/lib/task/start-task/models/task-details-cloud.model.ts b/lib/process-services-cloud/src/lib/task/start-task/models/task-details-cloud.model.ts index 64003220f1..2668626b93 100644 --- a/lib/process-services-cloud/src/lib/task/start-task/models/task-details-cloud.model.ts +++ b/lib/process-services-cloud/src/lib/task/start-task/models/task-details-cloud.model.ts @@ -36,7 +36,7 @@ export class TaskDetailsCloudModel { priority: number; processDefinitionId: string; processInstanceId: string; - status: string; + status: TaskStatusEnum; standAlone: boolean; candidateUsers: string[]; candidateGroups: string[]; @@ -73,8 +73,21 @@ export class TaskDetailsCloudModel { this.memberOfCandidateUsers = obj.memberOfCandidateUsers || null; } } + + isCompleted() { + return this.status && this.status === TaskStatusEnum.COMPLETED; + } } export interface StartTaskCloudResponseModel { entry: TaskDetailsCloudModel; } + +export enum TaskStatusEnum { + COMPLETED= 'COMPLETED', + DELETED = 'DELETED', + CREATED = 'CREATED', + ASSIGNED = 'ASSIGNED', + SUSPENDED = 'SUSPENDED', + CANCELLED = 'CANCELLED' +} diff --git a/lib/process-services-cloud/src/lib/task/task-header/components/task-header-cloud.component.html b/lib/process-services-cloud/src/lib/task/task-header/components/task-header-cloud.component.html index 0bd14053ad..a16a0935e7 100644 --- a/lib/process-services-cloud/src/lib/task/task-header/components/task-header-cloud.component.html +++ b/lib/process-services-cloud/src/lib/task/task-header/components/task-header-cloud.component.html @@ -1,15 +1,20 @@ - - - - - +

{{ taskDetails.name }}

- - - - -
+
+ + + + + + + + + + + + +
diff --git a/lib/process-services-cloud/src/lib/task/task-header/components/task-header-cloud.component.scss b/lib/process-services-cloud/src/lib/task/task-header/components/task-header-cloud.component.scss index c59023f992..6f440a3cdc 100644 --- a/lib/process-services-cloud/src/lib/task/task-header/components/task-header-cloud.component.scss +++ b/lib/process-services-cloud/src/lib/task/task-header/components/task-header-cloud.component.scss @@ -22,11 +22,6 @@ &-claim-controls { color: rgb(131, 131, 131); } - - &-card-container { - font-family: inherit; - } - } @media screen and ($mat-small) { @@ -36,5 +31,4 @@ white-space: nowrap; } } - } diff --git a/lib/process-services-cloud/src/lib/task/task-header/components/task-header-cloud.component.spec.ts b/lib/process-services-cloud/src/lib/task/task-header/components/task-header-cloud.component.spec.ts index f7ed7ae81a..acf648d631 100644 --- a/lib/process-services-cloud/src/lib/task/task-header/components/task-header-cloud.component.spec.ts +++ b/lib/process-services-cloud/src/lib/task/task-header/components/task-header-cloud.component.spec.ts @@ -21,20 +21,22 @@ import { TaskHeaderCloudComponent } from './task-header-cloud.component'; import { taskDetailsCloudMock } from '../mocks/task-details-cloud.mock'; import { TaskHeaderCloudModule } from '../task-header-cloud.module'; import { By } from '@angular/platform-browser'; -import { TaskHeaderCloudService } from '../services/task-header-cloud.service'; import { of } from 'rxjs'; import { ProcessServiceCloudTestingModule } from '../../../testing/process-service-cloud.testing.module'; +import { RouterTestingModule } from '@angular/router/testing'; +import { TaskCloudService } from '../services/task-cloud.service'; -describe('TaskHeaderComponent', () => { +describe('TaskHeaderCloudComponent', () => { let component: TaskHeaderCloudComponent; let fixture: ComponentFixture; - let service: TaskHeaderCloudService; + let service: TaskCloudService; let appConfigService: AppConfigService; setupTestBed({ imports: [ ProcessServiceCloudTestingModule, - TaskHeaderCloudModule + TaskHeaderCloudModule, + RouterTestingModule ] }); @@ -43,7 +45,7 @@ describe('TaskHeaderComponent', () => { component = fixture.componentInstance; component.appName = 'myApp'; component.taskId = taskDetailsCloudMock.id; - service = TestBed.get(TaskHeaderCloudService); + service = TestBed.get(TaskCloudService); appConfigService = TestBed.get(AppConfigService); spyOn(service, 'getTaskById').and.returnValue(of(taskDetailsCloudMock)); }); @@ -52,7 +54,7 @@ describe('TaskHeaderComponent', () => { component.appName = undefined; component.taskId = undefined; fixture.detectChanges(); - expect(fixture.debugElement.children.length).toBe(0); + expect(fixture.debugElement.children.length).toBe(2); })); it('should display assignee', async(() => { diff --git a/lib/process-services-cloud/src/lib/task/task-header/components/task-header-cloud.component.ts b/lib/process-services-cloud/src/lib/task/task-header/components/task-header-cloud.component.ts index 812d3ca259..d53e3a8f00 100644 --- a/lib/process-services-cloud/src/lib/task/task-header/components/task-header-cloud.component.ts +++ b/lib/process-services-cloud/src/lib/task/task-header/components/task-header-cloud.component.ts @@ -25,10 +25,10 @@ import { AppConfigService, UpdateNotification, CardViewUpdateService, - StorageService + IdentityUserService } from '@alfresco/adf-core'; -import { TaskHeaderCloudService } from '../services/task-header-cloud.service'; import { TaskDetailsCloudModel } from '../../start-task/models/task-details-cloud.model'; +import { TaskCloudService } from '../services/task-cloud.service'; @Component({ selector: 'adf-cloud-task-header', @@ -64,11 +64,11 @@ export class TaskHeaderCloudComponent implements OnInit { private currentUser: string; constructor( - private taskHeaderCloudService: TaskHeaderCloudService, + private taskCloudService: TaskCloudService, private translationService: TranslationService, private appConfig: AppConfigService, private cardViewUpdateService: CardViewUpdateService, - private storage: StorageService + private identityUserService: IdentityUserService ) { } ngOnInit() { @@ -79,12 +79,13 @@ export class TaskHeaderCloudComponent implements OnInit { this.cardViewUpdateService.itemUpdated$.subscribe(this.updateTaskDetails.bind(this)); } + loadCurrentBpmUserId(): any { - this.currentUser = this.storage.getItem('USERNAME'); + this.currentUser = this.identityUserService.getCurrentUserInfo().username; } loadTaskDetailsById(appName: string, taskId: string): any { - this.taskHeaderCloudService.getTaskById(appName, taskId).subscribe( + this.taskCloudService.getTaskById(appName, taskId).subscribe( (taskDetails) => { this.taskDetails = taskDetails; if (this.taskDetails.parentTaskId) { @@ -204,7 +205,7 @@ export class TaskHeaderCloudComponent implements OnInit { * @param updateNotification */ private updateTaskDetails(updateNotification: UpdateNotification) { - this.taskHeaderCloudService.updateTask(this.appName, this.taskId, updateNotification.changed) + this.taskCloudService.updateTask(this.appName, this.taskId, updateNotification.changed) .subscribe( (taskDetails) => { this.taskDetails = taskDetails; @@ -214,7 +215,7 @@ export class TaskHeaderCloudComponent implements OnInit { } private loadParentName(taskId) { - this.taskHeaderCloudService.getTaskById(this.appName, taskId) + this.taskCloudService.getTaskById(this.appName, taskId) .subscribe( (taskDetails) => { this.parentTaskName = taskDetails.name; @@ -223,10 +224,6 @@ export class TaskHeaderCloudComponent implements OnInit { ); } - isCompleted() { - return this.taskDetails && this.taskDetails.status === 'completed'; - } - isTaskClaimable(): boolean { return !this.hasAssignee() && this.isCandidateMember(); } @@ -240,7 +237,7 @@ export class TaskHeaderCloudComponent implements OnInit { } isTaskClaimedByCandidateMember(): boolean { - return this.isCandidateMember() && this.isAssignedToCurrentUser() && !this.isCompleted(); + return this.isCandidateMember() && this.isAssignedToCurrentUser() && !this.taskDetails.isCompleted(); } isAssignedToCurrentUser(): boolean { @@ -260,7 +257,7 @@ export class TaskHeaderCloudComponent implements OnInit { } claimTask() { - this.taskHeaderCloudService.claimTask(this.appName, this.taskId, this.currentUser).subscribe( + this.taskCloudService.claimTask(this.appName, this.taskId, this.currentUser).subscribe( (res: any) => { this.loadTaskDetailsById(this.appName, this.taskId); this.claim.emit(this.taskId); @@ -268,7 +265,7 @@ export class TaskHeaderCloudComponent implements OnInit { } unclaimTask() { - this.taskHeaderCloudService.unclaimTask(this.appName, this.taskId).subscribe( + this.taskCloudService.unclaimTask(this.appName, this.taskId).subscribe( () => { this.loadTaskDetailsById(this.appName, this.taskId); this.unclaim.emit(this.taskId); diff --git a/lib/process-services-cloud/src/lib/task/task-header/mocks/fake-complete-task.mock.ts b/lib/process-services-cloud/src/lib/task/task-header/mocks/fake-complete-task.mock.ts new file mode 100644 index 0000000000..2ce3c42f31 --- /dev/null +++ b/lib/process-services-cloud/src/lib/task/task-header/mocks/fake-complete-task.mock.ts @@ -0,0 +1,31 @@ +/*! + * @license + * Copyright 2019 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 const taskCompleteCloudMock = { + 'entry': { + 'appName': 'simple-app', + 'appVersion': '', + 'serviceName': 'simple-app-rb', + 'serviceFullName': 'simple-app-rb', + 'serviceType': 'runtime-bundle', + 'serviceVersion': '', + 'id': '68d54a8f', + 'name': 'NXltAGmT', + 'priority': 0, + 'status': 'COMPLETED' + } +}; diff --git a/lib/process-services-cloud/src/lib/task/task-header/public-api.ts b/lib/process-services-cloud/src/lib/task/task-header/public-api.ts index 0be25b6c89..596020d265 100644 --- a/lib/process-services-cloud/src/lib/task/task-header/public-api.ts +++ b/lib/process-services-cloud/src/lib/task/task-header/public-api.ts @@ -17,4 +17,6 @@ export * from './components/task-header-cloud.component'; +export * from './services/task-cloud.service'; + export * from './task-header-cloud.module'; diff --git a/lib/process-services-cloud/src/lib/task/task-header/services/task-header-cloud.service.spec.ts b/lib/process-services-cloud/src/lib/task/task-header/services/task-cloud.service.spec.ts similarity index 76% rename from lib/process-services-cloud/src/lib/task/task-header/services/task-header-cloud.service.spec.ts rename to lib/process-services-cloud/src/lib/task/task-header/services/task-cloud.service.spec.ts index ec1bc12ae6..89b464d51d 100644 --- a/lib/process-services-cloud/src/lib/task/task-header/services/task-header-cloud.service.spec.ts +++ b/lib/process-services-cloud/src/lib/task/task-header/services/task-cloud.service.spec.ts @@ -15,16 +15,42 @@ * limitations under the License. */ -import { async } from '@angular/core/testing'; -import { setupTestBed } from '@alfresco/adf-core'; -import { AlfrescoApiServiceMock, LogService, AppConfigService, StorageService, CoreModule } from '@alfresco/adf-core'; +import { async, TestBed } from '@angular/core/testing'; +import { setupTestBed, IdentityUserService, IdentityUserModel, AlfrescoApiServiceMock } from '@alfresco/adf-core'; +import { LogService, AppConfigService, StorageService, CoreModule } from '@alfresco/adf-core'; +import { TaskCloudService } from './task-cloud.service'; +import { taskDetailsCloudMock } from '../mocks/task-details-cloud.mock'; +import { taskCompleteCloudMock } from '../mocks/fake-complete-task.mock'; import { fakeTaskDetailsCloud } from '../mocks/fake-task-details-response.mock'; -import { TaskHeaderCloudService } from './task-header-cloud.service'; -describe('Task Header Cloud Service', () => { +describe('Task Cloud Service', () => { - let service: TaskHeaderCloudService; + let service: TaskCloudService; let alfrescoApiMock: AlfrescoApiServiceMock; + let identityService: IdentityUserService; + let identityUserWithOutFirstNameMock = { firstName: null, lastName: 'fake-identity-last-name', email: 'fakeIdentity@email.com', username: 'superadminuser' }; + let getCurrentUserInfoStub; + let fakeIdentityUser: IdentityUserModel = new IdentityUserModel(identityUserWithOutFirstNameMock); + + function returnFakeTaskCompleteResults() { + return { + oauth2Auth: { + callCustomApi : () => { + return Promise.resolve(taskCompleteCloudMock); + } + } + }; + } + + function returnFakeTaskCompleteResultsError() { + return { + oauth2Auth: { + callCustomApi : () => { + return Promise.reject(taskCompleteCloudMock); + } + } + }; + } function returnFakeTaskDetailsResults() { return { @@ -39,16 +65,53 @@ describe('Task Header Cloud Service', () => { setupTestBed({ imports: [ CoreModule.forRoot() - ] + ], + providers: [IdentityUserService, LogService] }); beforeEach(async(() => { + + identityService = TestBed.get(IdentityUserService); + getCurrentUserInfoStub = spyOn(identityService, 'getCurrentUserInfo'); + getCurrentUserInfoStub.and.returnValue(fakeIdentityUser); alfrescoApiMock = new AlfrescoApiServiceMock(new AppConfigService(null), new StorageService() ); - service = new TaskHeaderCloudService(alfrescoApiMock, + service = new TaskCloudService(alfrescoApiMock, new AppConfigService(null), - new LogService(new AppConfigService(null))); + new LogService(new AppConfigService(null)), + identityService); + })); + it('should complete a task', (done) => { + const appName = 'simple-app'; + const taskId = '68d54a8f'; + spyOn(alfrescoApiMock, 'getInstance').and.callFake(returnFakeTaskCompleteResults); + service.completeTask(appName, taskId).subscribe((res: any) => { + expect(res).toBeDefined(); + expect(res).not.toBeNull(); + expect(res.entry.appName).toBe('simple-app'); + expect(res.entry.id).toBe('68d54a8f'); + done(); + }); + }); + + it('should not complete a task', (done) => { + spyOn(alfrescoApiMock, 'getInstance').and.callFake(returnFakeTaskCompleteResultsError); + const appName = 'simple-app'; + const taskId = '68d54a8f'; + + service.completeTask(appName, taskId).toPromise().then( (res: any) => { + }, (error) => { + expect(error).toBeDefined(); + done(); + }); + }); + + it('should canCompleteTask', () => { + const canCompleteTaskResult = service.canCompleteTask(taskDetailsCloudMock); + expect(canCompleteTaskResult).toBe(true); + }); + it('should return the task details when querying by id', (done) => { const appName = 'taskp-app'; const taskId = '68d54a8f'; diff --git a/lib/process-services-cloud/src/lib/task/task-header/services/task-header-cloud.service.ts b/lib/process-services-cloud/src/lib/task/task-header/services/task-cloud.service.ts similarity index 71% rename from lib/process-services-cloud/src/lib/task/task-header/services/task-header-cloud.service.ts rename to lib/process-services-cloud/src/lib/task/task-header/services/task-cloud.service.ts index 1f1a98b5d7..1c8bb1c25b 100644 --- a/lib/process-services-cloud/src/lib/task/task-header/services/task-header-cloud.service.ts +++ b/lib/process-services-cloud/src/lib/task/task-header/services/task-cloud.service.ts @@ -15,27 +15,69 @@ * limitations under the License. */ -import { AlfrescoApiService, LogService, AppConfigService } from '@alfresco/adf-core'; import { Injectable } from '@angular/core'; -import { Observable, from, throwError } from 'rxjs'; +import { AlfrescoApiService, LogService, AppConfigService, IdentityUserService } from '@alfresco/adf-core'; +import { from, throwError, Observable } from 'rxjs'; import { catchError, map } from 'rxjs/operators'; import { TaskDetailsCloudModel } from '../../start-task/models/task-details-cloud.model'; @Injectable({ providedIn: 'root' }) -export class TaskHeaderCloudService { +export class TaskCloudService { + contextRoot: string; contentTypes = ['application/json']; accepts = ['application/json']; returnType = Object; - constructor(private alfrescoApiService: AlfrescoApiService, - private appConfigService: AppConfigService, - private logService: LogService) { + constructor( + private apiService: AlfrescoApiService, + private appConfigService: AppConfigService, + private logService: LogService, + private identityUserService: IdentityUserService + ) { this.contextRoot = this.appConfigService.get('bpmHost', ''); } + /** + * Complete a task. + * @param appName Name of the app + * @param taskId ID of the task to complete + * @returns Details of the task that was completed + */ + completeTask(appName: string, taskId: string) { + const queryUrl = this.buildCompleteTaskUrl(appName, taskId); + const bodyParam = { 'payloadType': 'CompleteTaskPayload' }; + const pathParams = {}, queryParams = {}, headerParams = {}, + formParams = {}, contentTypes = ['application/json'], accepts = ['application/json']; + + return from( + this.apiService + .getInstance() + .oauth2Auth.callCustomApi( + queryUrl, 'POST', pathParams, queryParams, + headerParams, formParams, bodyParam, + contentTypes, accepts, null, null) + ).pipe( + catchError((err) => this.handleError(err)) + ); + } + + /** + * Validate if a task can be completed. + * @param taskDetails task details object + * @returns Boolean value if the task can be completed + */ + canCompleteTask(taskDetails: TaskDetailsCloudModel): boolean { + const currentUser = this.identityUserService.getCurrentUserInfo().username; + return taskDetails.owner === currentUser && !taskDetails.isCompleted(); + } + + private buildCompleteTaskUrl(appName: string, taskId: string): any { + return `${this.appConfigService.get('bpmHost')}/${appName}-rb/v1/tasks/${taskId}/complete`; + } + /** * Gets details of a task. * @param appName Name of the app @@ -46,7 +88,7 @@ export class TaskHeaderCloudService { if (appName && taskId) { let queryUrl = `${this.contextRoot}/${appName}-query/v1/tasks/${taskId}`; - return from(this.alfrescoApiService.getInstance() + return from(this.apiService.getInstance() .oauth2Auth.callCustomApi(queryUrl, 'GET', null, null, null, null, null, @@ -77,7 +119,7 @@ export class TaskHeaderCloudService { updatePayload.payloadType = 'UpdateTaskPayload'; let queryUrl = `${this.contextRoot}/${appName}-rb/v1/tasks/${taskId}`; - return from(this.alfrescoApiService.getInstance() + return from(this.apiService.getInstance() .oauth2Auth.callCustomApi(queryUrl, 'PUT', null, null, null, null, updatePayload, @@ -106,7 +148,7 @@ export class TaskHeaderCloudService { if (appName && taskId) { let queryUrl = `${this.contextRoot}/${appName}-rb/v1/tasks/${taskId}/claim?assignee=${assignee}`; - return from(this.alfrescoApiService.getInstance() + return from(this.apiService.getInstance() .oauth2Auth.callCustomApi(queryUrl, 'POST', null, null, null, null, null, @@ -134,7 +176,7 @@ export class TaskHeaderCloudService { if (appName && taskId) { let queryUrl = `${this.contextRoot}/${appName}-rb/v1/tasks/${taskId}/release`; - return from(this.alfrescoApiService.getInstance() + return from(this.apiService.getInstance() .oauth2Auth.callCustomApi(queryUrl, 'POST', null, null, null, null, null, diff --git a/lib/process-services-cloud/src/lib/task/task-header/task-header-cloud.module.ts b/lib/process-services-cloud/src/lib/task/task-header/task-header-cloud.module.ts index 6571c48891..f676d0bfd5 100644 --- a/lib/process-services-cloud/src/lib/task/task-header/task-header-cloud.module.ts +++ b/lib/process-services-cloud/src/lib/task/task-header/task-header-cloud.module.ts @@ -20,7 +20,6 @@ import { CommonModule } from '@angular/common'; import { MaterialModule } from '../../material.module'; import { DataTableModule, TemplateModule, CardViewModule, CoreModule } from '@alfresco/adf-core'; import { TaskHeaderCloudComponent } from './components/task-header-cloud.component'; -import { TaskHeaderCloudService } from './services/task-header-cloud.service'; @NgModule({ imports: [ @@ -36,9 +35,6 @@ import { TaskHeaderCloudService } from './services/task-header-cloud.service'; ], exports: [ TaskHeaderCloudComponent - ], - providers: [ - TaskHeaderCloudService ] }) export class TaskHeaderCloudModule { }