[AAE-7313] User can claim a task only if he is an candidate and relea… (#7558)

* [AAE-7313] User can claim a task only if he is an candidate and release if he is assigned

* [AAE-7313] unit test improvements

* revert change on  angular.json

* [AAE-7313] removed focus from unit test describe

* Trigger travis

* Trigger travis

* [AAE-7313] exclude C593997 e2e
This commit is contained in:
Tomasz Gnyp
2022-04-19 11:05:24 +02:00
committed by GitHub
parent 61cd711cec
commit 7141230649
8 changed files with 140 additions and 155 deletions

View File

@@ -1,5 +1,6 @@
{
"C269081": "https://alfresco.atlassian.net/browse/ADF-5385",
"C272819": "https://alfresco.atlassian.net/browse/ADF-5385",
"C362241": "https://alfresco.atlassian.net/browse/ADF-5385"
"C362241": "https://alfresco.atlassian.net/browse/ADF-5385",
"C593997": "https://alfresco.atlassian.net/browse/ADF-5479"
}

View File

@@ -182,9 +182,7 @@ export class FormCloudService extends BaseCloudService implements FormCloudServi
skipCount
}) : EMPTY;
}),
map((res: any) => {
return res.list.entries.map((variable) => new TaskVariableCloud(variable.entry));
}),
map((res: any) => res.list.entries.map((variable) => new TaskVariableCloud(variable.entry))),
reduce((acc, res) => acc.concat(res), [])
);
}

View File

@@ -32,7 +32,3 @@ export const DEFAULT_TASK_PRIORITIES: TaskPriorityOption[] = [
{ label: 'ADF_CLOUD_TASK_LIST.PROPERTIES.PRIORITY_VALUES.NORMAL', value: '2', key: '2' },
{ label: 'ADF_CLOUD_TASK_LIST.PROPERTIES.PRIORITY_VALUES.HIGH', value: '3', key: '3' }
];
export const TASK_ASSIGNED_STATE = 'ASSIGNED';
export const TASK_CREATED_STATE = 'CREATED';

View File

@@ -19,11 +19,21 @@ import { Injectable } from '@angular/core';
import { AlfrescoApiService, LogService, AppConfigService, IdentityUserService, CardViewArrayItem, TranslationService } from '@alfresco/adf-core';
import { throwError, Observable, of, Subject } from 'rxjs';
import { map } from 'rxjs/operators';
import { TaskDetailsCloudModel, StartTaskCloudResponseModel } from '../start-task/models/task-details-cloud.model';
import {
TaskDetailsCloudModel,
StartTaskCloudResponseModel,
TASK_ASSIGNED_STATE,
TASK_CLAIM_PERMISSION,
TASK_CREATED_STATE,
TASK_RELEASE_PERMISSION
} from '../start-task/models/task-details-cloud.model';
import { BaseCloudService } from '../../services/base-cloud.service';
import { StartTaskCloudRequestModel } from '../start-task/models/start-task-cloud-request.model';
import { ProcessDefinitionCloud } from '../../models/process-definition-cloud.model';
import { DEFAULT_TASK_PRIORITIES, TaskPriorityOption, TASK_ASSIGNED_STATE, TASK_CREATED_STATE } from '../models/task.model';
import {
DEFAULT_TASK_PRIORITIES,
TaskPriorityOption
} from '../models/task.model';
import { TaskCloudServiceInterface } from './task-cloud.service.interface';
@Injectable({
@@ -98,7 +108,8 @@ export class TaskCloudService extends BaseCloudService implements TaskCloudServi
* @returns Boolean value if the task can be completed
*/
canClaimTask(taskDetails: TaskDetailsCloudModel): boolean {
return taskDetails && taskDetails.status === TASK_CREATED_STATE;
return taskDetails?.status === TASK_CREATED_STATE &&
taskDetails?.permissions.includes(TASK_CLAIM_PERMISSION);
}
/**
@@ -109,7 +120,10 @@ export class TaskCloudService extends BaseCloudService implements TaskCloudServi
*/
canUnclaimTask(taskDetails: TaskDetailsCloudModel): boolean {
const currentUser = this.identityUserService.getCurrentUserInfo().username;
return taskDetails && taskDetails.status === TASK_ASSIGNED_STATE && taskDetails.assignee === currentUser;
return taskDetails?.status === TASK_ASSIGNED_STATE &&
taskDetails?.assignee === currentUser &&
taskDetails?.permissions.includes(TASK_RELEASE_PERMISSION) &&
!taskDetails?.standalone;
}
/**

View File

@@ -33,6 +33,7 @@ export interface TaskDetailsCloudModel {
lastModifiedFrom?: Date;
owner?: any;
parentTaskId?: string;
permissions?: TaskPermissions[];
priority?: number;
processDefinitionId?: string;
processInstanceId?: string;
@@ -50,9 +51,26 @@ export interface StartTaskCloudResponseModel {
entry: TaskDetailsCloudModel;
}
export type TaskStatus = |
export type TaskStatus =
'COMPLETED' |
'CREATED' |
'ASSIGNED' |
'SUSPENDED' |
'CANCELLED';
export const TASK_COMPLETED_STATE: TaskStatus = 'COMPLETED';
export const TASK_CREATED_STATE: TaskStatus = 'CREATED';
export const TASK_ASSIGNED_STATE: TaskStatus = 'ASSIGNED';
export const TASK_SUSPENDED_STATE: TaskStatus = 'SUSPENDED';
export const TASK_CANCELLED_STATE: TaskStatus = 'CANCELLED';
export type TaskPermissions =
'VIEW' |
'CLAIM' |
'RELEASE' |
'UPDATE';
export const TASK_CLAIM_PERMISSION: TaskPermissions = 'CLAIM';
export const TASK_RELEASE_PERMISSION: TaskPermissions = 'RELEASE';
export const TASK_VIEW_PERMISSION: TaskPermissions = 'VIEW';
export const TASK_UPDATE_PERMISSION: TaskPermissions = 'UPDATE';

View File

@@ -57,7 +57,7 @@
(success)="onClaimTask()" (error)="onError($event)">
{{'ADF_CLOUD_TASK_FORM.EMPTY_FORM.BUTTONS.CLAIM' | translate}}
</button>
<button mat-button *ngIf="hasCandidateUsersOrGroups()" adf-cloud-unclaim-task [appName]="appName" [taskId]="taskId"
<button mat-button *ngIf="canUnclaimTask()" adf-cloud-unclaim-task [appName]="appName" [taskId]="taskId"
(success)="onUnclaimTask()" (error)="onError($event)">
{{'ADF_CLOUD_TASK_FORM.EMPTY_FORM.BUTTONS.UNCLAIM' | translate}}
</button>

View File

@@ -22,7 +22,14 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { IdentityUserService, setupTestBed } from '@alfresco/adf-core';
import { ProcessServiceCloudTestingModule } from '../../../testing/process-service-cloud.testing.module';
import { TaskFormCloudComponent } from './task-form-cloud.component';
import { TaskDetailsCloudModel } from '../../start-task/models/task-details-cloud.model';
import {
TaskDetailsCloudModel,
TASK_ASSIGNED_STATE,
TASK_CLAIM_PERMISSION,
TASK_CREATED_STATE,
TASK_RELEASE_PERMISSION,
TASK_VIEW_PERMISSION
} from '../../start-task/models/task-details-cloud.model';
import { TaskCloudService } from '../../services/task-cloud.service';
import { TranslateModule } from '@ngx-translate/core';
@@ -36,8 +43,9 @@ const taskDetails: TaskDetailsCloudModel = {
id: 'bd6b1741-6046-11e9-80f0-0a586460040d',
name: 'Task1',
owner: 'admin.adf',
standalone: true,
status: 'ASSIGNED'
standalone: false,
status: TASK_ASSIGNED_STATE,
permissions: [TASK_VIEW_PERMISSION]
};
describe('TaskFormCloudComponent', () => {
@@ -60,7 +68,10 @@ describe('TaskFormCloudComponent', () => {
});
beforeEach(() => {
taskDetails.status = 'ASSIGNED';
taskDetails.status = TASK_ASSIGNED_STATE;
taskDetails.permissions = [TASK_VIEW_PERMISSION];
taskDetails.standalone = false;
identityUserService = TestBed.inject(IdentityUserService);
getCurrentUserSpy = spyOn(identityUserService, 'getCurrentUserInfo').and.returnValue({ username: 'admin.adf' });
taskCloudService = TestBed.inject(TaskCloudService);
@@ -71,47 +82,33 @@ describe('TaskFormCloudComponent', () => {
fixture = TestBed.createComponent(TaskFormCloudComponent);
debugElement = fixture.debugElement;
component = fixture.componentInstance;
});
});
describe('Complete button', () => {
it('should show complete button when status is ASSIGNED', async () => {
component.appName = 'app1';
beforeEach(() => {
component.taskId = 'task1';
component.loadTask();
component.ngOnChanges({ appName: new SimpleChange(null, 'app1', false) });
fixture.detectChanges();
await fixture.whenStable();
const completeBtn = debugElement.query(By.css('[adf-cloud-complete-task]'));
expect(completeBtn.nativeElement).toBeDefined();
});
it('should not show complete button when status is ASSIGNED but assigned to a different person', async () => {
component.appName = 'app1';
component.taskId = 'task1';
it('should show complete button when status is ASSIGNED', () => {
const completeBtn = debugElement.query(By.css('[adf-cloud-complete-task]'));
expect(completeBtn.nativeElement).toBeDefined();
expect(completeBtn.nativeElement.innerText.trim()).toEqual('ADF_CLOUD_TASK_FORM.EMPTY_FORM.BUTTONS.COMPLETE');
});
it('should not show complete button when status is ASSIGNED but assigned to a different person', () => {
getCurrentUserSpy.and.returnValue({});
component.loadTask();
fixture.detectChanges();
await fixture.whenStable();
const completeBtn = debugElement.query(By.css('[adf-cloud-complete-task]'));
expect(completeBtn).toBeNull();
});
it('should not show complete button when showCompleteButton=false', async () => {
component.appName = 'app1';
component.taskId = 'task1';
it('should not show complete button when showCompleteButton=false', () => {
component.showCompleteButton = false;
component.loadTask();
fixture.detectChanges();
await fixture.whenStable();
const completeBtn = debugElement.query(By.css('[adf-cloud-complete-task]'));
expect(completeBtn).toBeNull();
@@ -120,88 +117,80 @@ describe('TaskFormCloudComponent', () => {
describe('Claim/Unclaim buttons', () => {
it('should not show release button for standalone task', async () => {
component.taskId = 'task1';
component.loadTask();
getTaskSpy.and.returnValue(of(taskDetails));
fixture.detectChanges();
await fixture.whenStable();
const unclaimBtn = debugElement.query(By.css('[adf-cloud-unclaim-task]'));
expect(unclaimBtn).toBeNull();
});
it('should show release button when task has candidate users and is assigned to one of these users', async () => {
beforeEach(() => {
spyOn(component, 'hasCandidateUsers').and.returnValue(true);
component.appName = 'app1';
getTaskSpy.and.returnValue(of(taskDetails));
component.taskId = 'task1';
component.loadTask();
component.ngOnChanges({ appName: new SimpleChange(null, 'app1', false) });
fixture.detectChanges();
await fixture.whenStable();
const unclaimBtn = debugElement.query(By.css('[adf-cloud-unclaim-task]'));
expect(unclaimBtn).not.toBeNull();
});
it('should not show unclaim button when status is ASSIGNED but assigned to different person', async () => {
component.appName = 'app1';
component.taskId = 'task1';
it('should not show release button for standalone task', () => {
taskDetails.permissions = [TASK_RELEASE_PERMISSION];
taskDetails.standalone = true;
getTaskSpy.and.returnValue(of(taskDetails));
fixture.detectChanges();
const unclaimBtn = debugElement.query(By.css('[adf-cloud-unclaim-task]'));
expect(unclaimBtn).toBeNull();
});
it('should show release button when task is assigned to one of the candidate users', () => {
taskDetails.permissions = [TASK_RELEASE_PERMISSION];
fixture.detectChanges();
const unclaimBtn = debugElement.query(By.css('[adf-cloud-unclaim-task]'));
expect(unclaimBtn.nativeElement).toBeDefined();
expect(unclaimBtn.nativeElement.innerText.trim()).toEqual('ADF_CLOUD_TASK_FORM.EMPTY_FORM.BUTTONS.UNCLAIM');
});
it('should not show unclaim button when status is ASSIGNED but assigned to different person', () => {
getCurrentUserSpy.and.returnValue({});
component.loadTask();
fixture.detectChanges();
await fixture.whenStable();
const unclaimBtn = debugElement.query(By.css('[adf-cloud-unclaim-task]'));
expect(unclaimBtn).toBeNull();
});
it('should not show unclaim button when status is not ASSIGNED', async () => {
component.appName = 'app1';
component.taskId = 'task1';
it('should not show unclaim button when status is not ASSIGNED', () => {
taskDetails.status = undefined;
getTaskSpy.and.returnValue(of(taskDetails));
component.loadTask();
fixture.detectChanges();
await fixture.whenStable();
const unclaimBtn = debugElement.query(By.css('[adf-cloud-unclaim-task]'));
expect(unclaimBtn).toBeNull();
});
it('should show claim button when status is CREATED', async () => {
component.appName = 'app1';
component.taskId = 'task1';
taskDetails.status = 'CREATED';
getTaskSpy.and.returnValue(of(taskDetails));
component.loadTask();
it('should not show unclaim button when status is ASSIGNED and permissions not include RELEASE', () => {
taskDetails.status = TASK_ASSIGNED_STATE;
taskDetails.permissions = [TASK_VIEW_PERMISSION];
fixture.detectChanges();
const unclaimBtn = debugElement.query(By.css('[adf-cloud-unclaim-task]'));
expect(unclaimBtn).toBeNull();
});
it('should show claim button when status is CREATED and permission includes CLAIM', () => {
taskDetails.status = TASK_CREATED_STATE;
taskDetails.permissions = [TASK_CLAIM_PERMISSION];
fixture.detectChanges();
await fixture.whenStable();
const claimBtn = debugElement.query(By.css('[adf-cloud-claim-task]'));
expect(claimBtn.nativeElement).toBeDefined();
expect(claimBtn.nativeElement.innerText.trim()).toEqual('ADF_CLOUD_TASK_FORM.EMPTY_FORM.BUTTONS.CLAIM');
});
it('should not show claim button when status is not CREATED', async () => {
component.appName = 'app1';
component.taskId = 'task1';
it('should not show claim button when status is not CREATED', () => {
taskDetails.status = undefined;
getTaskSpy.and.returnValue(of(taskDetails));
component.loadTask();
fixture.detectChanges();
await fixture.whenStable();
const claimBtn = debugElement.query(By.css('[adf-cloud-claim-task]'));
expect(claimBtn).toBeNull();
});
it('should not show claim button when status is CREATED and permission not includes CLAIM', () => {
taskDetails.status = TASK_CREATED_STATE;
taskDetails.permissions = [TASK_VIEW_PERMISSION];
fixture.detectChanges();
const claimBtn = debugElement.query(By.css('[adf-cloud-claim-task]'));
expect(claimBtn).toBeNull();
@@ -210,28 +199,23 @@ describe('TaskFormCloudComponent', () => {
describe('Cancel button', () => {
it('should show cancel button by default', async () => {
it('should show cancel button by default', () => {
component.appName = 'app1';
component.taskId = 'task1';
component.loadTask();
fixture.detectChanges();
await fixture.whenStable();
const cancelBtn = debugElement.query(By.css('#adf-cloud-cancel-task'));
expect(cancelBtn.nativeElement).toBeDefined();
expect(cancelBtn.nativeElement.innerText.trim()).toEqual('ADF_CLOUD_TASK_FORM.EMPTY_FORM.BUTTONS.CANCEL');
});
it('should not show cancel button when showCancelButton=false', async () => {
it('should not show cancel button when showCancelButton=false', () => {
component.appName = 'app1';
component.taskId = 'task1';
component.showCancelButton = false;
component.loadTask();
fixture.detectChanges();
await fixture.whenStable();
const cancelBtn = debugElement.query(By.css('#adf-cloud-cancel-task'));
expect(cancelBtn).toBeNull();
@@ -240,15 +224,12 @@ describe('TaskFormCloudComponent', () => {
describe('Inputs', () => {
it('should not show complete/claim/unclaim buttons when readOnly=true', async () => {
it('should not show complete/claim/unclaim buttons when readOnly=true', () => {
component.appName = 'app1';
component.taskId = 'task1';
component.readOnly = true;
component.loadTask();
fixture.detectChanges();
await fixture.whenStable();
const completeBtn = debugElement.query(By.css('[adf-cloud-complete-task]'));
expect(completeBtn).toBeNull();
@@ -261,6 +242,7 @@ describe('TaskFormCloudComponent', () => {
const cancelBtn = debugElement.query(By.css('#adf-cloud-cancel-task'));
expect(cancelBtn.nativeElement).toBeDefined();
expect(cancelBtn.nativeElement.innerText.trim()).toEqual('ADF_CLOUD_TASK_FORM.EMPTY_FORM.BUTTONS.CANCEL');
});
it('should load data when appName changes', () => {
@@ -288,15 +270,16 @@ describe('TaskFormCloudComponent', () => {
describe('Events', () => {
it('should emit cancelClick when cancel button is clicked', (done) => {
beforeEach(() => {
component.appName = 'app1';
component.taskId = 'task1';
});
it('should emit cancelClick when cancel button is clicked', (done) => {
component.cancelClick.subscribe(() => {
done();
});
component.loadTask();
fixture.detectChanges();
const cancelBtn = debugElement.query(By.css('#adf-cloud-cancel-task'));
@@ -304,17 +287,13 @@ describe('TaskFormCloudComponent', () => {
});
it('should emit taskCompleted when task is completed', (done) => {
spyOn(taskCloudService, 'completeTask').and.returnValue(of({}));
component.appName = 'app1';
component.taskId = 'task1';
component.taskCompleted.subscribe(() => {
done();
});
component.loadTask();
component.ngOnChanges({ appName: new SimpleChange(null, 'app1', false) });
fixture.detectChanges();
const completeBtn = debugElement.query(By.css('[adf-cloud-complete-task]'));
@@ -323,17 +302,16 @@ describe('TaskFormCloudComponent', () => {
it('should emit taskClaimed when task is claimed', (done) => {
spyOn(taskCloudService, 'claimTask').and.returnValue(of({}));
taskDetails.status = 'CREATED';
spyOn(component, 'hasCandidateUsers').and.returnValue(true);
taskDetails.status = TASK_CREATED_STATE;
taskDetails.permissions = [TASK_CLAIM_PERMISSION];
getTaskSpy.and.returnValue(of(taskDetails));
component.appName = 'app1';
component.taskId = 'task1';
component.taskClaimed.subscribe(() => {
done();
});
component.loadTask();
component.ngOnChanges({ appName: new SimpleChange(null, 'app1', false) });
fixture.detectChanges();
const claimBtn = debugElement.query(By.css('[adf-cloud-claim-task]'));
@@ -341,9 +319,6 @@ describe('TaskFormCloudComponent', () => {
});
it('should emit error when error occurs', (done) => {
component.appName = 'app1';
component.taskId = 'task1';
component.error.subscribe(() => {
done();
});
@@ -352,14 +327,10 @@ describe('TaskFormCloudComponent', () => {
});
it('should emit taskCompleted when task is completed', () => {
spyOn(taskCloudService, 'completeTask').and.returnValue(of({}));
const reloadSpy = spyOn(component, 'loadTask').and.callThrough();
const reloadSpy = spyOn(component, 'ngOnChanges').and.callThrough();
component.appName = 'app1';
component.taskId = 'task1';
component.loadTask();
component.ngOnChanges({ appName: new SimpleChange(null, 'app1', false) });
fixture.detectChanges();
const completeBtn = debugElement.query(By.css('[adf-cloud-complete-task]'));
@@ -369,15 +340,13 @@ describe('TaskFormCloudComponent', () => {
it('should emit taskClaimed when task is claimed', () => {
spyOn(taskCloudService, 'claimTask').and.returnValue(of({}));
const reloadSpy = spyOn(component, 'loadTask').and.callThrough();
taskDetails.status = 'CREATED';
spyOn(component, 'hasCandidateUsers').and.returnValue(true);
const reloadSpy = spyOn(component, 'ngOnChanges').and.callThrough();
taskDetails.permissions = [TASK_CLAIM_PERMISSION];
taskDetails.status = TASK_CREATED_STATE;
getTaskSpy.and.returnValue(of(taskDetails));
component.appName = 'app1';
component.taskId = 'task1';
component.loadTask();
component.ngOnChanges({ appName: new SimpleChange(null, 'app1', false) });
fixture.detectChanges();
const claimBtn = debugElement.query(By.css('[adf-cloud-claim-task]'));
@@ -387,16 +356,14 @@ describe('TaskFormCloudComponent', () => {
it('should emit taskUnclaimed when task is unclaimed', () => {
spyOn(taskCloudService, 'unclaimTask').and.returnValue(of({}));
const reloadSpy = spyOn(component, 'loadTask').and.callThrough();
const reloadSpy = spyOn(component, 'ngOnChanges').and.callThrough();
spyOn(component, 'hasCandidateUsers').and.returnValue(true);
taskDetails.status = 'ASSIGNED';
taskDetails.status = TASK_ASSIGNED_STATE;
taskDetails.permissions = [TASK_RELEASE_PERMISSION];
getTaskSpy.and.returnValue(of(taskDetails));
component.appName = 'app1';
component.taskId = 'task1';
component.loadTask();
component.ngOnChanges({ appName: new SimpleChange(null, 'app1', false) });
fixture.detectChanges();
const unclaimBtn = debugElement.query(By.css('[adf-cloud-unclaim-task]'));
@@ -415,10 +382,6 @@ describe('TaskFormCloudComponent', () => {
it('should not show loading template while task data is not being loaded', () => {
component.loading = false;
component.appName = 'app1';
component.taskId = 'task1';
component.loadTask();
fixture.detectChanges();
const loadingTemplate = debugElement.query(By.css('mat-progress-spinner'));

View File

@@ -133,7 +133,7 @@ export class TaskFormCloudComponent implements OnInit, OnChanges {
}
}
loadTask() {
private loadTask() {
this.loading = true;
this.taskCloudService
@@ -161,7 +161,7 @@ export class TaskFormCloudComponent implements OnInit, OnChanges {
}
canClaimTask(): boolean {
return !this.readOnly && this.taskCloudService.canClaimTask(this.taskDetails);
return !this.readOnly && this.taskCloudService.canClaimTask(this.taskDetails) && this.hasCandidateUsersOrGroups();
}
hasCandidateUsers(): boolean {
@@ -173,16 +173,11 @@ export class TaskFormCloudComponent implements OnInit, OnChanges {
}
hasCandidateUsersOrGroups(): boolean {
let hasCandidateUsersOrGroups = false;
if (this.taskDetails?.status === 'ASSIGNED') {
hasCandidateUsersOrGroups = this.hasCandidateUsers() || this.hasCandidateGroups();
}
return hasCandidateUsersOrGroups;
return this.hasCandidateUsers() || this.hasCandidateGroups();
}
canUnclaimTask(): boolean {
return !this.readOnly && this.taskCloudService.canUnclaimTask(this.taskDetails);
return !this.readOnly && this.taskCloudService.canUnclaimTask(this.taskDetails) && this.hasCandidateUsersOrGroups();
}
isReadOnly(): boolean {