diff --git a/lib/process-services/src/lib/task-list/components/task-form/task-form.component.spec.ts b/lib/process-services/src/lib/task-list/components/task-form/task-form.component.spec.ts
index d109016e1a..a14c149544 100644
--- a/lib/process-services/src/lib/task-list/components/task-form/task-form.component.spec.ts
+++ b/lib/process-services/src/lib/task-list/components/task-form/task-form.component.spec.ts
@@ -37,11 +37,15 @@ import {
standaloneTaskWithoutForm,
completedStandaloneTaskWithoutForm,
claimableTaskDetailsMock,
- initiatorCanCompleteTaskDetailsMock
+ initiatorCanCompleteTaskDetailsMock,
+ taskDetailsWithOutCandidateGroup,
+ claimedTaskDetailsMock,
+ claimedByGroupMemberMock
} from '../../../mock/task/task-details.mock';
import { TaskDetailsModel } from '../../models/task-details.model';
import { ProcessTestingModule } from '../../../testing/process.testing.module';
import { TranslateModule } from '@ngx-translate/core';
+import { By } from '@angular/platform-browser';
describe('TaskFormComponent', () => {
let component: TaskFormComponent;
@@ -495,4 +499,125 @@ describe('TaskFormComponent', () => {
expect(validationForm.textContent).toBe('check_circle');
});
});
+
+ describe('Claim/Unclaim buttons', () => {
+
+ it('should display the claim button if no assignee', async() => {
+ getTaskDetailsSpy.and.returnValue(of(claimableTaskDetailsMock));
+
+ component.taskId = 'mock-task-id';
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ const claimButton = fixture.debugElement.query(By.css('[data-automation-id="adf-task-form-claim-button"]'));
+ expect(claimButton.nativeElement.innerText).toBe('ADF_TASK_LIST.DETAILS.BUTTON.CLAIM');
+ });
+
+ it('should not display the claim/requeue button if the task is not claimable ', async() => {
+ getTaskDetailsSpy.and.returnValue(of(taskDetailsWithOutCandidateGroup));
+
+ component.taskId = 'mock-task-id';
+ fixture.detectChanges();
+
+ await fixture.whenStable();
+ const claimButton = fixture.debugElement.query(By.css('[data-automation-id="adf-task-form-claim-button"]'));
+ const unclaimButton = fixture.debugElement.query(By.css('[data-automation-id="adf-task-form-unclaim-button"]'));
+
+ expect(component.isTaskClaimable()).toBe(false);
+ expect(component.isTaskClaimedByCandidateMember()).toBe(false);
+ expect(unclaimButton).toBeNull();
+ expect(claimButton).toBeNull();
+ });
+
+ it('should display the claim button if the task is claimable', async() => {
+ getTaskDetailsSpy.and.returnValue(of(claimableTaskDetailsMock));
+
+ component.taskId = 'mock-task-id';
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ const claimButton = fixture.debugElement.query(By.css('[data-automation-id="adf-task-form-claim-button"]'));
+
+ expect(component.isTaskClaimable()).toBe(true);
+ expect(claimButton.nativeElement.innerText).toBe('ADF_TASK_LIST.DETAILS.BUTTON.CLAIM');
+ });
+
+ it('should display the release button if task is claimed by the current logged-in user', async() => {
+ getBpmLoggedUserSpy.and.returnValue(of(claimedTaskDetailsMock.assignee));
+ getTaskDetailsSpy.and.returnValue(of(claimedTaskDetailsMock));
+
+ component.taskId = 'mock-task-id';
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ const unclaimButton = fixture.debugElement.query(By.css('[data-automation-id="adf-task-form-unclaim-button"]'));
+
+ expect(component.isTaskClaimedByCandidateMember()).toBe(true);
+ expect(unclaimButton.nativeElement.innerText).toBe('ADF_TASK_LIST.DETAILS.BUTTON.UNCLAIM');
+ });
+
+ it('should not display the release button to logged in user if task is claimed by other candidate member', async() => {
+ getTaskDetailsSpy.and.returnValue(of(claimedByGroupMemberMock));
+
+ component.taskId = 'mock-task-id';
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ const unclaimButton = fixture.debugElement.query(By.css('[data-automation-id="adf-task-form-unclaim-button"]'));
+
+ expect(component.isTaskClaimedByCandidateMember()).toBe(false);
+ expect(unclaimButton).toBeNull();
+ });
+
+ it('should not display the release button if the task is completed', async() => {
+ getTaskDetailsSpy.and.returnValue(of(completedTaskDetailsMock));
+
+ component.taskId = 'mock-task-id';
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ const claimButton = fixture.debugElement.query(By.css('[data-automation-id="adf-task-form-claim-button"]'));
+ const unclaimButton = fixture.debugElement.query(By.css('[data-automation-id="adf-task-form-unclaim-button"]'));
+
+ expect(claimButton).toBeNull();
+ expect(unclaimButton).toBeNull();
+ });
+
+ it('should emit taskClaimed when task is claimed', (done) => {
+ spyOn(taskListService, 'claimTask').and.returnValue(of({}));
+ getTaskDetailsSpy.and.returnValue(of(claimableTaskDetailsMock));
+
+ component.taskId = 'mock-task-id';
+
+ component.taskClaimed.subscribe((taskId) => {
+ expect(taskId).toEqual(component.taskId);
+ done();
+ });
+
+ component.ngOnInit();
+ fixture.detectChanges();
+
+ const claimBtn = fixture.debugElement.query(By.css('[adf-claim-task]'));
+ claimBtn.nativeElement.click();
+ });
+
+ it('should emit taskUnClaimed when task is unclaimed', (done) => {
+ spyOn(taskListService, 'unclaimTask').and.returnValue(of({}));
+ getBpmLoggedUserSpy.and.returnValue(of(claimedTaskDetailsMock.assignee));
+ getTaskDetailsSpy.and.returnValue(of(claimedTaskDetailsMock));
+
+ component.taskId = 'mock-task-id';
+
+ component.taskUnclaimed.subscribe((taskId: string) => {
+ expect(taskId).toEqual(component.taskId);
+ done();
+ });
+
+ component.ngOnInit();
+ fixture.detectChanges();
+
+ const unclaimBtn = fixture.debugElement.query(By.css('[adf-unclaim-task]'));
+ unclaimBtn.nativeElement.click();
+ });
+ });
});
diff --git a/lib/process-services/src/lib/task-list/components/task-form/task-form.component.ts b/lib/process-services/src/lib/task-list/components/task-form/task-form.component.ts
index b53caf4ee3..77dca180fc 100644
--- a/lib/process-services/src/lib/task-list/components/task-form/task-form.component.ts
+++ b/lib/process-services/src/lib/task-list/components/task-form/task-form.component.ts
@@ -117,6 +117,14 @@ export class TaskFormComponent implements OnInit {
@Output()
cancel = new EventEmitter
();
+ /** Emitted when the task is claimed. */
+ @Output()
+ taskClaimed = new EventEmitter();
+
+ /** Emitted when the task is unclaimed (ie, requeued).. */
+ @Output()
+ taskUnclaimed = new EventEmitter();
+
taskDetails: TaskDetailsModel;
currentLoggedUser: UserRepresentation;
loading: boolean = false;
@@ -278,4 +286,28 @@ export class TaskFormComponent implements OnInit {
getCompletedTaskTranslatedMessage(): Observable {
return this.translationService.get('ADF_TASK_FORM.COMPLETED_TASK.TITLE', { taskName: this.taskDetails.name });
}
+
+ isCandidateMember(): boolean {
+ return this.taskDetails.managerOfCandidateGroup || this.taskDetails.memberOfCandidateGroup || this.taskDetails.memberOfCandidateUsers;
+ }
+
+ isTaskClaimable(): boolean {
+ return this.isCandidateMember() && !this.isAssigned();
+ }
+
+ isTaskClaimedByCandidateMember(): boolean {
+ return this.isCandidateMember() && this.isAssignedToMe() && !this.isCompletedTask();
+ }
+
+ reloadTask() {
+ this.loadTask(this.taskId);
+ }
+
+ onClaimTask(taskId: string) {
+ this.taskClaimed.emit(taskId);
+ }
+
+ onUnclaimTask(taskId: string) {
+ this.taskUnclaimed.emit(taskId);
+ }
}
diff --git a/lib/process-services/src/lib/task-list/components/task-form/unclaim-task.directive.spec.ts b/lib/process-services/src/lib/task-list/components/task-form/unclaim-task.directive.spec.ts
new file mode 100644
index 0000000000..43bd1d48df
--- /dev/null
+++ b/lib/process-services/src/lib/task-list/components/task-form/unclaim-task.directive.spec.ts
@@ -0,0 +1,126 @@
+/*!
+ * @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, Output, EventEmitter } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { setupTestBed } from '@alfresco/adf-core';
+import { of } from 'rxjs';
+import { TaskListService } from '../../services/tasklist.service';
+import { ProcessTestingModule } from '../../../testing/process.testing.module';
+
+describe('UnclaimTaskDirective', () => {
+
+ @Component({
+ selector: 'adf-unclaim-test-component',
+ template: ''
+ })
+ class TestComponent {
+ taskId = 'test1234';
+ @Output()
+ unclaim: EventEmitter = new EventEmitter();
+
+ onUnclaim(event) {
+ this.unclaim.emit(event);
+ }
+ }
+
+ let fixture: ComponentFixture;
+ let taskListService: TaskListService;
+
+ setupTestBed({
+ imports: [
+ ProcessTestingModule
+ ],
+ declarations: [
+ TestComponent
+ ]
+ });
+
+ beforeEach(() => {
+ taskListService = TestBed.get(TaskListService);
+ fixture = TestBed.createComponent(TestComponent);
+ fixture.detectChanges();
+ });
+
+ it('Should be able to call unclaim task service', () => {
+ const claimTaskSpy = spyOn(taskListService, 'unclaimTask').and.returnValue(of({}));
+
+ const button = fixture.nativeElement.querySelector('button');
+ button.click();
+
+ expect(claimTaskSpy).toHaveBeenCalledWith(fixture.componentInstance.taskId);
+ });
+
+ it('Should be able to catch success event on click of unclaim button', async() => {
+ spyOn(taskListService, 'unclaimTask').and.returnValue(of({}));
+ const unclaimSpy = spyOn(fixture.componentInstance.unclaim, 'emit');
+
+ const button = fixture.nativeElement.querySelector('button');
+ button.click();
+ fixture.detectChanges();
+ await fixture.whenStable();
+
+ expect(unclaimSpy).toHaveBeenCalledWith(fixture.componentInstance.taskId);
+ });
+});
+
+describe('Claim Task Directive validation errors', () => {
+
+ @Component({
+ selector: 'adf-unclaim-no-fields-validation-component',
+ template: ''
+ })
+ class ClaimTestMissingInputDirectiveComponent {
+
+ }
+
+ @Component({
+ selector: 'adf-claim-no-taskid-validation-component',
+ template: ''
+ })
+ class ClaimTestMissingTaskIdDirectiveComponent {
+
+ }
+
+ let fixture: ComponentFixture;
+
+ setupTestBed({
+ imports: [
+ ProcessTestingModule
+ ],
+ declarations: [
+ ClaimTestMissingTaskIdDirectiveComponent,
+ ClaimTestMissingInputDirectiveComponent
+ ]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ClaimTestMissingInputDirectiveComponent);
+ });
+
+ it('should throw error when missing input', () => {
+ fixture = TestBed.createComponent(ClaimTestMissingInputDirectiveComponent);
+
+ expect(() => fixture.detectChanges()).toThrowError();
+ });
+
+ it('should throw error when taskId is not set', () => {
+ fixture = TestBed.createComponent(ClaimTestMissingTaskIdDirectiveComponent);
+
+ expect( () => fixture.detectChanges()).toThrowError('Attribute taskId is required');
+ });
+});
diff --git a/lib/process-services/src/lib/task-list/components/task-form/unclaim-task.directive.ts b/lib/process-services/src/lib/task-list/components/task-form/unclaim-task.directive.ts
new file mode 100644
index 0000000000..57f1812c0b
--- /dev/null
+++ b/lib/process-services/src/lib/task-list/components/task-form/unclaim-task.directive.ts
@@ -0,0 +1,88 @@
+/*!
+ * @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,
+ HostListener,
+ Input,
+ Output,
+ EventEmitter
+} from '@angular/core';
+import { TaskListService } from '../../services/tasklist.service';
+import { LogService } from '@alfresco/adf-core';
+
+@Directive({
+ // tslint:disable-next-line: directive-selector
+ selector: '[adf-unclaim-task]'
+})
+export class UnclaimTaskDirective {
+ /** (Required) The id of the task. */
+ @Input()
+ taskId: string;
+
+ /** Emitted when the task is released. */
+ @Output()
+ success: EventEmitter = new EventEmitter();
+
+ /** Emitted when the task cannot be released. */
+ @Output()
+ error: EventEmitter = new EventEmitter();
+
+ invalidParams: string[] = [];
+
+ constructor(
+ private taskListService: TaskListService,
+ private logService: LogService) {}
+
+ ngOnInit() {
+ this.validateInputs();
+ }
+
+ validateInputs() {
+ if (!this.isTaskValid()) {
+ this.invalidParams.push('taskId');
+ }
+ if (this.invalidParams.length) {
+ throw new Error(
+ `Attribute ${this.invalidParams.join(', ')} is required`
+ );
+ }
+ }
+
+ isTaskValid(): boolean {
+ return this.taskId && this.taskId.length > 0;
+ }
+
+ @HostListener('click')
+ async onClick() {
+ try {
+ this.unclaimTask();
+ } catch (error) {
+ this.error.emit(error);
+ }
+ }
+
+ private async unclaimTask() {
+ await this.taskListService.unclaimTask(this.taskId).subscribe(
+ () => {
+ this.logService.info('Task unclaimed');
+ this.success.emit(this.taskId);
+ },
+ error => this.error.emit(error)
+ );
+ }
+}
diff --git a/lib/process-services/src/lib/task-list/components/task-header.component.html b/lib/process-services/src/lib/task-list/components/task-header.component.html
index 4246928719..d6c2879caf 100644
--- a/lib/process-services/src/lib/task-list/components/task-header.component.html
+++ b/lib/process-services/src/lib/task-list/components/task-header.component.html
@@ -3,10 +3,26 @@
-
-