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 index 9f5bbfe99b..802b01d4f3 100644 --- a/ng2-components/ng2-activiti-tasklist/src/assets/task-details.mock.ts +++ b/ng2-components/ng2-activiti-tasklist/src/assets/task-details.mock.ts @@ -45,12 +45,122 @@ export let taskDetailsMock = new TaskDetailsModel({ 'adhocTaskCanBeReassigned': false, 'taskDefinitionKey': 'sid-DDECD9E4-0299-433F-9193-C3D905C3EEBE', 'executionId': '86', + 'involvedGroups': [], 'involvedPeople': [], 'memberOfCandidateUsers': false, 'managerOfCandidateGroup': false, 'memberOfCandidateGroup': false }); +export let completedTaskDetailsMock = new TaskDetailsModel({ + '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': '2016-11-03T15:25:42.749+0000', + 'duration': null, + 'priority': 50, + 'parentTaskId': null, + 'parentTaskName': null, + 'processInstanceId': '86', + 'processInstanceName': null, + 'processDefinitionId': 'TranslationProcess:2:8', + 'processDefinitionName': 'Translation Process', + 'involvedGroups': [], + 'involvedPeople': [] +}); + +export let taskDetailsWithOutAssigneeMock = new TaskDetailsModel({ + 'id': '91', + 'name': 'Request translation', + 'description': null, + 'category': null, + 'assignee': null, + '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', + 'involvedGroups': [{'id': 7007, 'name': 'group1', 'externalId': null, 'status': 'active', 'groups': null}, + {'id': 8008, 'name': 'group2', 'externalId': null, 'status': 'active', 'groups': null}], + 'involvedPeople': [] +}); + +export let taskDetailsWithInvolvedGroupMock = new TaskDetailsModel({ + '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', + 'involvedGroups': [{'id': 7007, 'name': 'group1', 'externalId': null, 'status': 'active', 'groups': null}, + {'id': 8008, 'name': 'group2', 'externalId': null, 'status': 'active', 'groups': null}], + 'involvedPeople': [] +}); + +export let taskDetailsWithInvolvedPeopleMock = new TaskDetailsModel({ + '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', + 'involvedGroups': [], + 'involvedPeople': [{'id': 1001, 'firstName': 'Wilbur', 'lastName': 'Adams', 'email': 'wilbur@app.activiti.com'}, + {'id': 111, 'firstName': 'fake-first-name', 'lastName': 'fake-last-name', 'email': 'fake@app.activiti.com'}] +}); + +export let taskDetailsWithAssigneeMock = new TaskDetailsModel({ + 'id': '91', + 'name': 'Request translation', + 'description': null, + 'category': null, + 'assignee': {'id': 111, 'firstName': 'fake-first-name', 'lastName': 'fake-last-name', 'email': 'fake@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', + 'involvedGroups': [], + 'involvedPeople': [{'id': 1001, 'firstName': 'Wilbur', 'lastName': 'Adams', 'email': 'wilbur@app.activiti.com'}, + {'id': 111, 'firstName': 'fake-first-name', 'lastName': 'fake-last-name', 'email': 'fake@app.activiti.com'}] +}); + export let taskFormMock = new TaskDetailsModel({ 'id': 4, 'name': 'Translation request', diff --git a/ng2-components/ng2-activiti-tasklist/src/components/task-header.component.html b/ng2-components/ng2-activiti-tasklist/src/components/task-header.component.html index 5b84d77316..f60a2e496a 100644 --- a/ng2-components/ng2-activiti-tasklist/src/components/task-header.component.html +++ b/ng2-components/ng2-activiti-tasklist/src/components/task-header.component.html @@ -4,9 +4,9 @@ - - diff --git a/ng2-components/ng2-activiti-tasklist/src/components/task-header.component.spec.ts b/ng2-components/ng2-activiti-tasklist/src/components/task-header.component.spec.ts index ccabd4fe14..bff14c1973 100644 --- a/ng2-components/ng2-activiti-tasklist/src/components/task-header.component.spec.ts +++ b/ng2-components/ng2-activiti-tasklist/src/components/task-header.component.spec.ts @@ -19,12 +19,20 @@ import { DebugElement } from '@angular/core'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { AppConfigService, CardViewUpdateService, CoreModule, TranslationService, UserProcessModel } from 'ng2-alfresco-core'; +import { BpmUserService } from 'ng2-alfresco-userinfo'; import { Observable } from 'rxjs/Rx'; import { AppConfigServiceMock } from '../assets/app-config.service.mock'; import { TranslationMock } from '../assets/translation.service.mock'; import { TaskDetailsModel } from '../models/task-details.model'; -import { taskDetailsMock } from './../assets/task-details.mock'; +import { + completedTaskDetailsMock, + taskDetailsMock, + taskDetailsWithAssigneeMock, + taskDetailsWithInvolvedGroupMock, + taskDetailsWithInvolvedPeopleMock, + taskDetailsWithOutAssigneeMock +} from './../assets/task-details.mock'; import { TaskListService } from './../services/tasklist.service'; import { TaskHeaderComponent } from './task-header.component'; @@ -34,6 +42,22 @@ describe('TaskHeaderComponent', () => { let component: TaskHeaderComponent; let fixture: ComponentFixture; let debugElement: DebugElement; + let userBpmService: BpmUserService; + let getCurrentUserInfoSpy: jasmine.Spy; + + let fakeBpmAssignedUser = { + id: 1001, + apps: [], + capabilities: 'fake-capability', + company: 'fake-company', + created: 'fake-create-date', + email: 'wilbur@app.activiti.com', + externalId: 'fake-external-id', + firstName: 'Wilbur', + lastName: 'Adams', + fullname: 'Wilbur Adams', + groups: [] + }; beforeEach(async(() => { TestBed.configureTestingModule({ @@ -45,20 +69,21 @@ describe('TaskHeaderComponent', () => { ], providers: [ TaskListService, + BpmUserService, CardViewUpdateService, { provide: AppConfigService, useClass: AppConfigServiceMock }, { provide: TranslationService, useClass: TranslationMock } ] }).compileComponents(); - })); beforeEach(() => { fixture = TestBed.createComponent(TaskHeaderComponent); component = fixture.componentInstance; service = TestBed.get(TaskListService); + userBpmService = TestBed.get(BpmUserService); debugElement = fixture.debugElement; - + getCurrentUserInfoSpy = spyOn(userBpmService, 'getCurrentUserInfo').and.returnValue(Observable.of(fakeBpmAssignedUser)); component.taskDetails = new TaskDetailsModel(taskDetailsMock); }); @@ -111,7 +136,7 @@ describe('TaskHeaderComponent', () => { describe('Claiming', () => { it('should display the claim button if no assignee', () => { - component.taskDetails.assignee = null; + component.taskDetails = new TaskDetailsModel(taskDetailsWithOutAssigneeMock); component.ngOnChanges({}); fixture.detectChanges(); @@ -119,44 +144,89 @@ describe('TaskHeaderComponent', () => { let claimButton = fixture.debugElement.query(By.css('[data-automation-id="header-claim-button"]')); expect(claimButton.nativeElement.innerText).toBe('ADF_TASK_LIST.DETAILS.BUTTON.CLAIM'); }); + + it('should display the claim button to the invovled group', () => { + component.taskDetails = new TaskDetailsModel(taskDetailsWithOutAssigneeMock); + component.ngOnChanges({}); + fixture.detectChanges(); + let claimButton = fixture.debugElement.query(By.css('[data-automation-id="header-claim-button"]')); + expect(claimButton.nativeElement.innerText).toBe('ADF_TASK_LIST.DETAILS.BUTTON.CLAIM'); + }); }); - describe('Unclaim', () => { + it('should display the requeue button if the current logged-in user is a part of the invovled group', () => { + component.taskDetails = new TaskDetailsModel(taskDetailsWithInvolvedGroupMock); + component.ngOnChanges({}); + fixture.detectChanges(); + let unclaimButton = fixture.debugElement.query(By.css('[data-automation-id="header-unclaim-button"]')); + expect(unclaimButton.nativeElement.innerText).toBe('ADF_TASK_LIST.DETAILS.BUTTON.UNCLAIM'); + }); + it('should display the requeue button if the current logged-in user is a part of the invovled people', () => { + component.taskDetails = new TaskDetailsModel(taskDetailsWithInvolvedPeopleMock); + component.ngOnChanges({}); + fixture.detectChanges(); + let unclaimButton = fixture.debugElement.query(By.css('[data-automation-id="header-unclaim-button"]')); + expect(unclaimButton.nativeElement.innerText).toBe('ADF_TASK_LIST.DETAILS.BUTTON.UNCLAIM'); + }); + + it('should not display the claim/requeue button if the current logged-in user is not part of the group', () => { + component.taskDetails = new TaskDetailsModel(taskDetailsWithInvolvedGroupMock); + component.ngOnChanges({}); + fixture.detectChanges(); + let unclaimButton = fixture.debugElement.query(By.css('[data-automation-id="header-unclaim-button"]')); + expect(unclaimButton.nativeElement.innerText).toBe('ADF_TASK_LIST.DETAILS.BUTTON.UNCLAIM'); + }); + + it('should not display the requeue button if the task is assigned to others', () => { const batman = new UserProcessModel({ id : 1, email: 'bruce.wayne@gotham.com', firstName: 'Bruce', lastName: 'Wayne', userImage: 'batman.jpg' }); - let taskListService; + component.taskDetails.assignee = batman; + component.ngOnChanges({}); + fixture.detectChanges(); + let unclaimButton = fixture.debugElement.query(By.css('[data-automation-id="header-unclaim-button"]')); + expect(unclaimButton).toBeNull(); + }); - beforeEach(() => { - taskListService = TestBed.get(TaskListService); - component.taskDetails.assignee = batman; - component.taskDetails.id = 'repair-batmobile'; - fixture.detectChanges(); - }); + it('should not display the requeue button if the task is assigned to others in a group', () => { + component.taskDetails = new TaskDetailsModel(taskDetailsWithAssigneeMock); + component.ngOnChanges({}); + fixture.detectChanges(); + let unclaimButton = fixture.debugElement.query(By.css('[data-automation-id="header-unclaim-button"]')); + expect(unclaimButton).toBeNull(); + }); - it('should display the requeue button if there is assignee', () => { - let unclaimButton = fixture.debugElement.query(By.css('[data-automation-id="header-unclaim-button"]')); - expect(unclaimButton.nativeElement.innerText).toBe('ADF_TASK_LIST.DETAILS.BUTTON.UNCLAIM'); - }); + it('should not display the requeue button if the task is completed', () => { + component.taskDetails = new TaskDetailsModel(completedTaskDetailsMock); + component.ngOnChanges({}); + fixture.detectChanges(); + let unclaimButton = fixture.debugElement.query(By.css('[data-automation-id="header-unclaim-button"]')); + expect(unclaimButton).toBeNull(); + }); - it('should call the service\'s unclaim method on unclaiming' , () => { - spyOn(taskListService, 'unclaimTask'); + it('should call the service\'s unclaim method on unclaiming', () => { + spyOn(service, 'unclaimTask'); + component.taskDetails = new TaskDetailsModel(taskDetailsWithInvolvedGroupMock); + component.ngOnChanges({}); + fixture.detectChanges(); - let unclaimButton = fixture.debugElement.query(By.css('[data-automation-id="header-unclaim-button"]')); - unclaimButton.triggerEventHandler('click', {}); + let unclaimButton = fixture.debugElement.query(By.css('[data-automation-id="header-unclaim-button"]')); + unclaimButton.triggerEventHandler('click', {}); - expect(taskListService.unclaimTask).toHaveBeenCalledWith('repair-batmobile'); - }); + expect(service.unclaimTask).toHaveBeenCalledWith('91'); + }); - it('should trigger the unclaim event on successfull unclaiming', () => { - let unclaimed: boolean = false; - component.unclaim.subscribe(() => { unclaimed = true; }); - spyOn(taskListService, 'unclaimTask').and.returnValue(Observable.of(true)); + it('should trigger the unclaim event on successfull unclaiming', () => { + let unclaimed: boolean = false; + spyOn(service, 'unclaimTask').and.returnValue(Observable.of(true)); + component.taskDetails = new TaskDetailsModel(taskDetailsWithInvolvedGroupMock); + component.ngOnChanges({}); + fixture.detectChanges(); + component.unclaim.subscribe(() => { unclaimed = true; }); - let unclaimButton = fixture.debugElement.query(By.css('[data-automation-id="header-unclaim-button"]')); - unclaimButton.triggerEventHandler('click', {}); + let unclaimButton = fixture.debugElement.query(By.css('[data-automation-id="header-unclaim-button"]')); + unclaimButton.triggerEventHandler('click', {}); - expect(unclaimed).toBeTruthy(); - }); + expect(unclaimed).toBeTruthy(); }); it('should display due date', () => { diff --git a/ng2-components/ng2-activiti-tasklist/src/components/task-header.component.ts b/ng2-components/ng2-activiti-tasklist/src/components/task-header.component.ts index 9c90b0553b..f99769bb54 100644 --- a/ng2-components/ng2-activiti-tasklist/src/components/task-header.component.ts +++ b/ng2-components/ng2-activiti-tasklist/src/components/task-header.component.ts @@ -15,8 +15,9 @@ * limitations under the License. */ -import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core'; +import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core'; import { CardViewDateItemModel, CardViewItem, CardViewMapItemModel, CardViewTextItemModel, LogService } from 'ng2-alfresco-core'; +import { BpmUserService } from 'ng2-alfresco-userinfo'; import { TaskDetailsModel } from '../models/task-details.model'; import { TaskListService } from './../services/tasklist.service'; @@ -25,7 +26,7 @@ import { TaskListService } from './../services/tasklist.service'; templateUrl: './task-header.component.html', styleUrls: ['./task-header.component.scss'] }) -export class TaskHeaderComponent implements OnChanges { +export class TaskHeaderComponent implements OnChanges, OnInit { @Input() formName: string = null; @@ -39,13 +40,20 @@ export class TaskHeaderComponent implements OnChanges { @Output() unclaim: EventEmitter = new EventEmitter(); + private currentUserId: number; + properties: CardViewItem []; inEdit: boolean = false; constructor(private activitiTaskService: TaskListService, + private bpmUserService: BpmUserService, private logService: LogService) { } + ngOnInit() { + this.getCurrentUserId(); + } + ngOnChanges(changes: SimpleChanges) { this.refreshData(); } @@ -148,6 +156,15 @@ export class TaskHeaderComponent implements OnChanges { } } + /** + * Return the bpmUser + */ + private getCurrentUserId(): void { + this.bpmUserService.getCurrentUserInfo().subscribe((res) => { + this.currentUserId = res ? +res.id : null; + }); + } + /** * Return the process parent information */ @@ -161,14 +178,49 @@ export class TaskHeaderComponent implements OnChanges { * Does the task have an assignee */ public hasAssignee(): boolean { - return (this.taskDetails && this.taskDetails.assignee) ? true : false; + return !!this.taskDetails.assignee ? true : false; } /** - * Is the task assigned to the currently loggedin user + * Returns true if the task is assigne to logged in user */ - isAssignedToMe(): boolean { - return this.taskDetails.assignee ? true : false; + public isAssignedTo(userId): boolean { + return this.hasAssignee() ? this.taskDetails.assignee.id === userId : false; + } + + /** + * Return true if the task assigned + */ + public isAssignedToCurrentUser(): boolean { + return this.hasAssignee() && this.isAssignedTo(this.currentUserId); + } + + /** + * Return true if the task has involvedGroup + */ + public hasInvolvedGroup(): boolean { + return this.taskDetails.involvedGroups.length > 0 ? true : false; + } + + /** + * Return true if the task has involvedPeople + */ + public hasInvolvedPeople(): boolean { + return this.taskDetails.involvedPeople.length > 0 ? true : false; + } + + /** + * Return true if the task claimable + */ + public isTaskClaimable(): boolean { + return !this.isCompleted() && (this.hasInvolvedGroup() || this.hasInvolvedPeople()) && !this.hasAssignee() ; + } + + /** + * Return true if the task claimed by currentUser + */ + public isTaskClaimedByCurrentUser(): boolean { + return !this.isCompleted() && (this.hasInvolvedGroup() || this.hasInvolvedPeople()) && this.isAssignedToCurrentUser(); } /** @@ -207,7 +259,7 @@ export class TaskHeaderComponent implements OnChanges { /** * Returns true if the task is completed */ - isCompleted() { - return !!this.taskDetails.endDate; + isCompleted(): boolean { + return this.taskDetails && !!this.taskDetails.endDate; } } diff --git a/ng2-components/ng2-activiti-tasklist/src/models/task-details.model.ts b/ng2-components/ng2-activiti-tasklist/src/models/task-details.model.ts index 5f395a8981..d57488cfb6 100644 --- a/ng2-components/ng2-activiti-tasklist/src/models/task-details.model.ts +++ b/ng2-components/ng2-activiti-tasklist/src/models/task-details.model.ts @@ -24,6 +24,7 @@ */ import { TaskRepresentation } from 'alfresco-js-api'; import { UserProcessModel } from 'ng2-alfresco-core'; +import { UserGroupModel } from './user-group.model'; export class TaskDetailsModel implements TaskRepresentation { id: string; @@ -43,6 +44,7 @@ export class TaskDetailsModel implements TaskRepresentation { managerOfCandidateGroup: boolean; memberOfCandidateGroup: boolean; memberOfCandidateUsers: boolean; + involvedGroups: UserGroupModel []; involvedPeople: UserProcessModel []; parentTaskId: string; parentTaskName: string; @@ -77,6 +79,7 @@ export class TaskDetailsModel implements TaskRepresentation { this.managerOfCandidateGroup = !!obj.managerOfCandidateGroup; this.memberOfCandidateGroup = !!obj.memberOfCandidateGroup; this.memberOfCandidateUsers = !!obj.memberOfCandidateUsers; + this.involvedGroups = obj.involvedGroups; this.involvedPeople = obj.involvedPeople; this.parentTaskId = obj.parentTaskId || null; this.parentTaskName = obj.parentTaskName || null; diff --git a/ng2-components/ng2-activiti-tasklist/src/models/user-group.model.ts b/ng2-components/ng2-activiti-tasklist/src/models/user-group.model.ts new file mode 100644 index 0000000000..117237c9a7 --- /dev/null +++ b/ng2-components/ng2-activiti-tasklist/src/models/user-group.model.ts @@ -0,0 +1,36 @@ +/*! + * @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. + */ + +/** + * This object represent the process service user group.* + */ + +export class UserGroupModel { + id: number; + name: string; + externalId: string; + status: string; + groups: any = {}; + + constructor(obj?: any) { + this.id = obj && obj.id; + this.name = obj && obj.name; + this.externalId = obj && obj.externalId; + this.status = obj && obj.status; + this.groups = obj && obj.groups; + } +}