[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
This commit is contained in:
Silviu Popa
2019-03-18 14:34:08 +02:00
committed by Eugenio Romano
parent e586ee17d5
commit 791051edee
20 changed files with 654 additions and 82 deletions

View File

@@ -64,7 +64,7 @@ import { ContentModule } from '@alfresco/adf-content-services';
import { InsightsModule } from '@alfresco/adf-insights'; import { InsightsModule } from '@alfresco/adf-insights';
import { ProcessModule } from '@alfresco/adf-process-services'; import { ProcessModule } from '@alfresco/adf-process-services';
import { AuthBearerInterceptor } from './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 { TreeViewSampleComponent } from './components/tree-view/tree-view-sample.component';
import { CloudLayoutComponent } from './components/app-layout/cloud/cloud-layout.component'; import { CloudLayoutComponent } from './components/app-layout/cloud/cloud-layout.component';
import { AppsCloudDemoComponent } from './components/app-layout/cloud/apps-cloud-demo.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, ThemePickerModule,
ChartsModule, ChartsModule,
MonacoEditorModule.forRoot(), MonacoEditorModule.forRoot(),
GroupCloudModule ProcessServicesCloudModule,
GroupCloudModule,
TaskDirectiveModule
], ],
declarations: [ declarations: [
AppComponent, AppComponent,

View File

@@ -1,12 +1,13 @@
<button data-automation-id="go-back" mat-icon-button (click)="onGoBack()">
<mat-icon>arrow_back</mat-icon> Go Back
</button>
<h4 data-automation-id="task-details-header">Simple page to show the taskId: {{ taskId }} of the app: {{ appName }}</h4> <h4 data-automation-id="task-details-header">Simple page to show the taskId: {{ taskId }} of the app: {{ appName }}</h4>
<adf-cloud-task-header <div class="adf-task-detail-container">
[appName]="appName" <div class="adf-task-control">
[taskId]="taskId" <button mat-button (click)="goBack()">Cancel</button>
[readOnly]="readOnly"> <button mat-button color="primary" *ngIf="canCompleteTask()" adf-cloud-complete-task [appName]="appName" [taskId]="taskId"
(success)="onCompletedTask($event)">{{ 'ADF_TASK_LIST.DETAILS.BUTTON.COMPLETE' | translate }}</button>
</div>
<adf-cloud-task-header class="adf-demop-card-container" [appName]="appName" [taskId]="taskId" [readOnly]="readOnly">
</adf-cloud-task-header> </adf-cloud-task-header>
</div>

View File

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

View File

@@ -15,20 +15,26 @@
* limitations under the License. * limitations under the License.
*/ */
import { Component } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { TaskCloudService, TaskDetailsCloudModel } from '@alfresco/adf-process-services-cloud';
@Component({ @Component({
templateUrl: './task-details-cloud-demo.component.html', templateUrl: './task-details-cloud-demo.component.html',
styleUrls: ['./task-details-cloud-demo.component.scss'] styleUrls: ['./task-details-cloud-demo.component.scss']
}) })
export class TaskDetailsCloudDemoComponent { export class TaskDetailsCloudDemoComponent implements OnInit {
taskDetails: TaskDetailsCloudModel;
taskId: string; taskId: string;
appName: string; appName: string;
readOnly = false; 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.route.params.subscribe((params) => {
this.taskId = params.taskId; this.taskId = params.taskId;
}); });
@@ -37,8 +43,30 @@ export class TaskDetailsCloudDemoComponent {
}); });
} }
onGoBack() { ngOnInit() {
this.router.navigate([`/cloud/${this.appName}/`]); 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();
} }
} }

View File

@@ -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
<button adf-cloud-complete-task [appName]="appName" [taskId]="taskId" (success)="onTaskCompleted()">Complete</button>
```
## 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<any>` | empty | Emitted when the task is completed. |
| error | `EventEmitter<any>` | empty | Emitted when the task cannot be completed. |

View File

@@ -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`)<br/>
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))<br/>
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`<br/>
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)`>`<br/>
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`<br/>
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`<br/>
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)

View File

@@ -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: `<button adf-cloud-complete-task [taskId]='taskMock' [appName]='appNameMock' (success)="onCompleteTask($event)"></button>`
})
class TestComponent {
taskMock = 'test1234';
appNameMock = 'simple-app';
@ViewChild(CompleteTaskDirective)
completeTaskDirective: CompleteTaskDirective;
onCompleteTask(event: any) {
return event;
}
}
let fixture: ComponentFixture<TestComponent>;
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: '<button adf-cloud-complete-task (success)="onCompleteTask($event)"></button>'
})
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: '<button adf-cloud-complete-task [appName]="appName" (success)="onCompleteTask($event)"></button>'
})
class TestMissingTaskIdDirectiveComponent {
appName = 'simple-app';
@ContentChildren(CompleteTaskDirective)
completeTaskValidationDirective: CompleteTaskDirective;
onCompleteTask(event: any) {
return event;
}
}
@Component({
selector: 'adf-undefined-appname-component',
template: '<button adf-cloud-complete-task [taskId]="taskMock" [appName]="appNameUndefined" (success)="onCompleteTask($event)"></button>'
})
class TestInvalidAppNameUndefineddDirectiveComponent {
appName = 'simple-app';
taskMock = 'test1234';
@ContentChildren(CompleteTaskDirective)
completeTaskValidationDirective: CompleteTaskDirective;
onCompleteTask(event: any) {
return event;
}
}
@Component({
selector: 'adf-null-appname-component',
template: '<button adf-cloud-complete-task [taskId]="taskMock" [appName]="appNameNull" (success)="onCompleteTask($event)"></button>'
})
class TestInvalidAppNameNulldDirectiveComponent {
appName = 'simple-app';
taskMock = 'test1234';
@ContentChildren(CompleteTaskDirective)
completeTaskValidationDirective: CompleteTaskDirective;
onCompleteTask(event: any) {
return event;
}
}
let fixture: ComponentFixture<any>;
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');
});
});

View File

@@ -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<any> = new EventEmitter<any>();
/** Emitted when the task cannot be completed. */
@Output()
error: EventEmitter<any> = new EventEmitter<any>();
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);
}
}
}

View File

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

View File

@@ -21,3 +21,4 @@ export * from './start-task/public-api';
export * from './task-header/public-api'; export * from './task-header/public-api';
export * from './task-cloud.module'; export * from './task-cloud.module';
export * from './directives/task-directive.module';

View File

@@ -36,7 +36,7 @@ export class TaskDetailsCloudModel {
priority: number; priority: number;
processDefinitionId: string; processDefinitionId: string;
processInstanceId: string; processInstanceId: string;
status: string; status: TaskStatusEnum;
standAlone: boolean; standAlone: boolean;
candidateUsers: string[]; candidateUsers: string[];
candidateGroups: string[]; candidateGroups: string[];
@@ -73,8 +73,21 @@ export class TaskDetailsCloudModel {
this.memberOfCandidateUsers = obj.memberOfCandidateUsers || null; this.memberOfCandidateUsers = obj.memberOfCandidateUsers || null;
} }
} }
isCompleted() {
return this.status && this.status === TaskStatusEnum.COMPLETED;
}
} }
export interface StartTaskCloudResponseModel { export interface StartTaskCloudResponseModel {
entry: TaskDetailsCloudModel; entry: TaskDetailsCloudModel;
} }
export enum TaskStatusEnum {
COMPLETED= 'COMPLETED',
DELETED = 'DELETED',
CREATED = 'CREATED',
ASSIGNED = 'ASSIGNED',
SUSPENDED = 'SUSPENDED',
CANCELLED = 'CANCELLED'
}

View File

@@ -1,8 +1,11 @@
<h3 class="adf-task-tiitle">{{ taskDetails.name }}</h3>
<div class="adf-task-header-container">
<mat-card *ngIf="isTaskValid()" class="adf-card-container"> <mat-card *ngIf="isTaskValid()" class="adf-card-container">
<mat-card-content> <mat-card-content>
<adf-card-view <adf-card-view
[properties]="properties" [properties]="properties"
[editable]="!isCompleted()"> [editable]="!taskDetails.isCompleted()">
</adf-card-view> </adf-card-view>
</mat-card-content> </mat-card-content>
@@ -13,3 +16,5 @@
</button> </button>
</mat-card-actions> </mat-card-actions>
</mat-card> </mat-card>
</div>

View File

@@ -22,11 +22,6 @@
&-claim-controls { &-claim-controls {
color: rgb(131, 131, 131); color: rgb(131, 131, 131);
} }
&-card-container {
font-family: inherit;
}
} }
@media screen and ($mat-small) { @media screen and ($mat-small) {
@@ -36,5 +31,4 @@
white-space: nowrap; white-space: nowrap;
} }
} }
} }

View File

@@ -21,20 +21,22 @@ import { TaskHeaderCloudComponent } from './task-header-cloud.component';
import { taskDetailsCloudMock } from '../mocks/task-details-cloud.mock'; import { taskDetailsCloudMock } from '../mocks/task-details-cloud.mock';
import { TaskHeaderCloudModule } from '../task-header-cloud.module'; import { TaskHeaderCloudModule } from '../task-header-cloud.module';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { TaskHeaderCloudService } from '../services/task-header-cloud.service';
import { of } from 'rxjs'; import { of } from 'rxjs';
import { ProcessServiceCloudTestingModule } from '../../../testing/process-service-cloud.testing.module'; 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 component: TaskHeaderCloudComponent;
let fixture: ComponentFixture<TaskHeaderCloudComponent>; let fixture: ComponentFixture<TaskHeaderCloudComponent>;
let service: TaskHeaderCloudService; let service: TaskCloudService;
let appConfigService: AppConfigService; let appConfigService: AppConfigService;
setupTestBed({ setupTestBed({
imports: [ imports: [
ProcessServiceCloudTestingModule, ProcessServiceCloudTestingModule,
TaskHeaderCloudModule TaskHeaderCloudModule,
RouterTestingModule
] ]
}); });
@@ -43,7 +45,7 @@ describe('TaskHeaderComponent', () => {
component = fixture.componentInstance; component = fixture.componentInstance;
component.appName = 'myApp'; component.appName = 'myApp';
component.taskId = taskDetailsCloudMock.id; component.taskId = taskDetailsCloudMock.id;
service = TestBed.get(TaskHeaderCloudService); service = TestBed.get(TaskCloudService);
appConfigService = TestBed.get(AppConfigService); appConfigService = TestBed.get(AppConfigService);
spyOn(service, 'getTaskById').and.returnValue(of(taskDetailsCloudMock)); spyOn(service, 'getTaskById').and.returnValue(of(taskDetailsCloudMock));
}); });
@@ -52,7 +54,7 @@ describe('TaskHeaderComponent', () => {
component.appName = undefined; component.appName = undefined;
component.taskId = undefined; component.taskId = undefined;
fixture.detectChanges(); fixture.detectChanges();
expect(fixture.debugElement.children.length).toBe(0); expect(fixture.debugElement.children.length).toBe(2);
})); }));
it('should display assignee', async(() => { it('should display assignee', async(() => {

View File

@@ -25,10 +25,10 @@ import {
AppConfigService, AppConfigService,
UpdateNotification, UpdateNotification,
CardViewUpdateService, CardViewUpdateService,
StorageService IdentityUserService
} from '@alfresco/adf-core'; } from '@alfresco/adf-core';
import { TaskHeaderCloudService } from '../services/task-header-cloud.service';
import { TaskDetailsCloudModel } from '../../start-task/models/task-details-cloud.model'; import { TaskDetailsCloudModel } from '../../start-task/models/task-details-cloud.model';
import { TaskCloudService } from '../services/task-cloud.service';
@Component({ @Component({
selector: 'adf-cloud-task-header', selector: 'adf-cloud-task-header',
@@ -64,11 +64,11 @@ export class TaskHeaderCloudComponent implements OnInit {
private currentUser: string; private currentUser: string;
constructor( constructor(
private taskHeaderCloudService: TaskHeaderCloudService, private taskCloudService: TaskCloudService,
private translationService: TranslationService, private translationService: TranslationService,
private appConfig: AppConfigService, private appConfig: AppConfigService,
private cardViewUpdateService: CardViewUpdateService, private cardViewUpdateService: CardViewUpdateService,
private storage: StorageService private identityUserService: IdentityUserService
) { } ) { }
ngOnInit() { ngOnInit() {
@@ -79,12 +79,13 @@ export class TaskHeaderCloudComponent implements OnInit {
this.cardViewUpdateService.itemUpdated$.subscribe(this.updateTaskDetails.bind(this)); this.cardViewUpdateService.itemUpdated$.subscribe(this.updateTaskDetails.bind(this));
} }
loadCurrentBpmUserId(): any { loadCurrentBpmUserId(): any {
this.currentUser = this.storage.getItem('USERNAME'); this.currentUser = this.identityUserService.getCurrentUserInfo().username;
} }
loadTaskDetailsById(appName: string, taskId: string): any { loadTaskDetailsById(appName: string, taskId: string): any {
this.taskHeaderCloudService.getTaskById(appName, taskId).subscribe( this.taskCloudService.getTaskById(appName, taskId).subscribe(
(taskDetails) => { (taskDetails) => {
this.taskDetails = taskDetails; this.taskDetails = taskDetails;
if (this.taskDetails.parentTaskId) { if (this.taskDetails.parentTaskId) {
@@ -204,7 +205,7 @@ export class TaskHeaderCloudComponent implements OnInit {
* @param updateNotification * @param updateNotification
*/ */
private updateTaskDetails(updateNotification: UpdateNotification) { private updateTaskDetails(updateNotification: UpdateNotification) {
this.taskHeaderCloudService.updateTask(this.appName, this.taskId, updateNotification.changed) this.taskCloudService.updateTask(this.appName, this.taskId, updateNotification.changed)
.subscribe( .subscribe(
(taskDetails) => { (taskDetails) => {
this.taskDetails = taskDetails; this.taskDetails = taskDetails;
@@ -214,7 +215,7 @@ export class TaskHeaderCloudComponent implements OnInit {
} }
private loadParentName(taskId) { private loadParentName(taskId) {
this.taskHeaderCloudService.getTaskById(this.appName, taskId) this.taskCloudService.getTaskById(this.appName, taskId)
.subscribe( .subscribe(
(taskDetails) => { (taskDetails) => {
this.parentTaskName = taskDetails.name; this.parentTaskName = taskDetails.name;
@@ -223,10 +224,6 @@ export class TaskHeaderCloudComponent implements OnInit {
); );
} }
isCompleted() {
return this.taskDetails && this.taskDetails.status === 'completed';
}
isTaskClaimable(): boolean { isTaskClaimable(): boolean {
return !this.hasAssignee() && this.isCandidateMember(); return !this.hasAssignee() && this.isCandidateMember();
} }
@@ -240,7 +237,7 @@ export class TaskHeaderCloudComponent implements OnInit {
} }
isTaskClaimedByCandidateMember(): boolean { isTaskClaimedByCandidateMember(): boolean {
return this.isCandidateMember() && this.isAssignedToCurrentUser() && !this.isCompleted(); return this.isCandidateMember() && this.isAssignedToCurrentUser() && !this.taskDetails.isCompleted();
} }
isAssignedToCurrentUser(): boolean { isAssignedToCurrentUser(): boolean {
@@ -260,7 +257,7 @@ export class TaskHeaderCloudComponent implements OnInit {
} }
claimTask() { claimTask() {
this.taskHeaderCloudService.claimTask(this.appName, this.taskId, this.currentUser).subscribe( this.taskCloudService.claimTask(this.appName, this.taskId, this.currentUser).subscribe(
(res: any) => { (res: any) => {
this.loadTaskDetailsById(this.appName, this.taskId); this.loadTaskDetailsById(this.appName, this.taskId);
this.claim.emit(this.taskId); this.claim.emit(this.taskId);
@@ -268,7 +265,7 @@ export class TaskHeaderCloudComponent implements OnInit {
} }
unclaimTask() { unclaimTask() {
this.taskHeaderCloudService.unclaimTask(this.appName, this.taskId).subscribe( this.taskCloudService.unclaimTask(this.appName, this.taskId).subscribe(
() => { () => {
this.loadTaskDetailsById(this.appName, this.taskId); this.loadTaskDetailsById(this.appName, this.taskId);
this.unclaim.emit(this.taskId); this.unclaim.emit(this.taskId);

View File

@@ -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'
}
};

View File

@@ -17,4 +17,6 @@
export * from './components/task-header-cloud.component'; export * from './components/task-header-cloud.component';
export * from './services/task-cloud.service';
export * from './task-header-cloud.module'; export * from './task-header-cloud.module';

View File

@@ -15,16 +15,42 @@
* limitations under the License. * limitations under the License.
*/ */
import { async } from '@angular/core/testing'; import { async, TestBed } from '@angular/core/testing';
import { setupTestBed } from '@alfresco/adf-core'; import { setupTestBed, IdentityUserService, IdentityUserModel, AlfrescoApiServiceMock } from '@alfresco/adf-core';
import { AlfrescoApiServiceMock, LogService, AppConfigService, StorageService, CoreModule } 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 { 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 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() { function returnFakeTaskDetailsResults() {
return { return {
@@ -39,16 +65,53 @@ describe('Task Header Cloud Service', () => {
setupTestBed({ setupTestBed({
imports: [ imports: [
CoreModule.forRoot() CoreModule.forRoot()
] ],
providers: [IdentityUserService, LogService]
}); });
beforeEach(async(() => { beforeEach(async(() => {
identityService = TestBed.get(IdentityUserService);
getCurrentUserInfoStub = spyOn(identityService, 'getCurrentUserInfo');
getCurrentUserInfoStub.and.returnValue(fakeIdentityUser);
alfrescoApiMock = new AlfrescoApiServiceMock(new AppConfigService(null), new StorageService() ); alfrescoApiMock = new AlfrescoApiServiceMock(new AppConfigService(null), new StorageService() );
service = new TaskHeaderCloudService(alfrescoApiMock, service = new TaskCloudService(alfrescoApiMock,
new AppConfigService(null), 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) => { it('should return the task details when querying by id', (done) => {
const appName = 'taskp-app'; const appName = 'taskp-app';
const taskId = '68d54a8f'; const taskId = '68d54a8f';

View File

@@ -15,27 +15,69 @@
* limitations under the License. * limitations under the License.
*/ */
import { AlfrescoApiService, LogService, AppConfigService } from '@alfresco/adf-core';
import { Injectable } from '@angular/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 { catchError, map } from 'rxjs/operators';
import { TaskDetailsCloudModel } from '../../start-task/models/task-details-cloud.model'; import { TaskDetailsCloudModel } from '../../start-task/models/task-details-cloud.model';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class TaskHeaderCloudService { export class TaskCloudService {
contextRoot: string; contextRoot: string;
contentTypes = ['application/json']; contentTypes = ['application/json'];
accepts = ['application/json']; accepts = ['application/json'];
returnType = Object; returnType = Object;
constructor(private alfrescoApiService: AlfrescoApiService, constructor(
private apiService: AlfrescoApiService,
private appConfigService: AppConfigService, private appConfigService: AppConfigService,
private logService: LogService) { private logService: LogService,
private identityUserService: IdentityUserService
) {
this.contextRoot = this.appConfigService.get('bpmHost', ''); 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. * Gets details of a task.
* @param appName Name of the app * @param appName Name of the app
@@ -46,7 +88,7 @@ export class TaskHeaderCloudService {
if (appName && taskId) { if (appName && taskId) {
let queryUrl = `${this.contextRoot}/${appName}-query/v1/tasks/${taskId}`; let queryUrl = `${this.contextRoot}/${appName}-query/v1/tasks/${taskId}`;
return from(this.alfrescoApiService.getInstance() return from(this.apiService.getInstance()
.oauth2Auth.callCustomApi(queryUrl, 'GET', .oauth2Auth.callCustomApi(queryUrl, 'GET',
null, null, null, null, null, null,
null, null, null, null,
@@ -77,7 +119,7 @@ export class TaskHeaderCloudService {
updatePayload.payloadType = 'UpdateTaskPayload'; updatePayload.payloadType = 'UpdateTaskPayload';
let queryUrl = `${this.contextRoot}/${appName}-rb/v1/tasks/${taskId}`; let queryUrl = `${this.contextRoot}/${appName}-rb/v1/tasks/${taskId}`;
return from(this.alfrescoApiService.getInstance() return from(this.apiService.getInstance()
.oauth2Auth.callCustomApi(queryUrl, 'PUT', .oauth2Auth.callCustomApi(queryUrl, 'PUT',
null, null, null, null, null, null,
null, updatePayload, null, updatePayload,
@@ -106,7 +148,7 @@ export class TaskHeaderCloudService {
if (appName && taskId) { if (appName && taskId) {
let queryUrl = `${this.contextRoot}/${appName}-rb/v1/tasks/${taskId}/claim?assignee=${assignee}`; 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', .oauth2Auth.callCustomApi(queryUrl, 'POST',
null, null, null, null, null, null,
null, null, null, null,
@@ -134,7 +176,7 @@ export class TaskHeaderCloudService {
if (appName && taskId) { if (appName && taskId) {
let queryUrl = `${this.contextRoot}/${appName}-rb/v1/tasks/${taskId}/release`; let queryUrl = `${this.contextRoot}/${appName}-rb/v1/tasks/${taskId}/release`;
return from(this.alfrescoApiService.getInstance() return from(this.apiService.getInstance()
.oauth2Auth.callCustomApi(queryUrl, 'POST', .oauth2Auth.callCustomApi(queryUrl, 'POST',
null, null, null, null, null, null,
null, null, null, null,

View File

@@ -20,7 +20,6 @@ import { CommonModule } from '@angular/common';
import { MaterialModule } from '../../material.module'; import { MaterialModule } from '../../material.module';
import { DataTableModule, TemplateModule, CardViewModule, CoreModule } from '@alfresco/adf-core'; import { DataTableModule, TemplateModule, CardViewModule, CoreModule } from '@alfresco/adf-core';
import { TaskHeaderCloudComponent } from './components/task-header-cloud.component'; import { TaskHeaderCloudComponent } from './components/task-header-cloud.component';
import { TaskHeaderCloudService } from './services/task-header-cloud.service';
@NgModule({ @NgModule({
imports: [ imports: [
@@ -36,9 +35,6 @@ import { TaskHeaderCloudService } from './services/task-header-cloud.service';
], ],
exports: [ exports: [
TaskHeaderCloudComponent TaskHeaderCloudComponent
],
providers: [
TaskHeaderCloudService
] ]
}) })
export class TaskHeaderCloudModule { } export class TaskHeaderCloudModule { }