[ACA-3416] Add Claim/Release actions on adf task form (#5753)

* [ACA-3255] FE - Claim a task

* * Added unit tests

* * Added unit tests
* Changed cloud directive names

* * Added/Updated documents

* * Added showReleaseClaim button flag
* Add unit test too

* * Used claim/release directive in task-header component.

* * Fixed unit test

* * Fixed one comment

* * After rebase

* * Fixed comments
This commit is contained in:
siva kumar
2020-06-10 15:13:23 +05:30
committed by GitHub
parent 77bbecea8e
commit ea62b1e3bd
32 changed files with 947 additions and 105 deletions

View File

@@ -0,0 +1,24 @@
/*!
* @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 } from '@angular/core';
@Component({
selector: 'adf-form-custom-outcomes',
template: '<ng-content></ng-content>'
})
export class FormCustomOutcomesComponent {}

View File

@@ -34,6 +34,7 @@
</adf-form-renderer>
</mat-card-content>
<mat-card-actions *ngIf="form.hasOutcomes()" class="adf-form-mat-card-actions">
<ng-content select="adf-form-custom-outcomes"></ng-content>
<button [id]="'adf-form-'+ outcome.name | formatSpace" *ngFor="let outcome of form.outcomes"
[color]="getColorForOutcome(outcome.name)" mat-button [disabled]="!isOutcomeButtonEnabled(outcome)"
[class.adf-form-hide-button]="!isOutcomeButtonVisible(outcome, form.readOnly)"

View File

@@ -15,7 +15,8 @@
* limitations under the License.
*/
import { SimpleChange, ComponentFactoryResolver, Injector, NgModule, Component } from '@angular/core';
import { SimpleChange, ComponentFactoryResolver, Injector, NgModule, Component, ViewChild, DebugElement } from '@angular/core';
import { By } from '@angular/platform-browser';
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { Observable, of, throwError } from 'rxjs';
import { FormFieldModel, FormFieldTypes, FormModel, FormOutcomeEvent, FormOutcomeModel,
@@ -1013,3 +1014,79 @@ describe('FormComponent', () => {
expect(radioFieldById.value).toBe('option_3');
});
});
@Component({
selector: 'adf-form-with-custom-outcomes',
template: `
<adf-form #adfForm>
<adf-form-custom-outcomes>
<button mat-button id="adf-custom-outcome-1" (click)="onCustomButtonOneClick()">
CUSTOM-BUTTON-1
</button>
<button mat-button id="adf-custom-outcome-2" (click)="onCustomButtonTwoClick()">
CUSTOM-BUTTON-2
</button>
</adf-form-custom-outcomes>
</adf-form>`
})
class FormWithCustomOutComesComponent {
@ViewChild('adfForm')
adfForm: FormComponent;
onCustomButtonOneClick() { }
onCustomButtonTwoClick() { }
}
describe('FormWithCustomOutComesComponent', () => {
let fixture: ComponentFixture<FormWithCustomOutComesComponent>;
let customComponent: FormWithCustomOutComesComponent;
let debugElement: DebugElement;
setupTestBed({
imports: [
TranslateModule.forRoot(),
ProcessTestingModule
],
declarations: [FormWithCustomOutComesComponent]
});
beforeEach(() => {
fixture = TestBed.createComponent(FormWithCustomOutComesComponent);
customComponent = fixture.componentInstance;
debugElement = fixture.debugElement;
const formRepresentation = {
fields: [
{ id: 'container1' }
],
outcomes: [
{ id: 'outcome-1', name: 'outcome 1' }
]
};
const form = new FormModel(formRepresentation);
customComponent.adfForm.form = form;
fixture.detectChanges();
});
afterEach(() => {
fixture.destroy();
});
it('should be able to inject custom outcomes and click on custom outcomes', () => {
const onCustomButtonOneSpy = spyOn(customComponent, 'onCustomButtonOneClick').and.callThrough();
const buttonOneBtn = debugElement.query(By.css('#adf-custom-outcome-1'));
const buttonTwoBtn = debugElement.query(By.css('#adf-custom-outcome-2'));
expect(buttonOneBtn).not.toBeNull();
expect(buttonTwoBtn).not.toBeNull();
buttonOneBtn.nativeElement.click();
expect(onCustomButtonOneSpy).toHaveBeenCalled();
expect(buttonOneBtn.nativeElement.innerText).toBe('CUSTOM-BUTTON-1');
expect(buttonTwoBtn.nativeElement.innerText).toBe('CUSTOM-BUTTON-2');
});
});

View File

@@ -20,6 +20,7 @@ import { MaterialModule } from '../material.module';
import { CoreModule } from '@alfresco/adf-core';
import { FormComponent } from './form.component';
import { StartFormComponent } from './start-form.component';
import { FormCustomOutcomesComponent } from './form-custom-outcomes.component';
@NgModule({
imports: [
@@ -28,11 +29,13 @@ import { StartFormComponent } from './start-form.component';
],
declarations: [
FormComponent,
StartFormComponent
StartFormComponent,
FormCustomOutcomesComponent
],
exports: [
FormComponent,
StartFormComponent
StartFormComponent,
FormCustomOutcomesComponent
]
})
export class FormModule {}

View File

@@ -18,4 +18,5 @@
export * from './form.component';
export * from './start-form.component';
export * from './process-form-rendering.service';
export * from './form-custom-outcomes.component';
export * from './form.module';

View File

@@ -35,6 +35,8 @@
(completed)="onComplete()"
(showAttachForm)="onShowAttachForm()"
(executeOutcome)='onFormExecuteOutcome($event)'
(taskClaimed)="onClaimAction($event)"
(taskUnclaimed)="onUnclaimAction($event)"
(error)="onFormError($event)" #activitiTaskForm>
</adf-task-form>
<adf-attach-form *ngIf="isShowAttachForm()"

View File

@@ -0,0 +1,122 @@
/*!
* @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('ClaimTaskDirective', () => {
@Component({
selector: 'adf-claim-test-component',
template: '<button adf-claim-task [taskId]="taskId" (success)="onClaim($event)">Claim</button>'
})
class TestComponent {
taskId = 'test1234';
@Output()
claim: EventEmitter<any> = new EventEmitter<any>();
onClaim(event) {
this.claim.emit(event);
}
}
let fixture: ComponentFixture<TestComponent>;
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 claim task service', () => {
const claimTaskSpy = spyOn(taskListService, 'claimTask').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 claim button', async() => {
spyOn(taskListService, 'claimTask').and.returnValue(of({}));
const unclaimSpy = spyOn(fixture.componentInstance.claim, '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-claim-no-fields-validation-component',
template: '<button adf-claim-task></button>'
})
class ClaimTestMissingInputDirectiveComponent { }
@Component({
selector: 'adf-claim-no-taskid-validation-component',
template: '<button adf-claim-task [taskId]=""></button>'
})
class ClaimTestMissingTaskIdDirectiveComponent { }
let fixture: ComponentFixture<any>;
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');
});
});

View File

@@ -0,0 +1,89 @@
/*!
* @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,
Output,
EventEmitter,
HostListener
} from '@angular/core';
import { TaskListService } from '../../services/tasklist.service';
import { LogService } from '@alfresco/adf-core';
@Directive({
// tslint:disable-next-line: directive-selector
selector: '[adf-claim-task]'
})
export class ClaimTaskDirective {
/** (Required) The id of the task. */
@Input()
taskId: string;
/** Emitted when the task is claimed. */
@Output()
success: EventEmitter<any> = new EventEmitter<any>();
/** Emitted when the task cannot be claimed. */
@Output()
error: EventEmitter<any> = new EventEmitter<any>();
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.claimTask();
} catch (error) {
this.error.emit(error);
}
}
private async claimTask() {
await this.taskListService.claimTask(this.taskId).subscribe(
() => {
this.logService.info('Task claimed');
this.success.emit(this.taskId);
},
error => this.error.emit(error)
);
}
}

View File

@@ -16,6 +16,10 @@
(formError)='onFormError($event)'
(error)='onError($event)'
(executeOutcome)='onFormExecuteOutcome($event)'>
<adf-form-custom-outcomes>
<ng-template [ngTemplateOutlet]="taskFormButtons">
</ng-template>
</adf-form-custom-outcomes>
</adf-form>
<ng-template #withoutForm>
<adf-task-standalone *ngIf="isStandaloneTask(); else emptyFormMessage"
@@ -56,6 +60,7 @@
</ng-template>
</mat-card-content>
<mat-card-actions class="adf-task-form-actions">
<ng-template [ngTemplateOutlet]="taskFormButtons"></ng-template>
<button id="adf-no-form-cancel-button" mat-button *ngIf="showCancelButton" (click)="onCancel()">
{{'ADF_TASK_FORM.EMPTY_FORM.BUTTONS.CANCEL' | translate}}
</button>
@@ -66,6 +71,23 @@
</mat-card>
</ng-template>
</ng-template>
<ng-template #taskFormButtons>
<button mat-button data-automation-id="adf-task-form-claim-button"
*ngIf="isTaskClaimable()"
adf-claim-task
[taskId]="taskId"
(success)="onClaimTask($event)">
{{ 'ADF_TASK_LIST.DETAILS.BUTTON.CLAIM' | translate }}
</button>
<button mat-button data-automation-id="adf-task-form-unclaim-button"
*ngIf="isTaskClaimedByCandidateMember()"
adf-unclaim-task
[taskId]="taskId"
(success)="onUnclaimTask($event)">
{{ 'ADF_TASK_LIST.DETAILS.BUTTON.UNCLAIM' | translate }}
</button>
</ng-template>
</ng-container>
<ng-template #loadingTemplate>
<div fxLayout="row" fxLayoutAlign="center stretch">

View File

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

View File

@@ -117,6 +117,14 @@ export class TaskFormComponent implements OnInit {
@Output()
cancel = new EventEmitter<void>();
/** Emitted when the task is claimed. */
@Output()
taskClaimed = new EventEmitter<string>();
/** Emitted when the task is unclaimed (ie, requeued).. */
@Output()
taskUnclaimed = new EventEmitter<string>();
taskDetails: TaskDetailsModel;
currentLoggedUser: UserRepresentation;
loading: boolean = false;
@@ -278,4 +286,28 @@ export class TaskFormComponent implements OnInit {
getCompletedTaskTranslatedMessage(): Observable<string> {
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);
}
}

View File

@@ -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: '<button adf-unclaim-task [taskId]="taskId" (success)="onUnclaim($event)">Unclaim</button>'
})
class TestComponent {
taskId = 'test1234';
@Output()
unclaim: EventEmitter<any> = new EventEmitter<any>();
onUnclaim(event) {
this.unclaim.emit(event);
}
}
let fixture: ComponentFixture<TestComponent>;
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: '<button adf-unclaim-task></button>'
})
class ClaimTestMissingInputDirectiveComponent {
}
@Component({
selector: 'adf-claim-no-taskid-validation-component',
template: '<button adf-unclaim-task [taskId]=""></button>'
})
class ClaimTestMissingTaskIdDirectiveComponent {
}
let fixture: ComponentFixture<any>;
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');
});
});

View File

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

View File

@@ -3,10 +3,26 @@
<adf-card-view [properties]="properties" [editable]="!isCompleted()" [displayClearAction]="displayDateClearAction"></adf-card-view>
</mat-card-content>
<mat-card-actions class="adf-controls">
<button *ngIf="isTaskClaimedByCandidateMember()" mat-button data-automation-id="header-unclaim-button" id="unclaim-task" (click)="unclaimTask(taskDetails.id)" class="adf-claim-controls">{{ 'ADF_TASK_LIST.DETAILS.BUTTON.UNCLAIM' | translate }}
<mat-card-actions class="adf-controls" *ngIf="showClaimRelease">
<button *ngIf="isTaskClaimedByCandidateMember()"
mat-button
data-automation-id="header-unclaim-button"
id="unclaim-task"
class="adf-claim-controls"
adf-unclaim-task
[taskId]="taskDetails.id"
(success)="onUnclaimTask($event)">
{{ 'ADF_TASK_LIST.DETAILS.BUTTON.UNCLAIM' | translate }}
</button>
<button *ngIf="isTaskClaimable()" mat-button data-automation-id="header-claim-button" id="claim-task" (click)="claimTask(taskDetails.id)" class="adf-claim-controls">{{ 'ADF_TASK_LIST.DETAILS.BUTTON.CLAIM' | translate }}
<button *ngIf="isTaskClaimable()"
mat-button
data-automation-id="header-claim-button"
id="claim-task"
class="adf-claim-controls"
adf-claim-task
[taskId]="taskDetails.id"
(success)="onClaimTask($event)">
{{ 'ADF_TASK_LIST.DETAILS.BUTTON.CLAIM' | translate }}
</button>
</mat-card-actions>
</mat-card>

View File

@@ -136,6 +136,30 @@ describe('TaskHeaderComponent', () => {
describe('Claiming', () => {
it('should be able display the claim/release button if showClaimRelease set to true', async(() => {
component.taskDetails = new TaskDetailsModel(claimableTaskDetailsMock);
component.showClaimRelease = true;
component.refreshData();
fixture.detectChanges();
fixture.whenStable().then(() => {
const 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 not be able display the claim/release button if showClaimRelease set to false', async(() => {
component.taskDetails = new TaskDetailsModel(claimableTaskDetailsMock);
component.showClaimRelease = false;
component.refreshData();
fixture.detectChanges();
fixture.whenStable().then(() => {
const claimButton = fixture.debugElement.query(By.css('[data-automation-id="header-claim-button"]'));
expect(claimButton).toBeNull();
});
}));
it('should display the claim button if no assignee', async(() => {
component.taskDetails = new TaskDetailsModel(claimableTaskDetailsMock);
@@ -227,38 +251,37 @@ describe('TaskHeaderComponent', () => {
});
}));
it('should call the service unclaim method on un-claiming', async(() => {
spyOn(service, 'unclaimTask').and.returnValue(of(true));
component.taskDetails = new TaskDetailsModel(claimedTaskDetailsMock);
component.refreshData();
it('should emit claim event when task is claimed', (done) => {
spyOn(service, 'claimTask').and.returnValue(of({}));
component.taskDetails = claimableTaskDetailsMock;
component.claim.subscribe((taskId) => {
expect(taskId).toEqual(component.taskDetails.id);
done();
});
component.ngOnInit();
fixture.detectChanges();
fixture.whenStable().then(() => {
const unclaimButton = fixture.debugElement.query(By.css('[data-automation-id="header-unclaim-button"]'));
unclaimButton.triggerEventHandler('click', {});
const claimBtn = fixture.debugElement.query(By.css('[adf-claim-task]'));
claimBtn.nativeElement.click();
});
expect(service.unclaimTask).toHaveBeenCalledWith('91');
it('should emit unclaim event when task is unclaimed', (done) => {
spyOn(service, 'unclaimTask').and.returnValue(of({}));
component.taskDetails = claimedTaskDetailsMock;
component.unclaim.subscribe((taskId: string) => {
expect(taskId).toEqual(component.taskDetails.id);
done();
});
}));
it('should trigger the unclaim event on successful un-claiming', async(() => {
let unclaimed: boolean = false;
spyOn(service, 'unclaimTask').and.returnValue(of(true));
component.taskDetails = new TaskDetailsModel(claimedTaskDetailsMock);
component.refreshData();
component.ngOnInit();
fixture.detectChanges();
fixture.whenStable().then(() => {
component.unclaim.subscribe(() => {
unclaimed = true;
});
const unclaimButton = fixture.debugElement.query(By.css('[data-automation-id="header-unclaim-button"]'));
unclaimButton.triggerEventHandler('click', {});
expect(unclaimed).toBeTruthy();
});
}));
const unclaimBtn = fixture.debugElement.query(By.css('[adf-unclaim-task]'));
unclaimBtn.nativeElement.click();
});
it('should display due date', async(() => {
component.taskDetails.dueDate = new Date('2016-11-03');

View File

@@ -23,12 +23,10 @@ import {
CardViewMapItemModel,
CardViewTextItemModel,
CardViewBaseItemModel,
LogService,
TranslationService,
AppConfigService
} from '@alfresco/adf-core';
import { TaskDetailsModel } from '../models/task-details.model';
import { TaskListService } from './../services/tasklist.service';
import { TaskDescriptionValidator } from '../validators/task-description.validator';
@Component({
@@ -46,6 +44,10 @@ export class TaskHeaderComponent implements OnChanges, OnInit {
@Input()
taskDetails: TaskDetailsModel;
/** Toggles display of the claim/release button. */
@Input()
showClaimRelease = true;
/** Emitted when the task is claimed. */
@Output()
claim: EventEmitter<any> = new EventEmitter<any>();
@@ -62,10 +64,8 @@ export class TaskHeaderComponent implements OnChanges, OnInit {
dateFormat: string;
dateLocale: string;
constructor(private activitiTaskService: TaskListService,
private bpmUserService: BpmUserService,
constructor(private bpmUserService: BpmUserService,
private translationService: TranslationService,
private logService: LogService,
private appConfig: AppConfigService) {
this.dateFormat = this.appConfig.get('dateValues.defaultDateFormat');
this.dateLocale = this.appConfig.get('dateValues.defaultDateLocale');
@@ -281,28 +281,12 @@ export class TaskHeaderComponent implements OnChanges, OnInit {
return (this.taskDetails && this.taskDetails.isCompleted()) ? 'Completed' : 'Running';
}
/**
* Claim task
*
* @param taskId
*/
claimTask(taskId: string) {
this.activitiTaskService.claimTask(taskId).subscribe(() => {
this.logService.info('Task claimed');
this.claim.emit(taskId);
});
onClaimTask(taskId: string) {
this.claim.emit(taskId);
}
/**
* Unclaim task
*
* @param taskId
*/
unclaimTask(taskId: string) {
this.activitiTaskService.unclaimTask(taskId).subscribe(() => {
this.logService.info('Task unclaimed');
this.unclaim.emit(taskId);
});
onUnclaimTask(taskId: string) {
this.unclaim.emit(taskId);
}
/**

View File

@@ -21,6 +21,8 @@ export * from './components/task-header.component';
export * from './components/no-task-detail-template.directive';
export * from './components/task-filters.component';
export * from './components/task-form/task-form.component';
export * from './components/task-form/claim-task.directive';
export * from './components/task-form/unclaim-task.directive';
export * from './components/task-details.component';
export * from './components/task-audit.directive';
export * from './components/start-task.component';

View File

@@ -38,6 +38,8 @@ import { TaskListComponent } from './components/task-list.component';
import { TaskStandaloneComponent } from './components/task-standalone.component';
import { AttachFormComponent } from './components/attach-form.component';
import { FormModule } from '../form/form.module';
import { ClaimTaskDirective } from './components/task-form/claim-task.directive';
import { UnclaimTaskDirective } from './components/task-form/unclaim-task.directive';
@NgModule({
imports: [
@@ -63,7 +65,9 @@ import { FormModule } from '../form/form.module';
TaskHeaderComponent,
StartTaskComponent,
TaskStandaloneComponent,
AttachFormComponent
AttachFormComponent,
ClaimTaskDirective,
UnclaimTaskDirective
],
exports: [
NoTaskDetailsTemplateDirective,
@@ -76,7 +80,9 @@ import { FormModule } from '../form/form.module';
TaskHeaderComponent,
StartTaskComponent,
TaskStandaloneComponent,
AttachFormComponent
AttachFormComponent,
ClaimTaskDirective,
UnclaimTaskDirective
]
})
export class TaskListModule {