[ACA-30333]FE - [Process-services] Create ADF task form. (#5611)

* [ACA-3033] FE - [Process-services] Create ADF task form.

* * Modifed task-from-template

* * Refactored details component with the task-form

* * Updated unit tests to the recent changes

* * Modified task-details component

* * Fixed failing tests

* * Fixed failing tests* Added doc

* * Fixed task-details-form e2e

* Fixed failing test on tas-details e2e

* * Fixed flaky process-services e2e

* * Fixed flaky e2e tests
This commit is contained in:
siva kumar
2020-04-22 12:58:03 +05:30
committed by GitHub
parent fde037498f
commit ea1454dde0
26 changed files with 1207 additions and 294 deletions

View File

@@ -336,5 +336,20 @@
"CHOOSE_ITEM": "Choose {{ name }} to...",
"CHOOSE_IN": "Choose file in {{ name }}..."
}
},
"ADF_TASK_FORM": {
"EMPTY_FORM": {
"SUBTITLE": "Attach a form that can be viewed later",
"COMPLETE-TASK-MESSAGE": "Task {{taskName}} completed",
"COMPLETE-TASK-SUB-MESSAGE": "No forms to be added",
"BUTTONS": {
"COMPLETE": "COMPLETE",
"CANCEL": "CANCEL"
}
},
"COMPLETED_TASK": {
"TITLE": "Task {{taskName}} completed",
"SUBTITLE": "No forms to be added"
}
}
}

View File

@@ -97,6 +97,46 @@ export let standaloneTaskWithoutForm = new TaskDetailsModel({
memberOfCandidateGroup: false
});
export let completedStandaloneTaskWithoutForm = new TaskDetailsModel({
id: '200',
name: 'Standalone Task Without Form',
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: new Date(),
duration: null,
priority: 50,
parentTaskId: null,
parentTaskName: null,
processInstanceId: null,
processInstanceName: null,
processDefinitionId: null,
processDefinitionName: null,
processDefinitionDescription: null,
processDefinitionKey: null,
processDefinitionCategory: null,
processDefinitionVersion: null,
processDefinitionDeploymentId: null,
formKey: null,
processInstanceStartUserId: null,
initiatorCanCompleteTask: false,
adhocTaskCanBeReassigned: false,
taskDefinitionKey: 'sid-DDECD9E4-0299-433F-9193-C3D905C3EEBE',
executionId: '86',
involvedGroups: [],
involvedPeople: [],
memberOfCandidateUsers: false,
managerOfCandidateGroup: false,
memberOfCandidateGroup: false
});
export let taskDetailsMock = new TaskDetailsModel({
id: '91',
name: 'Request translation',
@@ -137,6 +177,54 @@ export let taskDetailsMock = new TaskDetailsModel({
memberOfCandidateGroup: false
});
export let initiatorCanCompleteTaskDetailsMock = new TaskDetailsModel({
id: '91',
name: 'Request translation',
description: null,
category: null,
assignee: { email: 'mock-user-email' },
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',
processDefinitionDescription: null,
processDefinitionKey: 'TranslationProcess',
processDefinitionCategory: 'http://www.activiti.org/processdef',
processDefinitionVersion: 2,
processDefinitionDeploymentId: '5',
formKey: '4',
processInstanceStartUserId: '1001',
initiatorCanCompleteTask: true,
adhocTaskCanBeReassigned: false,
taskDefinitionKey: 'sid-DDECD9E4-0299-433F-9193-C3D905C3EEBE',
executionId: '86',
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'
}
],
memberOfCandidateUsers: false,
managerOfCandidateGroup: false,
memberOfCandidateGroup: false
});
export let taskDetailsWithOutAssigneeMock = new TaskDetailsModel({
id: '91',
name: 'Request translation',
@@ -183,6 +271,7 @@ export let claimableTaskDetailsMock = new TaskDetailsModel({
endDate: null,
duration: null,
priority: 50,
formKey: '4',
parentTaskId: null,
parentTaskName: null,
processInstanceId: '86',
@@ -353,6 +442,36 @@ export let taskDetailsWithOutCandidateGroup = new TaskDetailsModel({
]
});
export let completedTaskWithFormMock = 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: new Date(),
duration: null,
priority: 50,
formKey: '91',
parentTaskId: null,
parentTaskName: null,
processInstanceId: '86',
processInstanceName: null,
processDefinitionId: 'TranslationProcess:2:8',
processDefinitionName: 'Translation Process',
involvedGroups: [],
involvedPeople: [],
managerOfCandidateGroup: true,
memberOfCandidateGroup: true,
memberOfCandidateUsers: false
});
export let completedTaskDetailsMock = new TaskDetailsModel({
id: '91',
name: 'Request translation',
@@ -366,9 +485,10 @@ export let completedTaskDetailsMock = new TaskDetailsModel({
},
created: '2016-11-03T15:25:42.749+0000',
dueDate: null,
endDate: '2016-11-03T15:25:42.749+0000',
endDate: new Date(),
duration: null,
priority: 50,
formKey: null,
parentTaskId: null,
parentTaskName: null,
processInstanceId: '86',
@@ -382,6 +502,40 @@ export let completedTaskDetailsMock = new TaskDetailsModel({
memberOfCandidateUsers: false
});
export let taskDetailsWithOutFormMock = new TaskDetailsModel({
'id': '91',
'name': 'Request translation',
'description': 'fake description',
'category': null,
'assignee': {'id': 1001, 'firstName': 'Admin', 'lastName': 'Paul', 'email': 'my@mymail.com' },
'created': '2016-11-03T15:25:42.749+0000',
'dueDate': '2016-11-03T15:25:42.749+0000',
'endDate': null,
'duration': null,
'priority': 50,
'parentTaskId': null,
'parentTaskName': null,
'processInstanceId': '86',
'processInstanceName': null,
'processDefinitionId': 'TranslationProcess:2:8',
'processDefinitionName': 'Translation Process',
'processDefinitionDescription': null,
'processDefinitionKey': 'TranslationProcess',
'processDefinitionCategory': 'http://www.activiti.org/processdef',
'processDefinitionVersion': 2,
'processDefinitionDeploymentId': '5',
'formKey': null,
'processInstanceStartUserId': '1001',
'initiatorCanCompleteTask': false,
'adhocTaskCanBeReassigned': false,
'taskDefinitionKey': 'sid-DDECD9E4-0299-433F-9193-C3D905C3EEBE',
'executionId': '86',
'involvedPeople': [],
'memberOfCandidateUsers': false,
'managerOfCandidateGroup': false,
'memberOfCandidateGroup': false
});
export const taskFormMock = {
id: 4,
name: 'Translation request',

View File

@@ -11,6 +11,7 @@
@import '../content-widget/attach-file-widget-dialog.component';
@import '../form/start-form.component';
@import '../process-list/components/start-process.component';
@import '../task-list/components/task-form/task-form.component';
@mixin adf-process-services-theme($theme) {
@include adf-process-filters-theme($theme);
@@ -26,4 +27,5 @@
@include adf-attach-file-widget-dialog-component-theme($theme);
@include adf-start-form-component-theme($theme);
@include adf-process-services-create-theme($theme);
@include adf-task-form-theme($theme);
}

View File

@@ -1,7 +1,7 @@
<div class="adf-attach-form">
<mat-card>
<mat-card-content>
<div class="adf-no-form-message-container">
<div class="adf-attache-form-message-container">
<mat-card-title class="mat-card-title">
<h4 class="adf-form-title">{{ 'ADF_TASK_LIST.ATTACH_FORM.SELECT_FORM' | translate }}</h4>
</mat-card-title>
@@ -23,13 +23,13 @@
</div>
</mat-card-content>
<mat-card-actions class="adf-no-form-mat-card-actions">
<mat-card-actions class="adf-attach-form-mat-card-actions">
<div>
<button mat-button id="adf-no-form-remove-button" color="warn" *ngIf="formKey" (click)="onRemoveButtonClick()">{{ 'ADF_TASK_LIST.ATTACH_FORM.REMOVE_FORM' | translate }}</button>
<button mat-button id="adf-attach-form-remove-button" color="warn" *ngIf="formKey" (click)="onRemoveButtonClick()">{{ 'ADF_TASK_LIST.ATTACH_FORM.REMOVE_FORM' | translate }}</button>
</div>
<div>
<button mat-button id="adf-no-form-cancel-button" (click)="onCancelButtonClick()">{{ 'ADF_TASK_LIST.START_TASK.FORM.ACTION.CANCEL' | translate }}</button>
<button mat-button id="adf-no-form-attach-form-button" [disabled]="disableSubmit" color="primary" (click)="onAttachFormButtonClick()">{{ 'ADF_TASK_LIST.START_TASK.FORM.LABEL.ATTACHFORM' | translate }}</button>
<button mat-button id="adf-attach-form-cancel-button" (click)="onCancelButtonClick()">{{ 'ADF_TASK_LIST.START_TASK.FORM.ACTION.CANCEL' | translate }}</button>
<button mat-button id="adf-attach-form-attach-button" [disabled]="disableSubmit" color="primary" (click)="onAttachFormButtonClick()">{{ 'ADF_TASK_LIST.START_TASK.FORM.LABEL.ATTACHFORM' | translate }}</button>
</div>
</mat-card-actions>
</mat-card>

View File

@@ -9,7 +9,7 @@
margin: 20px 0;
}
.adf-no-form-mat-card-actions {
.adf-attach-form-mat-card-actions {
justify-content: space-between;
margin-top: 30px;
text-align: right;

View File

@@ -51,7 +51,7 @@ describe('AttachFormComponent', () => {
it('should show the attach button disabled', async(() => {
fixture.detectChanges();
fixture.whenStable().then(() => {
const attachButton = fixture.debugElement.query(By.css('#adf-no-form-attach-form-button'));
const attachButton = fixture.debugElement.query(By.css('#adf-attach-form-attach-button'));
expect(attachButton.nativeElement.disabled).toBeTruthy();
});
}));
@@ -60,7 +60,7 @@ describe('AttachFormComponent', () => {
fixture.detectChanges();
fixture.whenStable().then(() => {
const emitSpy = spyOn(component.cancelAttachForm, 'emit');
const el = fixture.nativeElement.querySelector('#adf-no-form-cancel-button');
const el = fixture.nativeElement.querySelector('#adf-attach-form-cancel-button');
el.click();
expect(emitSpy).toHaveBeenCalled();
});
@@ -72,8 +72,8 @@ describe('AttachFormComponent', () => {
spyOn(taskService, 'attachFormToATask').and.returnValue(of(true));
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(element.querySelector('#adf-no-form-attach-form-button')).toBeDefined();
const el = fixture.nativeElement.querySelector('#adf-no-form-attach-form-button');
expect(element.querySelector('#adf-attach-form-attach-button')).toBeDefined();
const el = fixture.nativeElement.querySelector('#adf-attach-form-attach-button');
el.click();
expect(taskService.attachFormToATask).toHaveBeenCalledWith(1, 2);
});
@@ -87,7 +87,7 @@ describe('AttachFormComponent', () => {
spyOn(taskService, 'attachFormToATask').and.returnValue(of(true));
fixture.detectChanges();
fixture.whenStable().then(() => {
const attachButton = fixture.debugElement.query(By.css('#adf-no-form-attach-form-button'));
const attachButton = fixture.debugElement.query(By.css('#adf-attach-form-attach-button'));
expect(attachButton.nativeElement.disabled).toBeFalsy();
});
}));
@@ -103,7 +103,7 @@ describe('AttachFormComponent', () => {
fixture.detectChanges();
component.attachFormControl.setValue(2);
fixture.detectChanges();
const attachButton = fixture.debugElement.query(By.css('#adf-no-form-attach-form-button'));
const attachButton = fixture.debugElement.query(By.css('#adf-attach-form-attach-button'));
expect(attachButton.nativeElement.disabled).toBeTruthy();
});
}));
@@ -140,8 +140,8 @@ describe('AttachFormComponent', () => {
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(element.querySelector('#adf-no-form-remove-button')).toBeDefined();
const el = fixture.nativeElement.querySelector('#adf-no-form-remove-button');
expect(element.querySelector('#adf-attach-form-remove-button')).toBeDefined();
const el = fixture.nativeElement.querySelector('#adf-attach-form-remove-button');
el.click();
expect(component.formId).toBeNull();
});
@@ -163,7 +163,7 @@ describe('AttachFormComponent', () => {
fixture.detectChanges();
fixture.whenStable().then(() => {
const emitSpy = spyOn(component.success, 'emit');
const el = fixture.nativeElement.querySelector('#adf-no-form-attach-form-button');
const el = fixture.nativeElement.querySelector('#adf-attach-form-attach-button');
el.click();
expect(emitSpy).toHaveBeenCalled();
});

View File

@@ -17,19 +17,14 @@
import { NoTaskDetailsTemplateDirective } from './no-task-detail-template.directive';
import { TaskDetailsComponent } from './task-details.component';
import { AuthenticationService } from '@alfresco/adf-core';
import { of } from 'rxjs';
describe('NoTaskDetailsTemplateDirective', () => {
let component: NoTaskDetailsTemplateDirective;
let detailsComponent: TaskDetailsComponent;
let authService: AuthenticationService;
beforeEach(() => {
authService = new AuthenticationService(null, null, null, null, null);
spyOn(authService, 'getBpmLoggedUser').and.returnValue(of({ email: 'fake-email' }));
detailsComponent = new TaskDetailsComponent(null, authService, null, null, null, null);
detailsComponent = new TaskDetailsComponent(null, null, null, null, null);
component = new NoTaskDetailsTemplateDirective(detailsComponent);
});

View File

@@ -22,51 +22,21 @@
<div class="adf-task-details-core-form">
<div *ngIf="isAssigned()">
<adf-form *ngIf="isFormComponentVisible()" #activitiForm
<adf-task-form
[taskId]="taskDetails.id"
[showTitle]="showFormTitle"
[showRefreshButton]="showFormRefreshButton"
[showCompleteButton]="showFormCompleteButton"
[disableCompleteButton]="!isCompleteButtonEnabled()"
[showSaveButton]="isSaveButtonVisible()"
[readOnly]="internalReadOnlyForm"
[showFormTitle]="showFormTitle"
[showFormRefreshButton]="showFormRefreshButton"
[showCancelButton]="true"
[fieldValidators]="fieldValidators"
(formSaved)='onFormSaved($event)'
(formCompleted)='onFormCompleted($event)'
(formContentClicked)='onFormContentClick($event)'
(formLoaded)='onFormLoaded($event)'
(error)='onFormError($event)'
(executeOutcome)='onFormExecuteOutcome($event)'>
</adf-form>
<adf-task-standalone *ngIf="isTaskStandaloneComponentVisible()"
[taskName]="taskDetails.name"
[taskId]="taskDetails.id"
[isCompleted]="isCompletedTask()"
[hasCompletePermission]="isCompleteButtonEnabled()"
[hideCancelButton]="true"
(complete)="onComplete()"
(showAttachForm)="onShowAttachForm()">
</adf-task-standalone>
<mat-card class="adf-message-card" *ngIf="!isTaskStandaloneComponentVisible() && !isCompletedTask() && !isFormComponentVisible()" >
<mat-card-content>
<div class="adf-no-form-message-container">
<div class="adf-no-form-message-list">
<div *ngIf="!isCompletedTask()" class="adf-no-form-message">
<span id="adf-no-form-message">{{'ADF_TASK_LIST.STANDALONE_TASK.NO_FORM_MESSAGE' | translate}}</span>
</div>
</div>
</div>
</mat-card-content>
<mat-card-actions class="adf-no-form-mat-card-actions">
<div>
<button mat-button id="adf-no-form-complete-button" color="primary" (click)="onComplete()">{{ 'ADF_TASK_LIST.DETAILS.BUTTON.COMPLETE' | translate }}</button>
</div>
</mat-card-actions>
</mat-card>
(completed)="onComplete()"
(showAttachForm)="onShowAttachForm()"
(executeOutcome)='onFormExecuteOutcome($event)'
(error)="onFormError($event)" #activitiTaskForm>
</adf-task-form>
<adf-attach-form *ngIf="isShowAttachForm()"
[taskId]="taskDetails.id"
[formKey]="taskDetails.formKey"

View File

@@ -36,8 +36,6 @@ import { TaskDetailsModel } from '../models/task-details.model';
import {
noDataMock,
taskDetailsMock,
standaloneTaskWithForm,
standaloneTaskWithoutForm,
taskFormMock,
tasksMock,
taskDetailsWithOutAssigneeMock
@@ -64,7 +62,6 @@ describe('TaskDetailsComponent', () => {
let getTaskDetailsSpy: jasmine.Spy;
let getTasksSpy: jasmine.Spy;
let assignTaskSpy: jasmine.Spy;
let completeTaskSpy: jasmine.Spy;
let logService: LogService;
let commentProcessService: CommentProcessService;
let peopleProcessService: PeopleProcessService;
@@ -99,7 +96,6 @@ describe('TaskDetailsComponent', () => {
getTasksSpy = spyOn(service, 'getTasks').and.returnValue(of(tasksMock));
assignTaskSpy = spyOn(service, 'assignTask').and.returnValue(of(fakeUser));
completeTaskSpy = spyOn(service, 'completeTask').and.returnValue(of({}));
commentProcessService = TestBed.get(CommentProcessService);
authService = TestBed.get(AuthenticationService);
@@ -175,42 +171,6 @@ describe('TaskDetailsComponent', () => {
});
}));
it('should display task standalone component when the task does not have an associated form', async(() => {
component.taskId = '123';
getTaskDetailsSpy.and.returnValue(of(standaloneTaskWithoutForm));
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(component.isStandaloneTaskWithoutForm()).toBeTruthy();
expect(fixture.debugElement.query(By.css('adf-task-standalone'))).not.toBeNull();
});
}));
it('should not display task standalone component when the task has a form', async(() => {
component.taskId = '123';
getTaskDetailsSpy.and.returnValue(of(standaloneTaskWithForm));
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(component.isStandaloneTaskWithForm()).toBeTruthy();
expect(fixture.debugElement.query(By.css('adf-task-standalone'))).toBeDefined();
expect(fixture.debugElement.query(By.css('adf-task-standalone'))).not.toBeNull();
});
}));
it('should display the AttachFormComponent when standaloneTaskWithForm and click on attach button', async(() => {
component.taskId = '123';
getTaskDetailsSpy.and.returnValue(of(standaloneTaskWithForm));
fixture.detectChanges();
component.onShowAttachForm();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(component.isStandaloneTaskWithForm()).toBeTruthy();
expect(fixture.debugElement.query(By.css('adf-attach-form'))).toBeDefined();
});
}));
it('should display the claim message when the task is not assigned', async(() => {
component.taskDetails = taskDetailsWithOutAssigneeMock;
fixture.detectChanges();
@@ -229,86 +189,6 @@ describe('TaskDetailsComponent', () => {
});
}));
describe('and form with visiblity', () => {
beforeEach(async () => {
component.taskId = '123';
spyOn(formService, 'completeTaskForm').and.returnValue(of({}));
taskDetailsMock.formKey = '4';
getTaskDetailsSpy.and.returnValue(of(taskDetailsMock));
fixture.detectChanges();
await fixture.whenStable();
});
it('[C312410] - Should be possible to complete a task that has an invisible field on a form with a value', async (done) => {
component.formCompleted.subscribe((form: FormModel) => {
expect(form.id).toBe(taskFormMock.id);
done();
});
component.taskDetails.initiatorCanCompleteTask = true;
component.showNextTask = false;
fixture.detectChanges();
await fixture.whenStable();
const inputTextOne: HTMLInputElement = fixture.nativeElement.querySelector('#text1');
expect(inputTextOne).toBeDefined();
expect(inputTextOne).not.toBeNull();
const inputTextTwo: HTMLInputElement = fixture.nativeElement.querySelector('#text2');
expect(inputTextTwo).toBeDefined();
expect(inputTextTwo).not.toBeNull();
let inputTextThree: HTMLInputElement = fixture.nativeElement.querySelector('#text3');
expect(inputTextThree).toBeDefined();
expect(inputTextThree).not.toBeNull();
inputTextOne.value = 'a';
inputTextOne.dispatchEvent(new Event('input'));
inputTextTwo.value = 'a';
inputTextTwo.dispatchEvent(new Event('input'));
inputTextThree.value = 'a';
inputTextThree.dispatchEvent(new Event('input'));
fixture.detectChanges();
await fixture.whenStable();
inputTextThree = fixture.nativeElement.querySelector('#text3');
expect(inputTextThree).toBeDefined();
expect(inputTextThree).not.toBeNull();
inputTextOne.value = 'b';
inputTextOne.dispatchEvent(new Event('input'));
fixture.detectChanges();
await fixture.whenStable();
const inputThreeContainer = fixture.nativeElement.querySelector('#field-text3-container');
expect(inputThreeContainer.hidden).toBe(true);
const completeOutcomeButton: HTMLButtonElement = fixture.nativeElement.querySelector('#adf-form-complete');
expect(completeOutcomeButton.hidden).toBe(false);
completeOutcomeButton.click();
fixture.detectChanges();
});
it('[C277278] - Should show if the form is valid via the validation icon', async () => {
const numberInput: HTMLInputElement = fixture.nativeElement.querySelector('#numberField');
let validationForm = fixture.nativeElement.querySelector('#adf-valid-form-icon');
expect(numberInput).toBeDefined();
expect(numberInput).not.toBeNull();
expect(validationForm.textContent).toBe('check_circle');
numberInput.value = 'a';
numberInput.dispatchEvent(new Event('input'));
fixture.detectChanges();
await fixture.whenStable();
const invalidForm = fixture.nativeElement.querySelector('#adf-invalid-form-icon');
expect(invalidForm).not.toBeNull();
expect(invalidForm.textContent).toBe('error');
numberInput.value = '4';
numberInput.dispatchEvent(new Event('input'));
fixture.detectChanges();
await fixture.whenStable();
validationForm = fixture.nativeElement.querySelector('#adf-valid-form-icon');
expect(validationForm.textContent).toBe('check_circle');
});
});
describe('change detection', () => {
let change;
@@ -414,11 +294,6 @@ describe('TaskDetailsComponent', () => {
expect(getTasksSpy).not.toHaveBeenCalled();
});
it('should call service to complete task when complete button clicked', () => {
component.onComplete();
expect(completeTaskSpy).toHaveBeenCalled();
});
it('should emit a complete event when complete button clicked and task completed', () => {
const emitSpy: jasmine.Spy = spyOn(component.formCompleted, 'emit');
component.onComplete();

View File

@@ -17,7 +17,6 @@
import {
PeopleProcessService, UserProcessModel,
AuthenticationService,
CardViewUpdateService,
ClickNotification,
LogService,
@@ -42,8 +41,8 @@ import { Observable, Observer, of, Subject } from 'rxjs';
import { TaskQueryRequestRepresentationModel } from '../models/filter.model';
import { TaskDetailsModel } from '../models/task-details.model';
import { TaskListService } from './../services/tasklist.service';
import { UserRepresentation } from '@alfresco/js-api';
import { catchError, share, takeUntil } from 'rxjs/operators';
import { TaskFormComponent } from './task-form/task-form.component';
@Component({
selector: 'adf-task-details',
@@ -61,6 +60,9 @@ export class TaskDetailsComponent implements OnInit, OnChanges, OnDestroy {
@ViewChild('errorDialog')
errorDialog: TemplateRef<any>;
@ViewChild('activitiTaskForm')
taskFormComponent: TaskFormComponent;
/** Toggles debug mode. */
@Input()
debugMode: boolean = false;
@@ -179,11 +181,9 @@ export class TaskDetailsComponent implements OnInit, OnChanges, OnDestroy {
peopleSearch: Observable<UserProcessModel[]>;
currentLoggedUser: UserRepresentation;
data: any;
constructor(private taskListService: TaskListService,
private authService: AuthenticationService,
private peopleProcessService: PeopleProcessService,
private logService: LogService,
private cardViewUpdateService: CardViewUpdateService,
@@ -192,9 +192,6 @@ export class TaskDetailsComponent implements OnInit, OnChanges, OnDestroy {
ngOnInit() {
this.peopleSearch = new Observable<UserProcessModel[]>((observer) => this.peopleSearchObserver = observer).pipe(share());
this.authService.getBpmLoggedUser().subscribe(user => {
this.currentLoggedUser = user;
});
if (this.taskId) {
this.loadDetails(this.taskId);
@@ -225,26 +222,6 @@ export class TaskDetailsComponent implements OnInit, OnChanges, OnDestroy {
}
}
isStandaloneTask(): boolean {
return !(this.taskDetails && (!!this.taskDetails.processDefinitionId));
}
isStandaloneTaskWithForm(): boolean {
return this.isStandaloneTask() && this.hasFormKey();
}
isStandaloneTaskWithoutForm(): boolean {
return this.isStandaloneTask() && !this.hasFormKey();
}
isFormComponentVisible(): boolean {
return this.hasFormKey() && !this.isShowAttachForm();
}
isTaskStandaloneComponentVisible(): boolean {
return this.isStandaloneTaskWithoutForm() && !this.isShowAttachForm();
}
isShowAttachForm(): boolean {
return this.showAttachForm;
}
@@ -256,13 +233,6 @@ export class TaskDetailsComponent implements OnInit, OnChanges, OnDestroy {
this.taskDetails = null;
}
/**
* Check if the task has a form
*/
hasFormKey(): boolean {
return (this.taskDetails && (!!this.taskDetails.formKey));
}
isTaskActive() {
return this.taskDetails && this.taskDetails.duration === null;
}
@@ -329,44 +299,6 @@ export class TaskDetailsComponent implements OnInit, OnChanges, OnDestroy {
return !!this.taskDetails.assignee;
}
private hasEmailAddress(): boolean {
return this.taskDetails.assignee.email ? true : false;
}
isAssignedToMe(): boolean {
return this.isAssigned() && this.hasEmailAddress() ?
this.isEmailEqual(this.taskDetails.assignee.email, this.currentLoggedUser.email) :
this.isExternalIdEqual(this.taskDetails.assignee.externalId, this.currentLoggedUser.externalId);
}
private isEmailEqual(assigneeMail: string, currentLoggedEmail: string): boolean {
return assigneeMail.toLocaleLowerCase() === currentLoggedEmail.toLocaleLowerCase();
}
private isExternalIdEqual(assigneeExternalId: string, currentUserExternalId: string): boolean {
return assigneeExternalId.toLocaleLowerCase() === currentUserExternalId.toLocaleLowerCase();
}
isCompleteButtonEnabled(): boolean {
return this.isAssignedToMe() || this.canInitiatorComplete();
}
isCompleteButtonVisible(): boolean {
return !this.hasFormKey() && this.isTaskActive() && this.isCompleteButtonEnabled();
}
canInitiatorComplete(): boolean {
return this.taskDetails.initiatorCanCompleteTask;
}
isSaveButtonVisible(): boolean {
return this.hasSaveButton() && (!this.canInitiatorComplete() || this.isAssignedToMe());
}
hasSaveButton(): boolean {
return this.showFormSaveButton;
}
/**
* Retrieve the next open task
* @param processInstanceId
@@ -395,9 +327,7 @@ export class TaskDetailsComponent implements OnInit, OnChanges, OnDestroy {
* Complete button clicked
*/
onComplete(): void {
this.taskListService
.completeTask(this.taskId)
.subscribe(() => this.onFormCompleted(null));
this.onFormCompleted(null);
}
onShowAttachForm() {
@@ -410,6 +340,7 @@ export class TaskDetailsComponent implements OnInit, OnChanges, OnDestroy {
onCompleteAttachForm() {
this.showAttachForm = false;
this.taskFormComponent.loadTask(this.taskId);
this.loadDetails(this.taskId);
}
@@ -464,10 +395,6 @@ export class TaskDetailsComponent implements OnInit, OnChanges, OnDestroy {
this.loadDetails(taskId);
}
isCompletedTask(): boolean {
return this.taskDetails && this.taskDetails.endDate ? true : undefined;
}
searchUser(searchedWord: string) {
this.peopleProcessService.getWorkflowUsers(null, searchedWord)
.subscribe(

View File

@@ -0,0 +1,74 @@
<ng-container *ngIf="!loading; else loadingTemplate">
<adf-form *ngIf="hasFormKey(); else withoutForm"
[taskId]="taskDetails?.id"
[showTitle]="showFormTitle"
[showValidationIcon]="showFormValidationIcon"
[showRefreshButton]="showFormRefreshButton"
[showCompleteButton]="showFormCompleteButton"
[disableCompleteButton]="!isCompleteButtonEnabled()"
[showSaveButton]="isSaveButtonVisible()"
[readOnly]="isReadOnlyForm()"
[fieldValidators]="fieldValidators"
(formSaved)='onFormSaved($event)'
(formCompleted)='onFormCompleted($event)'
(formContentClicked)='onFormContentClick($event)'
(formLoaded)='onFormLoaded($event)'
(formError)='onFormError($event)'
(error)='onError($event)'
(executeOutcome)='onFormExecuteOutcome($event)'>
</adf-form>
<ng-template #withoutForm>
<adf-task-standalone *ngIf="isStandaloneTask(); else emptyFormMessage"
[taskName]="taskDetails.name"
[taskId]="taskDetails.id"
[isCompleted]="isCompletedTask()"
[hasCompletePermission]="isCompleteButtonVisible()"
[hideCancelButton]="showCancelButton"
(complete)="onCompleteTask()"
(showAttachForm)="onShowAttachForm()">
</adf-task-standalone>
<ng-template #emptyFormMessage>
<mat-card class="adf-task-form-container">
<mat-card-header>
<mat-card-title>
<h4>
<span class="adf-form-title">
{{taskDetails.name}}
<ng-container *ngIf="!taskDetails.name">
{{'FORM.FORM_RENDERER.NAMELESS_TASK' | translate}}
</ng-container>
</span>
</h4>
</mat-card-title>
</mat-card-header>
<mat-card-content>
<adf-empty-content *ngIf="isCompletedTask(); else emptyFormTemplate"
[icon]="'description'"
[title]="getCompletedTaskTranslatedMessage() | async"
[subtitle]="'ADF_TASK_FORM.COMPLETED_TASK.SUBTITLE'">
</adf-empty-content>
<ng-template #emptyFormTemplate>
<adf-empty-content
[icon]="'description'"
[title]="'ADF_TASK_LIST.STANDALONE_TASK.NO_FORM_MESSAGE'"
[subtitle]="'ADF_TASK_FORM.EMPTY_FORM.SUBTITLE'">
</adf-empty-content>
</ng-template>
</mat-card-content>
<mat-card-actions class="adf-task-form-actions">
<button id="adf-no-form-cancel-button" mat-button *ngIf="showCancelButton" (click)="onCancel()">
{{'ADF_TASK_FORM.EMPTY_FORM.BUTTONS.CANCEL' | translate}}
</button>
<button mat-button *ngIf="!isCompletedTask()" color="primary" (click)="onCompleteTask()" id="adf-no-form-complete-button">
{{'ADF_TASK_FORM.EMPTY_FORM.BUTTONS.COMPLETE' | translate}}
</button>
</mat-card-actions>
</mat-card>
</ng-template>
</ng-template>
</ng-container>
<ng-template #loadingTemplate>
<div fxLayout="row" fxLayoutAlign="center stretch">
<mat-spinner></mat-spinner>
</div>
</ng-template>

View File

@@ -0,0 +1,32 @@
@mixin adf-task-form-theme($theme) {
$config: mat-typography-config();
.adf-task-form {
&-container {
overflow: hidden;
}
&-actions {
float: right;
padding-bottom: 25px !important;
padding-right: 25px !important;
& .mat-button {
height: 36px;
border-radius: 5px;
}
& .mat-button-wrapper {
width: 58px;
height: 20px;
opacity: 0.54;
font-size: mat-font-size($config, body-2);
font-weight: bold;
}
}
}
}

View File

@@ -0,0 +1,504 @@
/*!
* @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 { ComponentFixture, TestBed } from '@angular/core/testing';
import { TaskFormComponent } from './task-form.component';
import {
setupTestBed,
TranslationService,
TranslationMock,
FormService,
AuthenticationService,
FormModel,
FormOutcomeEvent,
FormOutcomeModel
} from '@alfresco/adf-core';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { TaskListService } from '../../services/tasklist.service';
import { TranslateStore } from '@ngx-translate/core';
import { NO_ERRORS_SCHEMA, SimpleChange } from '@angular/core';
import { of, throwError } from 'rxjs';
import {
taskFormMock,
taskDetailsMock,
completedTaskDetailsMock,
taskDetailsWithOutFormMock,
standaloneTaskWithoutForm,
completedStandaloneTaskWithoutForm,
claimableTaskDetailsMock,
initiatorCanCompleteTaskDetailsMock
} from '../../../mock/task/task-details.mock';
import { TaskDetailsModel } from '../../models/task-details.model';
import { TaskListModule } from '../../task-list.module';
describe('TaskFormComponent', () => {
let component: TaskFormComponent;
let fixture: ComponentFixture<TaskFormComponent>;
let formService: FormService;
let taskListService: TaskListService;
let getTaskDetailsSpy: jasmine.Spy;
let completeTaskSpy: jasmine.Spy;
let element: HTMLElement;
let authService: AuthenticationService;
let getBpmLoggedUserSpy: jasmine.Spy;
setupTestBed({
imports: [
NoopAnimationsModule,
TaskListModule
],
providers: [
{ provide: TranslationService, useClass: TranslationMock },
TranslateStore],
schemas: [NO_ERRORS_SCHEMA]
});
beforeEach(() => {
fixture = TestBed.createComponent(TaskFormComponent);
component = fixture.componentInstance;
element = fixture.nativeElement;
taskListService = TestBed.get(TaskListService);
formService = TestBed.get(FormService);
getTaskDetailsSpy = spyOn(taskListService, 'getTaskDetails').and.returnValue(of(taskDetailsMock));
completeTaskSpy = spyOn(taskListService, 'completeTask').and.returnValue(of({}));
spyOn(formService, 'getTaskForm').and.returnValue(of(taskFormMock));
taskDetailsMock.processDefinitionId = null;
spyOn(formService, 'getTask').and.returnValue(of(taskDetailsMock));
authService = TestBed.get(AuthenticationService);
getBpmLoggedUserSpy = spyOn(authService, 'getBpmLoggedUser').and.returnValue(of({ email: 'fake-email' }));
});
afterEach(async() => {
await fixture.whenStable();
getTaskDetailsSpy.calls.reset();
fixture.destroy();
});
describe('Task with form', () => {
beforeEach(async() => {
await fixture.whenStable();
getTaskDetailsSpy.calls.reset();
});
it('Should be able to display task form', async () => {
component.taskId = '123';
taskDetailsMock.formKey = '4';
component.currentLoggedUser = taskDetailsMock.assignee;
getTaskDetailsSpy.and.returnValue(of(taskDetailsMock));
fixture.detectChanges();
await fixture.whenStable();
const activitFormSelector = element.querySelector('adf-form');
const inputFieldOne = fixture.debugElement.nativeElement.querySelector('#text1');
const inputFieldTwo = fixture.debugElement.nativeElement.querySelector('#text2');
const inputFieldThree = fixture.debugElement.nativeElement.querySelector('#text3');
expect(activitFormSelector).toBeDefined();
expect(inputFieldOne['disabled']).toEqual(false);
expect(inputFieldTwo['disabled']).toEqual(false);
expect(inputFieldThree['disabled']).toEqual(false);
});
it('Should be able to complete assigned task', async () => {
getBpmLoggedUserSpy.and.returnValue(of({ id: 1001, firstName: 'Wilbur', lastName: 'Adams', email: 'wilbur@app.activiti.com' }));
getTaskDetailsSpy.and.returnValue(of(taskDetailsMock));
const formCompletedSpy: jasmine.Spy = spyOn(component.formCompleted, 'emit');
const completeTaskFormSpy = spyOn(formService, 'completeTaskForm').and.returnValue(of({}));
component.taskId = '123';
component.ngOnInit();
fixture.detectChanges();
await fixture.whenStable();
const completeButton = fixture.debugElement.nativeElement.querySelector('#adf-form-complete');
expect(completeButton['disabled']).toEqual(false);
completeButton.click();
expect(completeTaskFormSpy).toHaveBeenCalled();
expect(formCompletedSpy).toHaveBeenCalled();
});
it('Should be able to complete the task as a process initiator', async () => {
const formCompletedSpy: jasmine.Spy = spyOn(component.formCompleted, 'emit');
const completeTaskFormSpy = spyOn(formService, 'completeTaskForm').and.returnValue(of({}));
getTaskDetailsSpy.and.returnValue(of(initiatorCanCompleteTaskDetailsMock));
component.taskId = '123';
fixture.detectChanges();
await fixture.whenStable();
const activitFormSelector = element.querySelector('adf-form');
const completeButton = fixture.debugElement.nativeElement.querySelector('#adf-form-complete');
expect(activitFormSelector).toBeDefined();
expect(completeButton['disabled']).toEqual(false);
completeButton.click();
expect(completeTaskFormSpy).toHaveBeenCalled();
expect(formCompletedSpy).toHaveBeenCalled();
});
it('Should emit error event in case form complete service fails', async () => {
const errorSpy: jasmine.Spy = spyOn(component.error, 'emit');
const completeTaskFormSpy = spyOn(formService, 'completeTaskForm').and.returnValue(throwError({message: 'servce failed'}));
getTaskDetailsSpy.and.returnValue(of(initiatorCanCompleteTaskDetailsMock));
component.taskId = '123';
fixture.detectChanges();
await fixture.whenStable();
const activitFormSelector = element.querySelector('adf-form');
const completeButton = fixture.debugElement.nativeElement.querySelector('#adf-form-complete');
expect(activitFormSelector).toBeDefined();
expect(completeButton['disabled']).toEqual(false);
completeButton.click();
expect(completeTaskFormSpy).toHaveBeenCalled();
expect(errorSpy).toHaveBeenCalled();
});
});
describe('change detection', () => {
beforeEach(async() => {
component.taskId = '123';
fixture.detectChanges();
await fixture.whenStable();
getTaskDetailsSpy.calls.reset();
});
it('should fetch new task details when taskId changed', () => {
const change = new SimpleChange('123', '456', true);
component.ngOnChanges({ 'taskId': change });
fixture.detectChanges();
expect(getTaskDetailsSpy).toHaveBeenCalledWith('123');
});
it('should NOT fetch new task details when taskId changed to null', async () => {
const nullChange = new SimpleChange('123', null, true);
component.ngOnChanges({ 'taskId': nullChange });
fixture.detectChanges();
await fixture.whenStable();
expect(getTaskDetailsSpy).not.toHaveBeenCalled();
});
});
describe('Task assigned to candidates', () => {
beforeEach(async() => {
component.taskId = '123';
fixture.detectChanges();
await fixture.whenStable();
getTaskDetailsSpy.calls.reset();
});
it('Should be able to display form in readonly mode if the task assigned to candidates', async() => {
getTaskDetailsSpy.and.returnValue(of(claimableTaskDetailsMock));
fixture.detectChanges();
await fixture.whenStable();
const activitFormSelector = element.querySelector('adf-form');
const inputFieldOne = fixture.debugElement.nativeElement.querySelector('#text1');
const inputFieldTwo = fixture.debugElement.nativeElement.querySelector('#text2');
const inputFieldThree = fixture.debugElement.nativeElement.querySelector('#text3');
expect(activitFormSelector).toBeDefined();
expect(inputFieldOne['disabled']).toEqual(true);
expect(inputFieldTwo['disabled']).toEqual(true);
expect(inputFieldThree['disabled']).toEqual(true);
});
});
describe('Form events', () => {
beforeEach(() => {
component.taskId = '123';
fixture.detectChanges();
});
it('Should emit a save event when form saved', () => {
const formSavedSpy: jasmine.Spy = spyOn(component.formSaved, 'emit');
component.onFormSaved(new FormModel());
expect(formSavedSpy).toHaveBeenCalled();
});
it('Should emit a outcome execution event when form outcome executed', () => {
const executeOutcomeSpy: jasmine.Spy = spyOn(component.executeOutcome, 'emit');
component.onFormExecuteOutcome(new FormOutcomeEvent(new FormOutcomeModel(new FormModel())));
expect(executeOutcomeSpy).toHaveBeenCalled();
});
it('Should emit a complete event when form completed', () => {
const formCompletedSpy: jasmine.Spy = spyOn(component.formCompleted, 'emit');
component.onFormCompleted(new FormModel());
expect(formCompletedSpy).toHaveBeenCalled();
});
it('Should call service to complete task when complete button clicked', () => {
component.onCompleteTask();
expect(completeTaskSpy).toHaveBeenCalled();
});
it('Should emit a complete event when complete button clicked and task completed', () => {
const completeSpy: jasmine.Spy = spyOn(component.completed, 'emit');
component.onCompleteTask();
expect(completeSpy).toHaveBeenCalled();
});
it('Should emit a load event when form loaded', () => {
const formLoadedSpy: jasmine.Spy = spyOn(component.formLoaded, 'emit');
component.onFormLoaded(new FormModel());
expect(formLoadedSpy).toHaveBeenCalled();
});
it('Should emit an error event when form error occurs', () => {
const formErrorSpy: jasmine.Spy = spyOn(component.formError, 'emit');
component.onFormError({});
expect(formErrorSpy).toHaveBeenCalled();
});
it('Should emit an error event when form services fails', () => {
const errorSpy: jasmine.Spy = spyOn(component.error, 'emit');
component.onError({});
expect(errorSpy).toHaveBeenCalled();
});
});
describe('Completed Process Task', () => {
beforeEach(async() => {
component.taskId = '123';
fixture.detectChanges();
await fixture.whenStable();
getTaskDetailsSpy.calls.reset();
});
it('Should be able to display form in readonly mode if the task completed', async() => {
getTaskDetailsSpy.and.returnValue(of(completedTaskDetailsMock));
fixture.detectChanges();
await fixture.whenStable();
const activitFormSelector = element.querySelector('adf-form');
const inputFieldOne = fixture.debugElement.nativeElement.querySelector('#text1');
const inputFieldTwo = fixture.debugElement.nativeElement.querySelector('#text2');
const inputFieldThree = fixture.debugElement.nativeElement.querySelector('#text3');
expect(activitFormSelector).toBeDefined();
expect(inputFieldOne['disabled']).toEqual(true);
expect(inputFieldTwo['disabled']).toEqual(true);
expect(inputFieldThree['disabled']).toEqual(true);
});
it('Should be able to show completed message and cancel button for the completed task without form', async () => {
completedTaskDetailsMock.formKey = null;
component.taskDetails = new TaskDetailsModel(completedTaskDetailsMock);
fixture.detectChanges();
await fixture.whenStable();
const completeButtonElement = fixture.debugElement.nativeElement.querySelector('#adf-no-form-complete-button');
const cancelButtonElement = fixture.debugElement.nativeElement.querySelector('#adf-no-form-cancel-button');
const completedFormMessage = fixture.debugElement.nativeElement.querySelector('.adf-empty-content__title');
const subMessage = fixture.debugElement.nativeElement.querySelector('.adf-empty-content__subtitle');
expect(completeButtonElement).toBeNull();
expect(cancelButtonElement).not.toBeNull();
expect(completedFormMessage.innerText).toContain('ADF_TASK_FORM.COMPLETED_TASK.TITLE');
expect(subMessage.innerText).toContain('ADF_TASK_FORM.COMPLETED_TASK.SUBTITLE');
});
it('Should not display complete button to the completed task without form', async () => {
completedTaskDetailsMock.formKey = null;
component.taskDetails = new TaskDetailsModel(completedTaskDetailsMock);
fixture.detectChanges();
await fixture.whenStable();
const completeButtonElement = fixture.debugElement.nativeElement.querySelector('#adf-no-form-complete-button');
expect(completeButtonElement).toBeNull();
});
});
describe('Process Task with no form', () => {
beforeEach(() => {
component.taskId = '123';
fixture.detectChanges();
});
it('Should be able to show no form message if the task does not attached a form', async () => {
component.taskDetails = new TaskDetailsModel(taskDetailsWithOutFormMock);
fixture.detectChanges();
await fixture.whenStable();
const completeButtonElement = fixture.debugElement.nativeElement.querySelector('#adf-no-form-complete-button');
const cancelButtonElement = fixture.debugElement.nativeElement.querySelector('#adf-no-form-cancel-button');
const completedFormMessage = fixture.debugElement.nativeElement.querySelector('.adf-empty-content__title');
const subMessage = fixture.debugElement.nativeElement.querySelector('.adf-empty-content__subtitle');
expect(completeButtonElement).not.toBeNull();
expect(cancelButtonElement).not.toBeNull();
expect(completedFormMessage.innerText).toContain('ADF_TASK_LIST.STANDALONE_TASK.NO_FORM_MESSAGE');
expect(subMessage.innerText).toContain('ADF_TASK_FORM.EMPTY_FORM.SUBTITLE');
});
it('Should be able display complete button to a task without form', async () => {
component.taskDetails = new TaskDetailsModel(taskDetailsWithOutFormMock);
fixture.detectChanges();
await fixture.whenStable();
const completeButtonElement = fixture.debugElement.nativeElement.querySelector('#adf-no-form-complete-button');
expect(completeButtonElement).not.toBeNull();
expect(completeButtonElement['disabled']).toEqual(false);
});
it('Should be able to complete a task with no form when complete button is clicked', async () => {
fixture.detectChanges();
component.taskDetails = new TaskDetailsModel(taskDetailsWithOutFormMock);
fixture.detectChanges();
await fixture.whenStable();
const completeButtonElement = fixture.debugElement.nativeElement.querySelector('#adf-no-form-complete-button');
completeButtonElement.click();
expect(completeTaskSpy).toHaveBeenCalledWith('91');
});
it('Should emit error event in case complete task service fails', async () => {
const errorSpy: jasmine.Spy = spyOn(component.error, 'emit');
completeTaskSpy.and.returnValue(throwError({message: 'servce failed'}));
component.taskDetails = new TaskDetailsModel(taskDetailsWithOutFormMock);
fixture.detectChanges();
await fixture.whenStable();
const activitFormSelector = element.querySelector('adf-form');
const completeButtonElement = fixture.debugElement.nativeElement.querySelector('#adf-no-form-complete-button');
expect(activitFormSelector).toBeDefined();
expect(completeButtonElement['disabled']).toEqual(false);
completeButtonElement.click();
expect(errorSpy).toHaveBeenCalled();
});
it('Should be able to emit cancel event on task with no-form when cancel button is clicked', async () => {
const cancelSpy = spyOn(component.cancel, 'emit');
getTaskDetailsSpy.and.returnValue(of(taskDetailsWithOutFormMock));
component.taskDetails = new TaskDetailsModel(taskDetailsWithOutFormMock);
fixture.detectChanges();
await fixture.whenStable();
const cancelButtonElement = fixture.debugElement.nativeElement.querySelector('#adf-no-form-cancel-button');
cancelButtonElement.click();
expect(cancelSpy).toHaveBeenCalled();
});
});
describe('Standalone Task with no form', () => {
beforeEach(() => {
component.taskId = '123';
fixture.detectChanges();
});
it('Should be able to display empty template in case standalone task does not attached a form', async () => {
component.taskDetails = new TaskDetailsModel(standaloneTaskWithoutForm);
fixture.detectChanges();
await fixture.whenStable();
const taskStandAlone = element.querySelector('adf-task-standalone');
const noFormMessage = fixture.debugElement.nativeElement.querySelector('#adf-no-form-message');
expect(taskStandAlone).not.toBeNull();
expect(noFormMessage.innerText).toContain('ADF_TASK_LIST.STANDALONE_TASK.NO_FORM_MESSAGE');
});
it('Should be able display attach form button for a standalone task without form', async() => {
const showAttachFormSpy = spyOn(component.showAttachForm, 'emit');
component.taskDetails = new TaskDetailsModel(standaloneTaskWithoutForm);
fixture.detectChanges();
await fixture.whenStable();
const attacheFormButton = fixture.debugElement.nativeElement.querySelector('#adf-no-form-attach-form-button');
expect(attacheFormButton).not.toBeNull();
attacheFormButton.click();
expect(showAttachFormSpy).toHaveBeenCalled();
});
it('Should be able to display completed template if standalone task completed', async() => {
component.taskDetails = completedStandaloneTaskWithoutForm;
fixture.detectChanges();
await fixture.whenStable();
const taskStandAlone = element.querySelector('adf-task-standalone');
const completedFormMessage = fixture.debugElement.nativeElement.querySelector('#adf-completed-form-message');
const subMessage = fixture.debugElement.nativeElement.querySelector('.adf-no-form-submessage');
expect(taskStandAlone).not.toBeNull();
expect(completedFormMessage.innerText).toContain('ADF_TASK_LIST.STANDALONE_TASK.COMPLETE_TASK_MESSAGE');
expect(subMessage.innerText).toContain('ADF_TASK_LIST.STANDALONE_TASK.COMPLETE_TASK_SUB_MESSAGE');
});
});
describe('Form with visiblity', () => {
beforeEach(async () => {
component.taskId = '123';
spyOn(formService, 'completeTaskForm').and.returnValue(of({}));
taskDetailsMock.formKey = '4';
getTaskDetailsSpy.and.returnValue(of(taskDetailsMock));
fixture.detectChanges();
await fixture.whenStable();
});
it('[C312410] - Should be possible to complete a task that has an invisible field on a form with a value', async () => {
component.formCompleted.subscribe((form: FormModel) => {
expect(form.id).toBe(taskFormMock.id);
});
component.taskDetails.initiatorCanCompleteTask = true;
fixture.detectChanges();
await fixture.whenStable();
const inputTextOne: HTMLInputElement = fixture.nativeElement.querySelector('#text1');
expect(inputTextOne).toBeDefined();
expect(inputTextOne).not.toBeNull();
const inputTextTwo: HTMLInputElement = fixture.nativeElement.querySelector('#text2');
expect(inputTextTwo).toBeDefined();
expect(inputTextTwo).not.toBeNull();
let inputTextThree: HTMLInputElement = fixture.nativeElement.querySelector('#text3');
expect(inputTextThree).toBeDefined();
expect(inputTextThree).not.toBeNull();
inputTextOne.value = 'a';
inputTextOne.dispatchEvent(new Event('input'));
inputTextTwo.value = 'a';
inputTextTwo.dispatchEvent(new Event('input'));
inputTextThree.value = 'a';
inputTextThree.dispatchEvent(new Event('input'));
fixture.detectChanges();
await fixture.whenStable();
inputTextThree = fixture.nativeElement.querySelector('#text3');
expect(inputTextThree).toBeDefined();
expect(inputTextThree).not.toBeNull();
inputTextOne.value = 'b';
inputTextOne.dispatchEvent(new Event('input'));
fixture.detectChanges();
await fixture.whenStable();
const inputThreeContainer = fixture.nativeElement.querySelector('#field-text3-container');
expect(inputThreeContainer.hidden).toBe(true);
const completeOutcomeButton: HTMLButtonElement = fixture.nativeElement.querySelector('#adf-form-complete');
expect(completeOutcomeButton.hidden).toBe(false);
completeOutcomeButton.click();
fixture.detectChanges();
});
it('[C277278] - Should show if the form is valid via the validation icon', async () => {
const numberInput: HTMLInputElement = fixture.nativeElement.querySelector('#numberField');
let validationForm = fixture.nativeElement.querySelector('#adf-valid-form-icon');
expect(numberInput).toBeDefined();
expect(numberInput).not.toBeNull();
expect(validationForm.textContent).toBe('check_circle');
numberInput.value = 'a';
numberInput.dispatchEvent(new Event('input'));
fixture.detectChanges();
await fixture.whenStable();
const invalidForm = fixture.nativeElement.querySelector('#adf-invalid-form-icon');
expect(invalidForm).not.toBeNull();
expect(invalidForm.textContent).toBe('error');
numberInput.value = '4';
numberInput.dispatchEvent(new Event('input'));
fixture.detectChanges();
await fixture.whenStable();
validationForm = fixture.nativeElement.querySelector('#adf-valid-form-icon');
expect(validationForm.textContent).toBe('check_circle');
});
});
});

View File

@@ -0,0 +1,287 @@
/*!
* @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, OnInit, Input, Output, EventEmitter, SimpleChanges } from '@angular/core';
import {
FormModel,
ContentLinkModel,
FormRenderingService,
FormFieldValidator,
FormOutcomeEvent,
AuthenticationService,
TranslationService,
FormFieldModel
} from '@alfresco/adf-core';
import { TaskDetailsModel } from '../../models/task-details.model';
import { TaskListService } from '../../services/tasklist.service';
import { UserRepresentation } from '@alfresco/js-api';
import { AttachFileWidgetComponent } from '../../../content-widget/attach-file-widget.component';
import { AttachFolderWidgetComponent } from '../../../content-widget/attach-folder-widget.component';
import { Observable } from 'rxjs';
@Component({
selector: 'adf-task-form',
templateUrl: './task-form.component.html',
styleUrls: ['./task-form.component.scss']
})
export class TaskFormComponent implements OnInit {
/** (**required**) The id of the task whose details we are asking for. */
@Input()
taskId: string;
/** Toggles rendering of the form title. */
@Input()
showFormTitle: boolean = false;
/** Toggles rendering of the `Complete` outcome button. */
@Input()
showFormCompleteButton: boolean = true;
/** Toggles rendering of the `Save` outcome button. */
@Input()
showFormSaveButton: boolean = true;
/** Toggle rendering of the `Cancel` button. */
@Input()
showCancelButton: boolean = true;
/** Toggles read-only state of the form. All form widgets render as read-only
* if enabled.
*/
@Input()
readOnlyForm: boolean = false;
/** Toggles rendering of the `Refresh` button. */
@Input()
showFormRefreshButton: boolean = true;
/** Toggle rendering of the validation icon next to the form title. */
@Input()
showFormValidationIcon: boolean = true;
/** Field validators for use with the form. */
@Input()
fieldValidators: FormFieldValidator[] = [];
/** Emitted when the form is submitted with the `Save` or custom outcomes. */
@Output()
formSaved = new EventEmitter<FormModel>();
/** Emitted when the form is submitted with the `Complete` outcome. */
@Output()
formCompleted = new EventEmitter<FormModel>();
/** Emitted when the form field content is clicked. */
@Output()
formContentClicked = new EventEmitter<ContentLinkModel>();
/** Emitted when the form is loaded or reloaded. */
@Output()
formLoaded = new EventEmitter<FormModel>();
/** Emitted when the form associated with the form task is attached. */
@Output()
showAttachForm: EventEmitter<void> = new EventEmitter<void>();
/** Emitted when any outcome is executed. Default behaviour can be prevented
* via `event.preventDefault()`.
*/
@Output()
executeOutcome = new EventEmitter<FormOutcomeEvent>();
/** Emitted when the form associated with the task is completed. */
@Output()
completed = new EventEmitter<void>();
/** Emitted when the supplied form values have a validation error. */
@Output()
formError: EventEmitter<FormFieldModel[]> = new EventEmitter<FormFieldModel[]>();
/** Emitted when an error occurs. */
@Output()
error = new EventEmitter<any>();
/** Emitted when the "Cancel" button is clicked. */
@Output()
cancel = new EventEmitter<void>();
taskDetails: TaskDetailsModel;
currentLoggedUser: UserRepresentation;
loading: boolean = false;
completedTaskMessage: string;
internalReadOnlyForm: boolean = false;
constructor(
private taskListService: TaskListService,
private authService: AuthenticationService,
private formRenderingService: FormRenderingService,
private translationService: TranslationService
) {
this.formRenderingService.setComponentTypeResolver('upload', () => AttachFileWidgetComponent, true);
this.formRenderingService.setComponentTypeResolver('select-folder', () => AttachFolderWidgetComponent, true);
}
ngOnInit() {
this.authService.getBpmLoggedUser().subscribe(user => {
this.currentLoggedUser = user;
});
this.loadTask(this.taskId);
}
ngOnChanges(changes: SimpleChanges) {
const taskId = changes['taskId'];
if (taskId && taskId.currentValue) {
this.loadTask(this.taskId);
return;
}
}
loadTask(taskId: string) {
this.loading = true;
if (taskId) {
this.taskListService.getTaskDetails(taskId).subscribe(
(res: TaskDetailsModel) => {
this.taskDetails = res;
if (!this.taskDetails.name) {
this.taskDetails.name = 'No name';
}
const endDate: any = res.endDate;
if (endDate && !isNaN(endDate.getTime())) {
this.internalReadOnlyForm = true;
} else {
this.internalReadOnlyForm = this.readOnlyForm;
}
this.loading = false;
});
}
}
onFormSaved(savedForm: FormModel) {
this.formSaved.emit(savedForm);
}
onFormCompleted(form: FormModel) {
this.formCompleted.emit(form);
}
onFormLoaded(form: FormModel): void {
this.formLoaded.emit(form);
}
onFormContentClick(content: ContentLinkModel): void {
this.formContentClicked.emit(content);
}
onFormExecuteOutcome(outcome: FormOutcomeEvent) {
this.executeOutcome.emit(outcome);
}
onFormError(error: any) {
this.formError.emit(error);
}
onError(error: any) {
this.error.emit(error);
}
onCompleteTask() {
this.taskListService.completeTask(this.taskDetails.id).subscribe(
() => this.completed.emit(),
(error) => this.error.emit(error));
}
onCancel() {
this.cancel.emit();
}
onShowAttachForm() {
this.showAttachForm.emit();
}
hasFormKey(): boolean {
return (this.taskDetails && (!!this.taskDetails.formKey));
}
isStandaloneTask(): boolean {
return !(this.taskDetails && (!!this.taskDetails.processDefinitionId));
}
isTaskLoaded(): boolean {
return !!this.taskDetails;
}
isCompletedTask(): boolean {
return this.taskDetails && this.taskDetails.endDate !== undefined && this.taskDetails.endDate !== null;
}
isCompleteButtonVisible(): boolean {
return !this.hasFormKey() && this.isTaskActive() && this.isCompleteButtonEnabled();
}
isTaskActive() {
return this.taskDetails && this.taskDetails.duration === null;
}
isAssigned(): boolean {
return !!this.taskDetails.assignee;
}
private hasEmailAddress(): boolean {
return this.taskDetails.assignee.email ? true : false;
}
isAssignedToMe(): boolean {
return this.isAssigned() && this.hasEmailAddress() ?
this.isEmailEqual() :
this.isExternalIdEqual();
}
private isEmailEqual(): boolean {
return (this.taskDetails.assignee && this.currentLoggedUser) && ( this.taskDetails.assignee.email.toLocaleLowerCase() === this.currentLoggedUser.email.toLocaleLowerCase());
}
private isExternalIdEqual(): boolean {
return (this.taskDetails.assignee && this.currentLoggedUser) && (this.taskDetails.assignee.externalId === this.currentLoggedUser.externalId);
}
isCompleteButtonEnabled(): boolean {
return this.isAssignedToMe() || this.canInitiatorComplete();
}
canInitiatorComplete(): boolean {
return this.taskDetails.initiatorCanCompleteTask;
}
isReadOnlyForm(): boolean {
return this.internalReadOnlyForm || !(this.isAssignedToMe() || this.canInitiatorComplete());
}
isSaveButtonVisible(): boolean {
return this.showFormSaveButton && (!this.canInitiatorComplete() || this.isAssignedToMe());
}
canCompleteTask(): boolean {
return !this.isCompletedTask() && this.isAssignedToMe();
}
getCompletedTaskTranslatedMessage(): Observable<string> {
return this.translationService.get('ADF_TASK_FORM.COMPLETED_TASK.TITLE', { taskName: this.taskDetails.name });
}
}

View File

@@ -20,6 +20,7 @@ export * from './components/checklist.component';
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-details.component';
export * from './components/task-audit.directive';
export * from './components/start-task.component';

View File

@@ -31,6 +31,7 @@ import { NoTaskDetailsTemplateDirective } from './components/no-task-detail-temp
import { StartTaskComponent } from './components/start-task.component';
import { TaskAuditDirective } from './components/task-audit.directive';
import { TaskDetailsComponent } from './components/task-details.component';
import { TaskFormComponent } from './components/task-form/task-form.component';
import { TaskFiltersComponent } from './components/task-filters.component';
import { TaskHeaderComponent } from './components/task-header.component';
import { TaskListComponent } from './components/task-list.component';
@@ -56,6 +57,7 @@ import { FormModule } from '../form/form.module';
TaskFiltersComponent,
TaskListComponent,
TaskDetailsComponent,
TaskFormComponent,
TaskAuditDirective,
ChecklistComponent,
TaskHeaderComponent,
@@ -68,6 +70,7 @@ import { FormModule } from '../form/form.module';
TaskFiltersComponent,
TaskListComponent,
TaskDetailsComponent,
TaskFormComponent,
TaskAuditDirective,
ChecklistComponent,
TaskHeaderComponent,