[ADF-4858] Make sure process-services works with ng commands (#5067)

* Process-services:
Making sure you can run
ng build process-services
ng test process-services

* Fix the path of the styles

* move the file in the right place
This commit is contained in:
Maurizio Vitale
2019-09-20 09:47:17 +01:00
committed by Eugenio Romano
parent 90b2cee70d
commit cd7e21a23d
283 changed files with 1324 additions and 332 deletions

View File

@@ -0,0 +1,36 @@
<div class="adf-attach-form">
<mat-card>
<mat-card-content>
<div class="adf-no-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>
<div class="adf-attach-form-row">
<mat-form-field class="adf-grid-full-width">
<mat-select [formControl]="attachFormControl" placeholder="{{ 'ADF_TASK_LIST.ATTACH_FORM.SELECT_OPTION' | translate }}" id="form_id" [(ngModel)]="selectedFormId">
<mat-option *ngFor="let form of forms" [value]="form.id">{{ form.name }}</mat-option>
</mat-select>
</mat-form-field>
</div>
<adf-form *ngIf="this.attachFormControl.valid"
[formId]="selectedFormId"
[readOnly]="true"
[showCompleteButton]="false"
[showRefreshButton]="false"
[showValidationIcon]="false">
</adf-form>
</div>
</mat-card-content>
<mat-card-actions class="adf-no-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>
</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>
</div>
</mat-card-actions>
</mat-card>
</div>

View File

@@ -0,0 +1,17 @@
.adf-attach-form {
.mat-form-field {
width: 100%;
}
&-row {
display: flex;
justify-content: space-between;
margin: 20px 0;
}
.adf-no-form-mat-card-actions {
justify-content: space-between;
margin-top: 30px;
text-align: right;
}
}

View File

@@ -0,0 +1,171 @@
/*!
* @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 { AttachFormComponent } from './attach-form.component';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { setupTestBed } from '@alfresco/adf-core';
import { ProcessTestingModule } from '../../testing/process.testing.module';
import { TaskListService } from './../services/tasklist.service';
import { of } from 'rxjs';
import { By } from '@angular/platform-browser';
describe('AttachFormComponent', () => {
let component: AttachFormComponent;
let fixture: ComponentFixture<AttachFormComponent>;
let element: HTMLElement;
let taskService: TaskListService;
setupTestBed({
imports: [
ProcessTestingModule
]
});
beforeEach(async(() => {
fixture = TestBed.createComponent(AttachFormComponent);
component = fixture.componentInstance;
element = fixture.nativeElement;
taskService = TestBed.get(TaskListService);
fixture.detectChanges();
}));
afterEach(() => {
fixture.destroy();
TestBed.resetTestingModule();
});
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'));
expect(attachButton.nativeElement.disabled).toBeTruthy();
});
}));
it('should emit cancel event if clicked on Cancel Button ', async(() => {
fixture.detectChanges();
fixture.whenStable().then(() => {
const emitSpy = spyOn(component.cancelAttachForm, 'emit');
const el = fixture.nativeElement.querySelector('#adf-no-form-cancel-button');
el.click();
expect(emitSpy).toHaveBeenCalled();
});
}));
it('should call attachFormToATask if clicked on attach Button', async(() => {
component.taskId = 1;
component.attachFormControl.setValue(2);
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');
el.click();
expect(taskService.attachFormToATask).toHaveBeenCalledWith(1, 2);
});
}));
it('should render the attachForm enabled if the user select the different formId', async(() => {
component.taskId = 1;
component.formId = 2;
component.attachFormControl.setValue(3);
fixture.detectChanges();
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'));
expect(attachButton.nativeElement.disabled).toBeFalsy();
});
}));
it('should render a disabled attachForm button if the user select the original formId', async(() => {
component.taskId = 1;
component.formId = 2;
component.attachFormControl.setValue(3);
fixture.detectChanges();
spyOn(taskService, 'attachFormToATask').and.returnValue(of(true));
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
component.attachFormControl.setValue(2);
fixture.detectChanges();
const attachButton = fixture.debugElement.query(By.css('#adf-no-form-attach-form-button'));
expect(attachButton.nativeElement.disabled).toBeTruthy();
});
}));
it('should show the adf-form of the selected form', async(() => {
component.taskId = 1;
component.selectedFormId = 12;
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
const formContainer = fixture.debugElement.nativeElement.querySelector('adf-form');
expect(formContainer).toBeDefined();
expect(formContainer).not.toBeNull();
});
}));
it('should show the formPreview of the selected form', async(() => {
component.formKey = 12;
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
const formContainer = fixture.debugElement.nativeElement.querySelector('.adf-form-container');
expect(formContainer).toBeDefined();
expect(formContainer).toBeNull();
});
}));
it('should remove form if it is present', async(() => {
component.taskId = 1;
component.attachFormControl.setValue(10);
component.formKey = 12;
spyOn(taskService, 'deleteForm').and.returnValue(of({}));
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');
el.click();
expect(component.formId).toBeNull();
});
}));
it('should emit success when a form is attached', async(() => {
component.taskId = 1;
component.attachFormControl.setValue(10);
spyOn(taskService, 'attachFormToATask').and.returnValue(of(
{
id: 91,
name: 'fakeName',
formKey: 1204,
assignee: null
}
));
fixture.detectChanges();
fixture.whenStable().then(() => {
const emitSpy = spyOn(component.success, 'emit');
const el = fixture.nativeElement.querySelector('#adf-no-form-attach-form-button');
el.click();
expect(emitSpy).toHaveBeenCalled();
});
}));
});

View File

@@ -0,0 +1,139 @@
/*!
* @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 { FormService, LogService } from '@alfresco/adf-core';
import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core';
import { Form } from '../models/form.model';
import { TaskListService } from './../services/tasklist.service';
import { FormControl, Validators } from '@angular/forms';
@Component({
selector: 'adf-attach-form',
templateUrl: './attach-form.component.html',
styleUrls: ['./attach-form.component.scss']
})
export class AttachFormComponent implements OnInit, OnChanges {
constructor(private taskService: TaskListService,
private logService: LogService,
private formService: FormService) { }
/** Id of the task. */
@Input()
taskId;
/** Identifier of the form to attach. */
@Input()
formKey;
/** Emitted when the "Cancel" button is clicked. */
@Output()
cancelAttachForm: EventEmitter<void> = new EventEmitter<void>();
/** Emitted when the form is attached successfully. */
@Output()
success: EventEmitter<void> = new EventEmitter<void>();
/** Emitted when an error occurs. */
@Output()
error: EventEmitter<any> = new EventEmitter<any>();
forms: Form[];
formId: number;
disableSubmit: boolean = true;
selectedFormId: number;
attachFormControl: FormControl;
ngOnInit() {
this.attachFormControl = new FormControl('', Validators.required);
this.attachFormControl.valueChanges.subscribe( (currentValue) => {
if (this.attachFormControl.valid) {
if ( this.formId !== currentValue) {
this.disableSubmit = false;
} else {
this.disableSubmit = true;
}
}
});
}
ngOnChanges() {
this.formId = undefined;
this.disableSubmit = true;
this.loadFormsTask();
if (this.formKey) {
this.onFormAttached();
}
}
onCancelButtonClick(): void {
this.selectedFormId = this.formId;
this.cancelAttachForm.emit();
}
onRemoveButtonClick(): void {
this.taskService.deleteForm(this.taskId).subscribe(
() => {
this.formId = this.selectedFormId = null;
this.success.emit();
},
(err) => {
this.error.emit(err);
this.logService.error('An error occurred while trying to delete the form');
});
}
onAttachFormButtonClick(): void {
this.attachForm(this.taskId, this.selectedFormId);
}
private loadFormsTask(): void {
this.taskService.getFormList().subscribe((form: Form[]) => {
this.forms = form;
},
(err) => {
this.error.emit(err);
this.logService.error('An error occurred while trying to get the forms');
});
}
private onFormAttached() {
this.formService.getTaskForm(this.taskId)
.subscribe((res) => {
this.formService.getFormDefinitionByName(res.name).subscribe((formDef) => {
this.formId = this.selectedFormId = formDef;
});
}, (err) => {
this.error.emit(err);
this.logService.error('Could not load forms');
});
}
private attachForm(taskId: string, formId: number) {
if (taskId && formId) {
this.taskService.attachFormToATask(taskId, formId)
.subscribe(() => {
this.success.emit();
}, (err) => {
this.error.emit(err);
this.logService.error('Could not attach form');
});
}
}
}

View File

@@ -0,0 +1,43 @@
<div class="adf-checklist-control">
<mat-chip-list data-automation-id="checklist-label">
<span class="adf-activiti-label">{{ 'ADF_TASK_LIST.DETAILS.LABELS.CHECKLIST' | translate }}</span>
<mat-chip class="adf-process-badge" color="accent" selected="true">{{checklist?.length}}</mat-chip>
</mat-chip-list>
<button mat-icon-button *ngIf="!readOnly" matTooltip="Add a checklist" [matTooltipPosition]="'before'"
id="add-checklist" class="adf-add-to-checklist-button" (click)="showDialog()">
<mat-icon>add</mat-icon>
</button>
</div>
<div class="adf-checklist-menu-container" *ngIf="checklist?.length > 0">
<mat-chip-list class="mat-chip-list-stacked">
<mat-chip id="check-{{check.id}}" class="adf-checklist-chip" *ngFor="let check of checklist"
(removed)="delete(check.id)">
<span>{{check.name}}</span>
<mat-icon *ngIf="!readOnly && !check.endDate" id="remove-{{check.id}}" matChipRemove>cancel
</mat-icon>
</mat-chip>
</mat-chip-list>
</div>
<div *ngIf="checklist?.length === 0" id="checklist-none-message" class="adf-checklist-none-message">
{{ 'ADF_TASK_LIST.DETAILS.CHECKLIST.NONE' | translate }}
</div>
<ng-template #dialog>
<div class="adf-checklist-dialog" id="checklist-dialog">
<h4 matDialogTitle id="add-checklist-title">{{ 'ADF_TASK_LIST.DETAILS.CHECKLIST.DIALOG.TITLE' | translate }}</h4>
<mat-dialog-content>
<mat-form-field>
<input matInput placeholder="{{ 'ADF_TASK_LIST.DETAILS.CHECKLIST.DIALOG.PLACEHOLDER' | translate }}" [(ngModel)]="taskName" id="checklist-name"
data-automation-id="checklist-name">
</mat-form-field>
</mat-dialog-content>
<mat-dialog-actions class="adf-checklist-dialog-actions">
<button mat-button type="button" id="close-check-dialog" (click)="cancel()">{{ 'ADF_TASK_LIST.DETAILS.CHECKLIST.DIALOG.CANCEL-BUTTON' | translate | uppercase }}</button>
<button mat-button type="button" id="add-check" (click)="add()">{{ 'ADF_TASK_LIST.DETAILS.CHECKLIST.DIALOG.ADD-BUTTON' | translate | uppercase }}</button>
</mat-dialog-actions>
</div>
</ng-template>

View File

@@ -0,0 +1,48 @@
:host {
width: 100%;
}
.adf-activiti-label {
font-weight: bolder;
}
.mat-form-field {
width: 100%;
}
.adf-checklist-cancel-button {
margin-top: -13px;
margin-right: -13px;
float: right;
}
.adf-checklist-chip {
outline: none;
}
.adf-checklist-menu-container {
margin-top: 10px;
}
.adf-checklist-none-message {
margin-top: 10px;
}
.adf-checklist-control {
display: flex;
justify-content: space-between;
.adfactiviti-label {
margin-top: 6px;
margin-right: 10px;
}
.adf-add-to-checklist-button {
float: right;
}
}
.adf-checklist-dialog-actions {
display: flex;
justify-content: flex-end;
}

View File

@@ -0,0 +1,289 @@
/*!
* @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 { SimpleChange } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { TaskDetailsModel } from '../models/task-details.model';
import { ChecklistComponent } from './checklist.component';
import { setupTestBed } from '@alfresco/adf-core';
import { ProcessTestingModule } from '../../testing/process.testing.module';
import { TaskListService } from './../services/tasklist.service';
import { of } from 'rxjs';
describe('ChecklistComponent', () => {
let checklistComponent: ChecklistComponent;
let fixture: ComponentFixture<ChecklistComponent>;
let element: HTMLElement;
let showChecklistDialog;
let service: TaskListService;
setupTestBed({
imports: [ProcessTestingModule]
});
beforeEach(async(() => {
service = TestBed.get(TaskListService);
spyOn(service, 'getTaskChecklist').and.returnValue(of([{
id: 'fake-check-changed-id',
name: 'fake-check-changed-name'
}]));
fixture = TestBed.createComponent(ChecklistComponent);
checklistComponent = fixture.componentInstance;
element = fixture.nativeElement;
fixture.detectChanges();
}));
it('should show checklist component title', () => {
expect(element.querySelector('[data-automation-id=checklist-label]')).toBeDefined();
expect(element.querySelector('[data-automation-id=checklist-label]')).not.toBeNull();
});
it('should show no checklist message', () => {
expect(element.querySelector('#checklist-none-message')).not.toBeNull();
expect(element.querySelector('#checklist-none-message').textContent).toContain('ADF_TASK_LIST.DETAILS.CHECKLIST.NONE');
});
describe('when is readonly mode', () => {
beforeEach(() => {
checklistComponent.taskId = 'fake-task-id';
checklistComponent.checklist.push(new TaskDetailsModel({
id: 'fake-check-id',
name: 'fake-check-name'
}));
checklistComponent.readOnly = true;
fixture.detectChanges();
showChecklistDialog = <HTMLElement> element.querySelector('#add-checklist');
});
it('should NOT show add checklist button', () => {
expect(element.querySelector('#add-checklist')).toBeNull();
});
it('should NOT show cancel checklist button', () => {
expect(element.querySelector('#remove-fake-check-id')).toBeNull();
});
});
describe('when is not in readonly mode', () => {
beforeEach(() => {
checklistComponent.taskId = 'fake-task-id';
checklistComponent.readOnly = false;
checklistComponent.checklist.push(new TaskDetailsModel({
id: 'fake-check-id',
name: 'fake-check-name'
}));
fixture.detectChanges();
showChecklistDialog = <HTMLElement> element.querySelector('#add-checklist');
});
it('should show add checklist button', () => {
expect(element.querySelector('#add-checklist')).not.toBeNull();
});
it('should show cancel checklist button', () => {
expect(element.querySelector('#remove-fake-check-id')).not.toBeNull();
});
});
describe('when interact with checklist dialog', () => {
beforeEach(() => {
checklistComponent.taskId = 'fake-task-id';
checklistComponent.checklist = [];
fixture.detectChanges();
showChecklistDialog = <HTMLElement> element.querySelector('#add-checklist');
});
it('should show dialog when clicked on add', (done) => {
expect(showChecklistDialog).not.toBeNull();
showChecklistDialog.click();
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(window.document.querySelector('#checklist-dialog')).not.toBeNull();
expect(window.document.querySelector('#add-checklist-title')).not.toBeNull();
expect(window.document.querySelector('#add-checklist-title').textContent).toContain('ADF_TASK_LIST.DETAILS.CHECKLIST.DIALOG.TITLE');
done();
});
});
});
describe('when there are task checklist', () => {
beforeEach(() => {
checklistComponent.taskId = 'fake-task-id';
checklistComponent.checklist = [];
fixture.detectChanges();
showChecklistDialog = <HTMLElement> element.querySelector('#add-checklist');
});
afterEach(() => {
const overlayContainers = <any> window.document.querySelectorAll('.cdk-overlay-container');
overlayContainers.forEach((overlayContainer) => {
overlayContainer.innerHTML = '';
});
});
it('should show task checklist', () => {
checklistComponent.checklist.push(new TaskDetailsModel({
id: 'fake-check-id',
name: 'fake-check-name'
}));
fixture.detectChanges();
expect(element.querySelector('#check-fake-check-id')).not.toBeNull();
expect(element.querySelector('#check-fake-check-id').textContent).toContain('fake-check-name');
});
it('should not show delete icon when checklist task is completed', () => {
checklistComponent.checklist.push(new TaskDetailsModel({
id: 'fake-check-id',
name: 'fake-check-name'
}));
checklistComponent.checklist.push(new TaskDetailsModel({
id: 'fake-completed-id',
name: 'fake-completed-name',
endDate: '2018-05-23T11:25:14.552+0000'
}));
fixture.detectChanges();
expect(element.querySelector('#remove-fake-check-id')).not.toBeNull();
expect(element.querySelector('#check-fake-completed-id')).not.toBeNull();
expect(element.querySelector('#check-fake-completed-id')).toBeDefined();
expect(element.querySelector('#remove-fake-completed-id')).toBeNull();
});
it('should add checklist', async(() => {
spyOn(service, 'addTask').and.returnValue(of({
id: 'fake-check-added-id', name: 'fake-check-added-name'
}));
showChecklistDialog.click();
const addButtonDialog = <HTMLElement> window.document.querySelector('#add-check');
addButtonDialog.click();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(element.querySelector('#check-fake-check-added-id')).not.toBeNull();
expect(element.querySelector('#check-fake-check-added-id').textContent).toContain('fake-check-added-name');
});
}));
it('should remove a checklist element', async(() => {
spyOn(service, 'deleteTask').and.returnValue(of(''));
checklistComponent.taskId = 'new-fake-task-id';
checklistComponent.checklist.push(new TaskDetailsModel({
id: 'fake-check-id',
name: 'fake-check-name'
}));
fixture.detectChanges();
const checklistElementRemove = <HTMLElement> element.querySelector('#remove-fake-check-id');
expect(checklistElementRemove).toBeDefined();
expect(checklistElementRemove).not.toBeNull();
checklistElementRemove.click();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(element.querySelector('#fake-check-id')).toBeNull();
});
}));
it('should send an event when the checklist is deleted', async(() => {
spyOn(service, 'deleteTask').and.returnValue(of(''));
checklistComponent.taskId = 'new-fake-task-id';
checklistComponent.checklist.push(new TaskDetailsModel({
id: 'fake-check-id',
name: 'fake-check-name'
}));
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(checklistComponent.checklist.length).toBe(1);
const checklistElementRemove = <HTMLElement> element.querySelector('#remove-fake-check-id');
expect(checklistElementRemove).toBeDefined();
expect(checklistElementRemove).not.toBeNull();
checklistElementRemove.click();
expect(checklistComponent.checklist.length).toBe(0);
});
}));
it('should show load task checklist on change', async(() => {
checklistComponent.taskId = 'new-fake-task-id';
checklistComponent.checklist.push(new TaskDetailsModel({
id: 'fake-check-id',
name: 'fake-check-name'
}));
fixture.detectChanges();
const change = new SimpleChange(null, 'new-fake-task-id', true);
checklistComponent.ngOnChanges({
taskId: change
});
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(element.querySelector('#check-fake-check-changed-id')).not.toBeNull();
expect(element.querySelector('#check-fake-check-changed-id').textContent).toContain('fake-check-changed-name');
});
}));
it('should show empty checklist when task id is null', async(() => {
checklistComponent.taskId = 'new-fake-task-id';
checklistComponent.checklist.push(new TaskDetailsModel({
id: 'fake-check-id',
name: 'fake-check-name'
}));
fixture.detectChanges();
checklistComponent.taskId = null;
const change = new SimpleChange(null, 'new-fake-task-id', true);
checklistComponent.ngOnChanges({
taskId: change
});
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(element.querySelector('#checklist-none-message')).not.toBeNull();
expect(element.querySelector('#checklist-none-message').textContent).toContain('ADF_TASK_LIST.DETAILS.CHECKLIST.NONE');
});
}));
it('should emit checklist task created event when the checklist is successfully added', (done) => {
spyOn(service, 'addTask').and.returnValue(of({ id: 'fake-check-added-id', name: 'fake-check-added-name' }));
const disposableCreated = checklistComponent.checklistTaskCreated.subscribe((taskAdded: TaskDetailsModel) => {
fixture.detectChanges();
expect(taskAdded.id).toEqual('fake-check-added-id');
expect(taskAdded.name).toEqual('fake-check-added-name');
expect(element.querySelector('#check-fake-check-added-id')).not.toBeNull();
expect(element.querySelector('#check-fake-check-added-id').textContent).toContain('fake-check-added-name');
disposableCreated.unsubscribe();
done();
});
showChecklistDialog.click();
const addButtonDialog = <HTMLElement> window.document.querySelector('#add-check');
addButtonDialog.click();
});
});
});

View File

@@ -0,0 +1,138 @@
/*!
* @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, EventEmitter, Input, OnChanges, Output, SimpleChanges, ViewChild } from '@angular/core';
import { MatDialog } from '@angular/material';
import { TaskDetailsModel } from '../models/task-details.model';
import { TaskListService } from './../services/tasklist.service';
@Component({
selector: 'adf-checklist',
templateUrl: './checklist.component.html',
styleUrls: ['./checklist.component.scss']
})
export class ChecklistComponent implements OnChanges {
/** (required) The id of the parent task to which subtasks are
* attached.
*/
@Input()
taskId: string;
/** Toggle readonly state of the form. All form widgets
* will render as readonly if enabled.
*/
@Input()
readOnly: boolean = false;
/** (required) The assignee id that the subtasks are assigned to. */
@Input()
assignee: string;
/** Emitted when a new checklist task is created. */
@Output()
checklistTaskCreated: EventEmitter<TaskDetailsModel> = new EventEmitter<TaskDetailsModel>();
/** Emitted when a checklist task is deleted. */
@Output()
checklistTaskDeleted: EventEmitter<string> = new EventEmitter<string>();
/** Emitted when an error occurs. */
@Output()
error: EventEmitter<any> = new EventEmitter<any>();
@ViewChild('dialog')
addNewDialog: any;
taskName: string;
checklist: TaskDetailsModel [] = [];
/**
* Constructor
* @param auth
* @param translate
*/
constructor(private activitiTaskList: TaskListService,
private dialog: MatDialog) {
}
ngOnChanges(changes: SimpleChanges) {
const taskId = changes['taskId'];
if (taskId && taskId.currentValue) {
this.getTaskChecklist();
return;
}
}
getTaskChecklist() {
this.checklist = [];
if (this.taskId) {
this.activitiTaskList.getTaskChecklist(this.taskId).subscribe(
(taskDetailsModel: TaskDetailsModel[]) => {
taskDetailsModel.forEach((task) => {
this.checklist.push(task);
});
},
(error) => {
this.error.emit(error);
}
);
} else {
this.checklist = [];
}
}
showDialog() {
this.dialog.open(this.addNewDialog, { width: '350px' });
}
public add() {
const newTask = new TaskDetailsModel({
name: this.taskName,
parentTaskId: this.taskId,
assignee: { id: this.assignee }
});
this.activitiTaskList.addTask(newTask).subscribe(
(taskDetailsModel: TaskDetailsModel) => {
this.checklist.push(taskDetailsModel);
this.checklistTaskCreated.emit(taskDetailsModel);
this.taskName = '';
},
(error) => {
this.error.emit(error);
}
);
this.cancel();
}
public delete(taskId: string) {
this.activitiTaskList.deleteTask(taskId).subscribe(
() => {
this.checklist = this.checklist.filter((check) => check.id !== taskId);
this.checklistTaskDeleted.emit(taskId);
},
(error) => {
this.error.emit(error);
});
}
public cancel() {
this.dialog.closeAll();
this.taskName = '';
}
}

View File

@@ -0,0 +1,43 @@
/*!
* @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 { 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);
spyOn(authService, 'getBpmLoggedUser').and.returnValue(of({ email: 'fake-email'}));
detailsComponent = new TaskDetailsComponent(null, authService, null, null, null, null);
component = new NoTaskDetailsTemplateDirective(detailsComponent);
});
it('should set "no task details" template on task details component', () => {
const testTemplate: any = 'test template';
component.template = testTemplate;
component.ngAfterContentInit();
expect(detailsComponent.noTaskDetailsTemplateComponent).toBe(testTemplate);
});
});

View File

@@ -0,0 +1,44 @@
/*!
* @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 {
AfterContentInit,
ContentChild,
Directive,
TemplateRef
} from '@angular/core';
import { TaskDetailsComponent } from './task-details.component';
/**
* Directive selectors without adf- prefix will be deprecated on 3.0.0
*/
@Directive({
selector: 'adf-no-task-details-template, no-task-details-template'
})
export class NoTaskDetailsTemplateDirective implements AfterContentInit {
@ContentChild(TemplateRef)
template: any;
constructor(
private activitiTaskDetails: TaskDetailsComponent) {
}
ngAfterContentInit() {
this.activitiTaskDetails.noTaskDetailsTemplateComponent = this.template;
}
}

View File

@@ -0,0 +1,98 @@
<mat-card fxFlex="70%" class="adf-new-task-layout-card">
<mat-card-header fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="10px" class="adf-new-task-heading">
<mat-card-title>{{'ADF_TASK_LIST.START_TASK.FORM.TITLE' | translate}}</mat-card-title>
</mat-card-header>
<mat-card-content>
<form [formGroup]="taskForm" fxLayout="column" fxLayoutGap="10px">
<div class="adf-task-name">
<mat-form-field fxFlex>
<mat-label>{{'ADF_TASK_LIST.START_TASK.FORM.LABEL.NAME' | translate}}</mat-label>
<input
matInput
id="name_id"
formControlName="name">
<mat-error *ngIf="nameController.hasError('required') || nameController.hasError('whitespace')">
{{ 'ADF_TASK_LIST.START_TASK.FORM.ERROR.REQUIRED' | translate }}
</mat-error>
<mat-error *ngIf="nameController.hasError('maxlength')">
{{ 'ADF_TASK_LIST.START_TASK.FORM.ERROR.MAXIMUM_LENGTH' | translate : { characters : maxTaskNameLength } }}
</mat-error>
</mat-form-field>
</div>
<div class="adf-task-description">
<mat-form-field fxFlex>
<mat-label>{{'ADF_TASK_LIST.START_TASK.FORM.LABEL.DESCRIPTION' | translate}}</mat-label>
<textarea
matInput
rows="1"
id="description_id"
formControlName="description">
</textarea>
<mat-error *ngIf="descriptionController.hasError('whitespace')">
{{ 'ADF_TASK_LIST.START_TASK.FORM.ERROR.MESSAGE' | translate }}
</mat-error>
</mat-form-field>
</div>
<div class="input-row" fxLayout="row" fxLayout.lt-md="column" fxLayoutGap="20px" fxLayoutGap.lt-md="0px">
<mat-form-field fxFlex>
<input
matInput
(keyup)="onDateChanged($event.srcElement.value)"
(dateInput)="onDateChanged($event.value)"
[matDatepicker]="taskDatePicker"
placeholder="{{'ADF_TASK_LIST.START_TASK.FORM.LABEL.DATE'|translate}}"
id="date_id">
<mat-datepicker-toggle
matSuffix
[for]="taskDatePicker"></mat-datepicker-toggle>
<mat-datepicker
#taskDatePicker
[touchUi]="true">
</mat-datepicker>
<div class="adf-error-text-container">
<div *ngIf="dateError">
<div class="adf-error-text">{{'ADF_TASK_LIST.START_TASK.FORM.ERROR.DATE'|translate}}</div>
<mat-icon class="adf-error-icon">warning</mat-icon>
</div>
</div>
</mat-form-field>
<div fxFlex>
<people-widget
(peopleSelected)="getAssigneeId($event)"
[field]="field"
class="adf-people-widget-content"></people-widget>
</div>
</div>
<div class="adf-task-form">
<mat-form-field fxFlex="48%" fxFlex.xs="100%">
<mat-label id="form_label">{{'ADF_TASK_LIST.START_TASK.FORM.LABEL.FORM'|translate}}</mat-label>
<mat-select
id="form_id"
class="form-control"
formControlName="formKey">
<mat-option>{{'ADF_TASK_LIST.START_TASK.FORM.LABEL.NONE'|translate}}</mat-option>
<mat-option *ngFor="let form of forms$ | async" [value]="form.id">{{ form.name }}</mat-option>
</mat-select>
</mat-form-field>
</div>
</form>
</mat-card-content>
<mat-card-actions>
<div class="adf-new-task-footer" fxLayout="row" fxLayoutAlign="end end">
<button
mat-button
(click)="onCancel()"
id="button-cancel">
{{'ADF_TASK_LIST.START_TASK.FORM.ACTION.CANCEL'|translate}}
</button>
<button
color="primary"
mat-button
[disabled]="!isFormValid()"
(click)="saveTask()"
id="button-start">
{{'ADF_TASK_LIST.START_TASK.FORM.ACTION.START'|translate}}
</button>
</div>
</mat-card-actions>
</mat-card>

View File

@@ -0,0 +1,126 @@
@mixin adf-task-list-start-task-theme($theme) {
$primary: map-get($theme, primary);
$accent: map-get($theme, accent);
$warn: map-get($theme, warn);
$foreground: map-get($theme, foreground);
$header-border: 1px solid mat-color($foreground, divider);
.adf-new-task-heading {
padding-top: 12px;
border-bottom: $header-border;
.mat-card-title {
font-weight: bold;
font-size: 18px;
}
}
.adf-new-task-form {
width: 100%;
}
.adf-new-task-layout-card {
margin: 10px auto;
}
.adf-new-task-footer {
padding: 4px;
font-size: 18px;
border-top: 1px solid #eee;
}
.adf-mat-select {
padding-top: 0;
}
adf-start-task {
people-widget {
width: 100%;
.mat-form-field-label-wrapper {
top: -14px !important;
}
}
.adf-people-widget-content {
.mat-form-field {
width: 100%;
}
.adf-label {
line-height: 0;
}
.adf-error-text-container {
margin-top: -10px;
}
}
.adf {
&-new-task-footer {
.mat-button {
text-transform: uppercase !important;
}
}
&-start-task-input-container .mat-form-field-wrapper {
padding-top: 8px;
}
&-error-text-container {
position: absolute;
height: 20px;
margin-top: 12px;
width: 100%;
& > div {
display: flex;
flex-flow: row;
justify-content: flex-start;
}
}
&-error-text {
padding-right: 8px;
height: 16px;
font-size: 12px;
line-height: 1.33;
color: mat-color($warn);
width: auto;
}
&-error-icon {
font-size: 17px;
color: mat-color($warn);
}
&-label {
color: rgb(186, 186, 186);;
}
&-invalid {
.mat-form-field-underline {
background-color: #f44336 !important;
}
.adf-file {
border-color: mat-color($warn);
}
.mat-form-field-prefix {
color: mat-color($warn);
}
.adf-input {
border-color: mat-color($warn);
}
.adf-label {
color: mat-color($warn);
&::after {
background-color: mat-color($warn);
}
}
}
}
}
}

View File

@@ -0,0 +1,418 @@
/*!
* @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 { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { setupTestBed, LogService } from '@alfresco/adf-core';
import { of, throwError, Observable } from 'rxjs';
import { TaskListService } from '../services/tasklist.service';
import { StartTaskComponent } from './start-task.component';
import { ProcessTestingModule } from '../../testing/process.testing.module';
import { taskDetailsMock } from '../../mock/task/task-details.mock';
import { TaskDetailsModel } from '../models/task-details.model';
describe('StartTaskComponent', () => {
let component: StartTaskComponent;
let fixture: ComponentFixture<StartTaskComponent>;
let service: TaskListService;
let logService: LogService;
let element: HTMLElement;
let getFormListSpy: jasmine.Spy;
let createNewTaskSpy: jasmine.Spy;
let logSpy: jasmine.Spy;
const fakeForms$ = [
{
id: 123,
name: 'Display Data'
},
{
id: 1111,
name: 'Employee Info'
}
];
const testUser = { id: 1001, firstName: 'fakeName', email: 'fake@app.activiti.com' };
setupTestBed({
imports: [ProcessTestingModule]
});
beforeEach(async(() => {
fixture = TestBed.createComponent(StartTaskComponent);
component = fixture.componentInstance;
element = fixture.nativeElement;
service = TestBed.get(TaskListService);
logService = TestBed.get(LogService);
getFormListSpy = spyOn(service, 'getFormList').and.returnValue(new Observable((observer) => {
observer.next(fakeForms$);
observer.complete();
}));
fixture.detectChanges();
}));
afterEach(() => {
fixture.destroy();
TestBed.resetTestingModule();
});
it('should create instance of StartTaskComponent', () => {
expect(component instanceof StartTaskComponent).toBe(true, 'should create StartTaskComponent');
});
it('should fetch fake form on init', () => {
component.ngOnInit();
fixture.detectChanges();
expect(component.forms$).toBeDefined();
expect(getFormListSpy).toHaveBeenCalled();
});
describe('create task', () => {
beforeEach(() => {
createNewTaskSpy = spyOn(service, 'createNewTask').and.returnValue(of(
{
id: 91,
name: 'fakeName',
formKey: null,
assignee: null
}
));
});
it('should create new task when start is clicked', () => {
const successSpy = spyOn(component.success, 'emit');
component.taskForm.controls['name'].setValue('task');
fixture.detectChanges();
const createTaskButton = <HTMLElement> element.querySelector('#button-start');
createTaskButton.click();
expect(successSpy).toHaveBeenCalled();
});
it('should send on success event when the task is started', () => {
const successSpy = spyOn(component.success, 'emit');
component.taskDetailsModel = new TaskDetailsModel(taskDetailsMock);
component.taskForm.controls['name'].setValue('fakeName');
fixture.detectChanges();
const createTaskButton = <HTMLElement> element.querySelector('#button-start');
createTaskButton.click();
expect(successSpy).toHaveBeenCalledWith({
id: 91,
name: 'fakeName',
formKey: null,
assignee: null
});
});
it('should send on success event when only name is given', () => {
const successSpy = spyOn(component.success, 'emit');
component.appId = 42;
component.taskForm.controls['name'].setValue('fakeName');
fixture.detectChanges();
const createTaskButton = <HTMLElement> element.querySelector('#button-start');
createTaskButton.click();
expect(successSpy).toHaveBeenCalled();
});
it('should not emit success event when data not present', () => {
const successSpy = spyOn(component.success, 'emit');
component.taskDetailsModel = new TaskDetailsModel(null);
fixture.detectChanges();
const createTaskButton = <HTMLElement> element.querySelector('#button-start');
createTaskButton.click();
expect(createNewTaskSpy).not.toHaveBeenCalled();
expect(successSpy).not.toHaveBeenCalled();
});
});
describe('attach form', () => {
beforeEach(() => {
spyOn(service, 'createNewTask').and.returnValue(of(
{
id: 91,
name: 'fakeName',
formKey: null,
assignee: null
}
));
});
it('should attach form to the task when a form is selected', () => {
spyOn(service, 'attachFormToATask').and.returnValue(of(
{
id: 91,
name: 'fakeName',
formKey: 1204,
assignee: null
}
));
const successSpy = spyOn(component.success, 'emit');
component.taskForm.controls['name'].setValue('fakeName');
component.taskForm.controls['formKey'].setValue(1204);
component.appId = 42;
component.taskDetailsModel = new TaskDetailsModel(taskDetailsMock);
fixture.detectChanges();
const createTaskButton = <HTMLElement> element.querySelector('#button-start');
createTaskButton.click();
expect(successSpy).toHaveBeenCalledWith({
id: 91,
name: 'fakeName',
formKey: 1204,
assignee: null
});
});
it('should not attach form to the task when a no form is selected', () => {
spyOn(service, 'attachFormToATask').and.returnValue(of(
{
id: 91,
name: 'fakeName',
formKey: null,
assignee: null
}
));
const successSpy = spyOn(component.success, 'emit');
component.taskForm.controls['name'].setValue('fakeName');
component.taskForm.controls['formKey'].setValue(null);
component.appId = 42;
component.taskDetailsModel = new TaskDetailsModel(taskDetailsMock);
fixture.detectChanges();
const createTaskButton = <HTMLElement> element.querySelector('#button-start');
createTaskButton.click();
expect(successSpy).toHaveBeenCalledWith({
id: 91,
name: 'fakeName',
formKey: null,
assignee: null
});
});
});
describe('assign user', () => {
beforeEach(() => {
spyOn(service, 'createNewTask').and.returnValue(of(
{
id: 91,
name: 'fakeName',
formKey: null,
assignee: null
}
));
spyOn(service, 'attachFormToATask').and.returnValue(of(
{
id: 91,
name: 'fakeName',
formKey: 1204,
assignee: null
}
));
spyOn(service, 'assignTaskByUserId').and.returnValue(of(
{
id: 91,
name: 'fakeName',
formKey: 1204,
assignee: testUser
}
));
});
it('should assign task when an assignee is selected', () => {
const successSpy = spyOn(component.success, 'emit');
component.taskForm.controls['name'].setValue('fakeName');
component.taskForm.controls['formKey'].setValue(1204);
component.appId = 42;
component.assigneeId = testUser.id;
fixture.detectChanges();
const createTaskButton = <HTMLElement> element.querySelector('#button-start');
createTaskButton.click();
expect(successSpy).toHaveBeenCalledWith({
id: 91,
name: 'fakeName',
formKey: 1204,
assignee: testUser
});
});
it('should assign task with id of selected user assigned', () => {
const successSpy = spyOn(component.success, 'emit');
component.taskDetailsModel = new TaskDetailsModel(taskDetailsMock);
component.taskForm.controls['name'].setValue('fakeName');
component.taskForm.controls['formKey'].setValue(1204);
component.appId = 42;
component.getAssigneeId(testUser.id);
fixture.detectChanges();
const createTaskButton = <HTMLElement> element.querySelector('#button-start');
createTaskButton.click();
expect(successSpy).toHaveBeenCalledWith({
id: 91,
name: 'fakeName',
formKey: 1204,
assignee: testUser
});
});
it('should not assign task when no assignee is selected', () => {
const successSpy = spyOn(component.success, 'emit');
component.taskForm.controls['name'].setValue('fakeName');
component.taskForm.controls['formKey'].setValue(1204);
component.appId = 42;
component.assigneeId = null;
component.taskDetailsModel = new TaskDetailsModel(taskDetailsMock);
fixture.detectChanges();
const createTaskButton = <HTMLElement> element.querySelector('#button-start');
createTaskButton.click();
expect(successSpy).toHaveBeenCalledWith({
id: 91,
name: 'fakeName',
formKey: 1204,
assignee: null
});
});
});
it('should not attach a form when a form id is not selected', () => {
const attachFormToATask = spyOn(service, 'attachFormToATask').and.returnValue([]);
spyOn(service, 'createNewTask').and.callFake(
function() {
return new Observable((observer) => {
observer.next({ id: 'task-id'});
observer.complete();
});
});
component.taskForm.controls['name'].setValue('fakeName');
fixture.detectChanges();
const createTaskButton = <HTMLElement> element.querySelector('#button-start');
fixture.detectChanges();
createTaskButton.click();
expect(attachFormToATask).not.toHaveBeenCalled();
});
it('should show start task button', () => {
fixture.detectChanges();
expect(element.querySelector('#button-start')).toBeDefined();
expect(element.querySelector('#button-start')).not.toBeNull();
expect(element.querySelector('#button-start').textContent).toContain('ADF_TASK_LIST.START_TASK.FORM.ACTION.START');
});
it('should not emit TaskDetails OnCancel', () => {
const emitSpy = spyOn(component.cancel, 'emit');
component.onCancel();
expect(emitSpy).not.toBeNull();
expect(emitSpy).toHaveBeenCalled();
});
it('should disable start button if name is empty', () => {
component.taskForm.controls['name'].setValue('');
fixture.detectChanges();
const createTaskButton = fixture.nativeElement.querySelector('#button-start');
expect(createTaskButton.disabled).toBeTruthy();
});
it('should cancel start task on cancel button click', () => {
const emitSpy = spyOn(component.cancel, 'emit');
const cancelTaskButton = <HTMLElement> element.querySelector('#button-cancel');
fixture.detectChanges();
cancelTaskButton.click();
expect(emitSpy).not.toBeNull();
expect(emitSpy).toHaveBeenCalled();
});
it('should enable start button if name is filled out', () => {
component.taskForm.controls['name'].setValue('fakeName');
fixture.detectChanges();
const createTaskButton = fixture.nativeElement.querySelector('#button-start');
expect(createTaskButton.disabled).toBeFalsy();
});
it('should define the select options for Forms', () => {
component.forms$ = service.getFormList();
fixture.detectChanges();
const selectElement = fixture.nativeElement.querySelector('#form_label');
expect(selectElement.innerHTML).toContain('ADF_TASK_LIST.START_TASK.FORM.LABEL.FORM');
});
it('should get formatted fullname', () => {
const testUser1 = { 'id': 1001, 'firstName': 'Wilbur', 'lastName': 'Adams', 'email': 'wilbur@app.activiti.com' };
const testUser2 = { 'id': 1002, 'firstName': '', 'lastName': 'Adams', 'email': 'adams@app.activiti.com' };
const testUser3 = { 'id': 1003, 'firstName': 'Wilbur', 'lastName': '', 'email': 'wilbur@app.activiti.com' };
const testUser4 = { 'id': 1004, 'firstName': '', 'lastName': '', 'email': 'test@app.activiti.com' };
const testFullName1 = component.getDisplayUser(testUser1.firstName, testUser1.lastName, ' ');
const testFullName2 = component.getDisplayUser(testUser2.firstName, testUser2.lastName, ' ');
const testFullName3 = component.getDisplayUser(testUser3.firstName, testUser3.lastName, ' ');
const testFullName4 = component.getDisplayUser(testUser4.firstName, testUser4.lastName, ' ');
expect(testFullName1.trim()).toBe('Wilbur Adams');
expect(testFullName2.trim()).toBe('Adams');
expect(testFullName3.trim()).toBe('Wilbur');
expect(testFullName4.trim()).toBe('');
});
it('should emit error when there is an error while creating task', () => {
component.taskForm.controls['name'].setValue('fakeName');
const errorSpy = spyOn(component.error, 'emit');
spyOn(service, 'createNewTask').and.returnValue(throwError({}));
const createTaskButton = <HTMLElement> element.querySelector('#button-start');
fixture.detectChanges();
createTaskButton.click();
expect(errorSpy).toHaveBeenCalled();
});
it('should emit error when task name exceeds maximum length', () => {
component.maxTaskNameLength = 2;
component.ngOnInit();
fixture.detectChanges();
const name = component.taskForm.controls['name'];
name.setValue('task');
fixture.detectChanges();
expect(name.valid).toBeFalsy();
name.setValue('ta');
fixture.detectChanges();
expect(name.valid).toBeTruthy();
});
it('should emit error when task name field is empty', () => {
fixture.detectChanges();
const name = component.taskForm.controls['name'];
name.setValue('');
fixture.detectChanges();
expect(name.valid).toBeFalsy();
name.setValue('task');
fixture.detectChanges();
expect(name.valid).toBeTruthy();
});
it('should call logService when task name exceeds maximum length', () => {
logSpy = spyOn(logService, 'log').and.callThrough();
component.maxTaskNameLength = 300;
component.ngOnInit();
fixture.detectChanges();
expect(logSpy).toHaveBeenCalled();
});
it('should emit error when description have only white spaces', () => {
fixture.detectChanges();
const description = component.taskForm.controls['description'];
description.setValue(' ');
fixture.detectChanges();
expect(description.valid).toBeFalsy();
description.setValue('');
fixture.detectChanges();
expect(description.valid).toBeTruthy();
});
});

View File

@@ -0,0 +1,255 @@
/*!
* @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 { LogService, UserPreferencesService, UserPreferenceValues, UserProcessModel, FormFieldModel, FormModel } from '@alfresco/adf-core';
import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation, OnDestroy } from '@angular/core';
import { DateAdapter, MAT_DATE_FORMATS } from '@angular/material/core';
import { MOMENT_DATE_FORMATS, MomentDateAdapter } from '@alfresco/adf-core';
import moment from 'moment-es6';
import { Moment } from 'moment';
import { Observable, of, Subject } from 'rxjs';
import { Form } from '../models/form.model';
import { TaskDetailsModel } from '../models/task-details.model';
import { TaskListService } from './../services/tasklist.service';
import { switchMap, defaultIfEmpty, takeUntil } from 'rxjs/operators';
import { FormBuilder, AbstractControl, Validators, FormGroup, FormControl } from '@angular/forms';
@Component({
selector: 'adf-start-task',
templateUrl: './start-task.component.html',
styleUrls: ['./start-task.component.scss'],
providers: [
{ provide: DateAdapter, useClass: MomentDateAdapter },
{ provide: MAT_DATE_FORMATS, useValue: MOMENT_DATE_FORMATS }],
encapsulation: ViewEncapsulation.None
})
export class StartTaskComponent implements OnInit, OnDestroy {
public FORMAT_DATE: string = 'DD/MM/YYYY';
MAX_LENGTH: number = 255;
/** (required) The id of the app. */
@Input()
appId: number;
/** Default Task Name. */
@Input()
name: string = '';
/** Emitted when the task is successfully created. */
@Output()
success: EventEmitter<any> = new EventEmitter<any>();
/** Emitted when the cancel button is clicked by the user. */
@Output()
cancel: EventEmitter<void> = new EventEmitter<void>();
/** Emitted when an error occurs. */
@Output()
error: EventEmitter<any> = new EventEmitter<any>();
taskDetailsModel: TaskDetailsModel = new TaskDetailsModel();
forms$: Observable<Form[]>;
assigneeId: number;
field: FormFieldModel;
taskForm: FormGroup;
dateError: boolean = false;
maxTaskNameLength: number = this.MAX_LENGTH;
loading = false;
private onDestroy$ = new Subject<boolean>();
/**
* Constructor
* @param auth
* @param translate
* @param taskService
*/
constructor(private taskService: TaskListService,
private dateAdapter: DateAdapter<Moment>,
private userPreferencesService: UserPreferencesService,
private formBuilder: FormBuilder,
private logService: LogService) {
}
ngOnInit() {
if (this.name) {
this.taskDetailsModel.name = this.name;
}
this.validateMaxTaskNameLength();
this.field = new FormFieldModel(new FormModel(), { id: this.assigneeId, value: this.assigneeId, placeholder: 'Assignee' });
this.userPreferencesService
.select(UserPreferenceValues.Locale)
.pipe(takeUntil(this.onDestroy$))
.subscribe(locale => this.dateAdapter.setLocale(locale));
this.loadFormsTask();
this.buildForm();
}
ngOnDestroy() {
this.onDestroy$.next(true);
this.onDestroy$.complete();
}
buildForm() {
this.taskForm = this.formBuilder.group({
name: new FormControl(this.taskDetailsModel.name, [Validators.required, Validators.maxLength(this.maxTaskNameLength), this.whitespaceValidator]),
description: new FormControl('', [this.whitespaceValidator]),
formKey: new FormControl('')
});
this.taskForm.valueChanges
.pipe(takeUntil(this.onDestroy$))
.subscribe(taskFormValues => this.setTaskDetails(taskFormValues));
}
public whitespaceValidator(control: FormControl) {
if (control.value) {
const isWhitespace = (control.value || '').trim().length === 0;
const isValid = control.value.length === 0 || !isWhitespace;
return isValid ? null : { 'whitespace': true };
}
}
setTaskDetails(form) {
this.taskDetailsModel.name = form.name;
this.taskDetailsModel.description = form.description;
this.taskDetailsModel.formKey = form.formKey ? form.formKey.toString() : null;
}
isFormValid() {
return this.taskForm.valid && !this.dateError && !this.loading;
}
public saveTask(): void {
this.loading = true;
if (this.appId) {
this.taskDetailsModel.category = this.appId.toString();
}
this.taskService.createNewTask(this.taskDetailsModel)
.pipe(
switchMap((createRes: any) =>
this.attachForm(createRes.id, this.taskDetailsModel.formKey).pipe(
defaultIfEmpty(createRes),
switchMap((attachRes: any) =>
this.assignTaskByUserId(createRes.id, this.assigneeId).pipe(
defaultIfEmpty(attachRes ? attachRes : createRes)
)
)
)
)
)
.subscribe(
(res: any) => {
this.loading = false;
this.success.emit(res);
},
(err) => {
this.loading = false;
this.error.emit(err);
this.logService.error('An error occurred while creating new task');
});
}
getAssigneeId(userId) {
this.assigneeId = userId;
}
private attachForm(taskId: string, formKey: string): Observable<any> {
let response = of();
if (taskId && formKey) {
response = this.taskService.attachFormToATask(taskId, parseInt(formKey, 10));
}
return response;
}
private assignTaskByUserId(taskId: string, userId: any): Observable<any> {
let response = of();
if (taskId && userId) {
response = this.taskService.assignTaskByUserId(taskId, userId);
}
return response;
}
public onCancel(): void {
this.cancel.emit();
}
private loadFormsTask(): void {
this.forms$ = this.taskService.getFormList();
}
public isUserNameEmpty(user: UserProcessModel): boolean {
return !user || (this.isEmpty(user.firstName) && this.isEmpty(user.lastName));
}
private isEmpty(data: string): boolean {
return data === undefined || data === null || data.trim().length === 0;
}
public getDisplayUser(firstName: string, lastName: string, delimiter: string = '-'): string {
firstName = (firstName !== null ? firstName : '');
lastName = (lastName !== null ? lastName : '');
return firstName + delimiter + lastName;
}
onDateChanged(newDateValue: any) {
this.dateError = false;
if (newDateValue) {
let momentDate;
if (typeof newDateValue === 'string') {
momentDate = moment(newDateValue, this.FORMAT_DATE, true);
} else {
momentDate = newDateValue;
}
if (momentDate.isValid()) {
this.taskDetailsModel.dueDate = momentDate.toDate();
} else {
this.dateError = true;
this.taskDetailsModel.dueDate = null;
}
} else {
this.taskDetailsModel.dueDate = null;
}
}
private validateMaxTaskNameLength() {
if (this.maxTaskNameLength > this.MAX_LENGTH) {
this.maxTaskNameLength = this.MAX_LENGTH;
this.logService.log(`the task name length cannot be greater than ${this.MAX_LENGTH}`);
}
}
get nameController(): AbstractControl {
return this.taskForm.get('name');
}
get descriptionController(): AbstractControl {
return this.taskForm.get('description');
}
get formKeyController(): AbstractControl {
return this.taskForm.get('formKey');
}
}

View File

@@ -0,0 +1,157 @@
/*!
* @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';
import {
async,
ComponentFixture,
fakeAsync,
TestBed
} from '@angular/core/testing';
import { of } from 'rxjs';
import { TaskListService } from './../services/tasklist.service';
import { setupTestBed, CoreModule } from '@alfresco/adf-core';
import { TaskAuditDirective } from './task-audit.directive';
declare let jasmine: any;
describe('TaskAuditDirective', () => {
@Component({
selector: 'adf-basic-button',
template: `
<button id="auditButton"
adf-task-audit
[task-id]="currentTaskId"
[download]="download"
[fileName]="fileName"
[format]="format"
(clicked)="onAuditClick($event)">My button
</button>`
})
class BasicButtonComponent {
download: boolean = false;
fileName: string;
format: string;
onAuditClick() {}
}
let fixture: ComponentFixture<BasicButtonComponent>;
let component: BasicButtonComponent;
let service: TaskListService;
function createFakePdfBlob(): Blob {
const pdfData = atob(
'JVBERi0xLjcKCjEgMCBvYmogICUgZW50cnkgcG9pbnQKPDwKICAvVHlwZSAvQ2F0YWxvZwog' +
'IC9QYWdlcyAyIDAgUgo+PgplbmRvYmoKCjIgMCBvYmoKPDwKICAvVHlwZSAvUGFnZXMKICAv' +
'TWVkaWFCb3ggWyAwIDAgMjAwIDIwMCBdCiAgL0NvdW50IDEKICAvS2lkcyBbIDMgMCBSIF0K' +
'Pj4KZW5kb2JqCgozIDAgb2JqCjw8CiAgL1R5cGUgL1BhZ2UKICAvUGFyZW50IDIgMCBSCiAg' +
'L1Jlc291cmNlcyA8PAogICAgL0ZvbnQgPDwKICAgICAgL0YxIDQgMCBSIAogICAgPj4KICA+' +
'PgogIC9Db250ZW50cyA1IDAgUgo+PgplbmRvYmoKCjQgMCBvYmoKPDwKICAvVHlwZSAvRm9u' +
'dAogIC9TdWJ0eXBlIC9UeXBlMQogIC9CYXNlRm9udCAvVGltZXMtUm9tYW4KPj4KZW5kb2Jq' +
'Cgo1IDAgb2JqICAlIHBhZ2UgY29udGVudAo8PAogIC9MZW5ndGggNDQKPj4Kc3RyZWFtCkJU' +
'CjcwIDUwIFRECi9GMSAxMiBUZgooSGVsbG8sIHdvcmxkISkgVGoKRVQKZW5kc3RyZWFtCmVu' +
'ZG9iagoKeHJlZgowIDYKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDEwIDAwMDAwIG4g' +
'CjAwMDAwMDAwNzkgMDAwMDAgbiAKMDAwMDAwMDE3MyAwMDAwMCBuIAowMDAwMDAwMzAxIDAw' +
'MDAwIG4gCjAwMDAwMDAzODAgMDAwMDAgbiAKdHJhaWxlcgo8PAogIC9TaXplIDYKICAvUm9v' +
'dCAxIDAgUgo+PgpzdGFydHhyZWYKNDkyCiUlRU9G');
return new Blob([pdfData], {type: 'application/pdf'});
}
setupTestBed({
imports: [CoreModule.forRoot()],
declarations: [BasicButtonComponent, TaskAuditDirective],
providers: [TaskListService]
});
beforeEach(async(() => {
fixture = TestBed.createComponent(BasicButtonComponent);
component = fixture.componentInstance;
service = TestBed.get(TaskListService);
jasmine.Ajax.install();
}));
afterEach(() => {
jasmine.Ajax.uninstall();
});
it('should fetch the pdf Blob when the format is pdf', fakeAsync(() => {
component.fileName = 'FakeAuditName';
component.format = 'pdf';
const blob = createFakePdfBlob();
spyOn(service, 'fetchTaskAuditPdfById').and.returnValue(of(blob));
spyOn(component, 'onAuditClick').and.callThrough();
fixture.detectChanges();
const button = fixture.nativeElement.querySelector('#auditButton');
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(component.onAuditClick).toHaveBeenCalledWith({ format: 'pdf', value: blob, fileName: 'FakeAuditName' });
});
button.click();
}));
it('should fetch the json info when the format is json', fakeAsync(() => {
component.fileName = 'FakeAuditName';
component.format = 'json';
component.download = true;
const auditJson = { taskId: '77', taskName: 'Fake Task Name', assignee: 'FirstName LastName', formData: [], selectedOutcome: null, comments: [] };
spyOn(service, 'fetchTaskAuditJsonById').and.returnValue(of(auditJson));
spyOn(component, 'onAuditClick').and.callThrough();
fixture.detectChanges();
const button = fixture.nativeElement.querySelector('#auditButton');
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(component.onAuditClick).toHaveBeenCalledWith({ format: 'json', value: auditJson, fileName: 'FakeAuditName' });
});
button.click();
}));
it('should fetch the pdf Blob as default when the format is UNKNOWN', fakeAsync(() => {
component.fileName = 'FakeAuditName';
component.format = 'fakeFormat';
const blob = createFakePdfBlob();
spyOn(service, 'fetchTaskAuditPdfById').and.returnValue(of(blob));
spyOn(component, 'onAuditClick').and.callThrough();
fixture.detectChanges();
const button = fixture.nativeElement.querySelector('#auditButton');
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(component.onAuditClick).toHaveBeenCalledWith({ format: 'pdf', value: blob, fileName: 'FakeAuditName' });
});
button.click();
}));
});

View File

@@ -0,0 +1,128 @@
/*!
* @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.
*/
/* tslint:disable:no-input-rename */
import { ContentService } from '@alfresco/adf-core';
import { Directive, EventEmitter, Input, OnChanges, Output } from '@angular/core';
import { TaskListService } from './../services/tasklist.service';
const JSON_FORMAT: string = 'json';
const PDF_FORMAT: string = 'pdf';
@Directive({
selector: 'button[adf-task-audit]',
host: {
'role': 'button',
'(click)': 'onClickAudit()'
}
})
export class TaskAuditDirective implements OnChanges {
/** (**required**) The id of the task. */
@Input('task-id')
taskId: string;
/** Name of the downloaded file (for PDF downloads). */
@Input()
fileName: string = 'Audit';
/** Format of the audit information. Can be "pdf" or "json". */
@Input()
format: string = 'pdf';
/** Enables downloading of the audit when the decorated element is clicked. */
@Input()
download: boolean = true;
/** Emitted when the decorated element is clicked. */
@Output()
clicked: EventEmitter<any> = new EventEmitter<any>();
/** Emitted when an error occurs. */
@Output()
error: EventEmitter<any> = new EventEmitter<any>();
public audit: any;
/**
*
* @param translateService
* @param taskListService
*/
constructor(private contentService: ContentService,
private taskListService: TaskListService) {
}
ngOnChanges(): void {
if (!this.isValidType()) {
this.setDefaultFormatType();
}
}
isValidType() {
if (this.format && (this.isJsonFormat() || this.isPdfFormat())) {
return true;
}
return false;
}
setDefaultFormatType(): void {
this.format = PDF_FORMAT;
}
/**
* fetch the audit information in the requested format
*/
fetchAuditInfo(): void {
if (this.isPdfFormat()) {
this.taskListService.fetchTaskAuditPdfById(this.taskId).subscribe(
(blob: Blob) => {
this.audit = blob;
if (this.download) {
this.contentService.downloadBlob(this.audit, this.fileName + '.pdf');
}
this.clicked.emit({ format: this.format, value: this.audit, fileName: this.fileName });
},
(err) => {
this.error.emit(err);
});
} else {
this.taskListService.fetchTaskAuditJsonById(this.taskId).subscribe(
(res) => {
this.audit = res;
this.clicked.emit({ format: this.format, value: this.audit, fileName: this.fileName });
},
(err) => {
this.error.emit(err);
});
}
}
onClickAudit() {
this.fetchAuditInfo();
}
isJsonFormat() {
return this.format === JSON_FORMAT;
}
isPdfFormat() {
return this.format === PDF_FORMAT;
}
}

View File

@@ -0,0 +1,148 @@
<div *ngIf="!taskDetails" data-automation-id="adf-tasks-details--empty">
<ng-template *ngIf="noTaskDetailsTemplateComponent" ngFor [ngForOf]="[data]"
[ngForTemplate]="noTaskDetailsTemplateComponent">
{{ 'ADF_TASK_LIST.DETAILS.MESSAGES.NONE' | translate }}
</ng-template>
<div *ngIf="!noTaskDetailsTemplateComponent">
{{ 'ADF_TASK_LIST.DETAILS.MESSAGES.NONE' | translate }}
</div>
</div>
<div *ngIf="taskDetails" class="adf-task-details">
<div *ngIf="showHeader" class="adf-task-details-header">
<h2 class="adf-activiti-task-details__header">
<span>{{taskDetails.name || 'No name'}}</span>
</h2>
</div>
<div class="adf-task-details-core"
fxLayout="column"
fxLayoutGap="8px"
fxLayout.lt-lg="column">
<div class="adf-task-details-core-form">
<div *ngIf="isAssigned()">
<adf-form *ngIf="isFormComponentVisible()" #activitiForm
[taskId]="taskDetails.id"
[showTitle]="showFormTitle"
[showRefreshButton]="showFormRefreshButton"
[showCompleteButton]="showFormCompleteButton"
[disableCompleteButton]="!isCompleteButtonEnabled()"
[showSaveButton]="isSaveButtonVisible()"
[readOnly]="internalReadOnlyForm"
[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>
<adf-attach-form *ngIf="isShowAttachForm()"
[taskId]="taskDetails.id"
[formKey]="taskDetails.formKey"
(cancelAttachForm)="onCancelAttachForm()"
(success)="onCompleteAttachForm()">
</adf-attach-form>
</div>
<div *ngIf="!isAssigned()" id="claim-message-id">
{{ 'ADF_TASK_LIST.DETAILS.MESSAGES.CLAIM' | translate }}
</div>
</div>
<div class="adf-task-details-core-sidebar">
<adf-info-drawer *ngIf="showHeaderContent" title="ADF_TASK_LIST.DETAILS.LABELS.INFO_DRAWER_TITLE" id="adf-task-details-core-sidebar-drawer" class="adf-task-details-core-sidebar-drawer">
<adf-info-drawer-tab label="ADF_TASK_LIST.DETAILS.LABELS.INFO_DRAWER_TAB_DETAILS_TITLE">
<div class="adf-assignment-container" *ngIf="showAssignee">
<adf-people-search
(searchPeople)="searchUser($event)"
(success)="assignTaskToUser($event)"
(closeSearch)="onCloseSearch()"
[results]="peopleSearch">
<ng-container adf-people-search-title>{{ 'ADF_TASK_LIST.DETAILS.LABELS.ADD_ASSIGNEE' | translate }}
</ng-container>
<ng-container adf-people-search-action-label>{{ 'ADF_TASK_LIST.PEOPLE.ADD_ASSIGNEE' | translate }}
</ng-container>
</adf-people-search>
</div>
<adf-task-header
[class]="getTaskHeaderViewClass()"
[taskDetails]="taskDetails"
[formName]="taskFormName"
(claim)="onClaimAction($event)"
(unclaim)="onUnclaimAction($event)">
</adf-task-header>
<adf-people *ngIf="showInvolvePeople" #people
[people]="taskPeople"
[readOnly]="internalReadOnlyForm"
[taskId]="taskDetails.id">
</adf-people>
</adf-info-drawer-tab>
<adf-info-drawer-tab label="ADF_TASK_LIST.DETAILS.LABELS.INFO_DRAWER_TAB_ACTIVITY_TITLE">
<mat-card *ngIf="showComments">
<mat-card-content>
<adf-comments #activitiComments
[readOnly]="isReadOnlyComment()"
[taskId]="taskDetails.id">
</adf-comments>
</mat-card-content>
</mat-card>
</adf-info-drawer-tab>
</adf-info-drawer>
<div *ngIf="showHeaderContent" class="adf-task-details-core-sidebar-checklist">
<div *ngIf="showChecklist">
<adf-checklist #activitiChecklist
[readOnly]="internalReadOnlyForm"
[taskId]="taskDetails.id"
[assignee]="taskDetails?.assignee?.id"
(checklistTaskCreated)="onChecklistTaskCreated($event)"
(checklistTaskDeleted)="onChecklistTaskDeleted($event)">
</adf-checklist>
</div>
</div>
</div>
</div>
<ng-template #errorDialog>
<h3 matDialogTitle>{{'ADF_TASK_LIST.DETAILS.ERROR.TITLE'|translate}}</h3>
<mat-dialog-content>
<p>{{'ADF_TASK_LIST.DETAILS.ERROR.DESCRIPTION'|translate}}</p>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button type="button" (click)="closeErrorDialog()">{{'ADF_TASK_LIST.DETAILS.ERROR.CLOSE'|translate}}
</button>
</mat-dialog-actions>
</ng-template>
</div>

View File

@@ -0,0 +1,104 @@
:host {
width: 100%;
}
.adf-error-dialog h3 {
margin: 16px 0;
}
.adf-activiti-task-details__header {
align-self: flex-end;
display: flex;
font-size: 24px;
font-weight: 300;
line-height: normal;
overflow: hidden;
margin: 8px 0 16px;
cursor: pointer;
user-select: none;
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
}
.adf-activiti-task-details__action-button {
text-transform: uppercase;
}
.adf-assignment-container {
padding: 10px 20px;
width: auto;
}
adf-task-header.adf-assign-edit-view ::ng-deep adf-card-view ::ng-deep
.adf-property[data-automation-id='header-assignee'] {
display: none;
}
.adf-task-details {
&-header {
display: flex;
justify-content: space-between;
&-toggle {
position: relative;
top: 10px;
margin-right: 2px;
height: 23px;
cursor: pointer;
user-select: none;
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
}
}
&-toggle {
position: relative;
}
&-core {
display: flex;
justify-content: space-between;
&-sidebar {
&-drawer {
@media screen and (max-width: 1279px) {
margin-left: 0;
}
}
&-checklist {
margin-top: 30px;
padding-left: 20px;
padding-right: 20px;
}
}
&-form {
flex-grow: 1;
& ::ng-deep .adf-form-debug-container {
display: flex;
flex-direction: column;
padding: 20px 0;
.mat-slide-toggle {
margin-left: auto;
& + div {
background-color: black;
padding: 20px;
clear: both;
margin-top: 30px;
color: white;
}
}
}
& ::ng-deep .mat-tab-label {
flex-grow: 1;
}
}
}
}

View File

@@ -0,0 +1,507 @@
/*!
* @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 { NO_ERRORS_SCHEMA, SimpleChange } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { of, throwError } from 'rxjs';
import {
FormModel,
FormOutcomeEvent,
FormOutcomeModel,
FormService,
setupTestBed,
BpmUserService
} from '@alfresco/adf-core';
import { CommentProcessService, LogService, AuthenticationService } from '@alfresco/adf-core';
import { UserProcessModel } from '@alfresco/adf-core';
import { TaskDetailsModel } from '../models/task-details.model';
import {
noDataMock,
taskDetailsMock,
standaloneTaskWithForm,
standaloneTaskWithoutForm,
taskFormMock,
tasksMock,
taskDetailsWithOutAssigneeMock
} from '../../mock';
import { TaskListService } from './../services/tasklist.service';
import { TaskDetailsComponent } from './task-details.component';
import { ProcessTestingModule } from '../../testing/process.testing.module';
import { PeopleProcessService } from '@alfresco/adf-core';
const fakeUser: UserProcessModel = new UserProcessModel({
id: 'fake-id',
firstName: 'fake-name',
lastName: 'fake-last',
email: 'fake@mail.com'
});
describe('TaskDetailsComponent', () => {
let service: TaskListService;
let formService: FormService;
let component: TaskDetailsComponent;
let fixture: ComponentFixture<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;
let authService: AuthenticationService;
setupTestBed({
imports: [
ProcessTestingModule
],
schemas: [NO_ERRORS_SCHEMA]
});
beforeEach(() => {
logService = TestBed.get(LogService);
const userService: BpmUserService = TestBed.get(BpmUserService);
spyOn(userService, 'getCurrentUserInfo').and.returnValue(of({}));
service = TestBed.get(TaskListService);
spyOn(service, 'getTaskChecklist').and.returnValue(of(noDataMock));
formService = TestBed.get(FormService);
getTaskDetailsSpy = spyOn(service, 'getTaskDetails').and.returnValue(of(taskDetailsMock));
spyOn(formService, 'getTaskForm').and.returnValue(of(taskFormMock));
taskDetailsMock.processDefinitionId = null;
spyOn(formService, 'getTask').and.returnValue(of(taskDetailsMock));
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);
spyOn(authService, 'getBpmLoggedUser').and.returnValue(of({ email: 'fake-email' }));
spyOn(commentProcessService, 'getTaskComments').and.returnValue(of([
{ message: 'Test1', created: Date.now(), createdBy: { firstName: 'Admin', lastName: 'User' } },
{ message: 'Test2', created: Date.now(), createdBy: { firstName: 'Admin', lastName: 'User' } },
{ message: 'Test3', created: Date.now(), createdBy: { firstName: 'Admin', lastName: 'User' } }
]));
fixture = TestBed.createComponent(TaskDetailsComponent);
peopleProcessService = TestBed.get(PeopleProcessService);
component = fixture.componentInstance;
});
afterEach(() => {
getTaskDetailsSpy.calls.reset();
fixture.destroy();
});
it('should load task details when taskId specified', () => {
component.taskId = '123';
fixture.detectChanges();
expect(getTaskDetailsSpy).toHaveBeenCalled();
});
it('should not load task details when no taskId is specified', () => {
fixture.detectChanges();
expect(getTaskDetailsSpy).not.toHaveBeenCalled();
});
it('should send a claim task event when a task is claimed', async(() => {
component.claimedTask.subscribe((taskId) => {
expect(taskId).toBe('FAKE-TASK-CLAIM');
});
component.onClaimAction('FAKE-TASK-CLAIM');
}));
it('should send a unclaim task event when a task is unclaimed', async(() => {
component.claimedTask.subscribe((taskId) => {
expect(taskId).toBe('FAKE-TASK-UNCLAIM');
});
component.onUnclaimAction('FAKE-TASK-UNCLAIM');
}));
it('should set a placeholder message when taskId not initialised', () => {
fixture.detectChanges();
expect(fixture.nativeElement.innerText).toBe('ADF_TASK_LIST.DETAILS.MESSAGES.NONE');
});
it('should display a form when the task has an associated form', async(() => {
component.taskId = '123';
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('adf-form'))).not.toBeNull();
}));
it('should display a form in readonly when the task has an associated form and readOnlyForm is true', async(() => {
component.readOnlyForm = true;
component.taskId = '123';
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('adf-form'))).not.toBeNull();
expect(fixture.debugElement.query(By.css('.adf-readonly-form'))).not.toBeNull();
}));
it('should not display a form when the task does not have an associated form', async(() => {
component.taskId = '123';
taskDetailsMock.formKey = undefined;
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('adf-form'))).toBeNull();
});
}));
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();
fixture.whenStable().then(() => {
const claimMessage = fixture.nativeElement.querySelector('#claim-message-id');
expect(claimMessage).toBeDefined();
expect(claimMessage.innerText).toBe('ADF_TASK_LIST.DETAILS.MESSAGES.CLAIM');
});
}));
it('should not display the claim message when the task is assigned', async(() => {
fixture.detectChanges();
fixture.whenStable().then(() => {
const claimMessage = fixture.nativeElement.querySelector('#claim-message-id');
expect(claimMessage).toBeNull();
});
}));
describe('change detection', () => {
let change;
let nullChange;
beforeEach(() => {
change = new SimpleChange('123', '456', true);
nullChange = new SimpleChange('123', null, true);
});
beforeEach(async(() => {
component.taskId = '123';
fixture.detectChanges();
fixture.whenStable().then(() => {
getTaskDetailsSpy.calls.reset();
});
}));
it('should fetch new task details when taskId changed', () => {
component.ngOnChanges({ 'taskId': change });
expect(getTaskDetailsSpy).toHaveBeenCalledWith('456');
});
it('should NOT fetch new task details when empty changeset made', async(() => {
fixture.detectChanges();
fixture.whenStable().then(() => {
component.ngOnChanges({});
expect(getTaskDetailsSpy).not.toHaveBeenCalled();
});
}));
it('should NOT fetch new task details when taskId changed to null', async(() => {
fixture.detectChanges();
fixture.whenStable().then(() => {
component.ngOnChanges({ 'taskId': nullChange });
expect(getTaskDetailsSpy).not.toHaveBeenCalled();
});
}));
it('should set a placeholder message when taskId changed to null', () => {
component.ngOnChanges({ 'taskId': nullChange });
fixture.detectChanges();
expect(fixture.nativeElement.innerText).toBe('ADF_TASK_LIST.DETAILS.MESSAGES.NONE');
});
});
describe('Form events', () => {
beforeEach(async(() => {
component.taskId = '123';
fixture.detectChanges();
fixture.whenStable();
}));
afterEach(() => {
const overlayContainers = <any> window.document.querySelectorAll('.cdk-overlay-container');
overlayContainers.forEach((overlayContainer) => {
overlayContainer.innerHTML = '';
});
});
it('should emit a save event when form saved', () => {
const emitSpy: jasmine.Spy = spyOn(component.formSaved, 'emit');
component.onFormSaved(new FormModel());
expect(emitSpy).toHaveBeenCalled();
});
it('should emit a outcome execution event when form outcome executed', () => {
const emitSpy: jasmine.Spy = spyOn(component.executeOutcome, 'emit');
component.onFormExecuteOutcome(new FormOutcomeEvent(new FormOutcomeModel(new FormModel())));
expect(emitSpy).toHaveBeenCalled();
});
it('should emit a complete event when form completed', () => {
const emitSpy: jasmine.Spy = spyOn(component.formCompleted, 'emit');
component.onFormCompleted(new FormModel());
expect(emitSpy).toHaveBeenCalled();
});
it('should load next task when form completed', () => {
component.onComplete();
expect(getTasksSpy).toHaveBeenCalled();
});
it('should show placeholder message if there is no next task', () => {
getTasksSpy.and.returnValue(of([]));
component.onComplete();
fixture.detectChanges();
expect(fixture.nativeElement.innerText).toBe('ADF_TASK_LIST.DETAILS.MESSAGES.NONE');
});
it('should emit an error event if an error occurs fetching the next task', () => {
const emitSpy: jasmine.Spy = spyOn(component.error, 'emit');
getTasksSpy.and.returnValue(throwError({}));
component.onComplete();
expect(emitSpy).toHaveBeenCalled();
});
it('should NOT load next task when form completed if showNextTask is false', () => {
component.showNextTask = false;
component.onComplete();
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();
expect(emitSpy).toHaveBeenCalled();
});
it('should call service to load next task when complete button clicked', () => {
component.onComplete();
expect(getTasksSpy).toHaveBeenCalled();
});
it('should emit a load event when form loaded', () => {
const emitSpy: jasmine.Spy = spyOn(component.formLoaded, 'emit');
component.onFormLoaded(new FormModel());
expect(emitSpy).toHaveBeenCalled();
});
it('should emit an error event when form error occurs', () => {
const emitSpy: jasmine.Spy = spyOn(component.error, 'emit');
component.onFormError({});
expect(emitSpy).toHaveBeenCalled();
});
it('should display a dialog to the user when a form error occurs', () => {
let dialogEl = window.document.querySelector('mat-dialog-content');
expect(dialogEl).toBeNull();
component.onFormError({});
fixture.detectChanges();
dialogEl = window.document.querySelector('mat-dialog-content');
expect(dialogEl).not.toBeNull();
});
it('should emit a task created event when checklist task is created', () => {
const emitSpy: jasmine.Spy = spyOn(component.taskCreated, 'emit');
const mockTask = new TaskDetailsModel(taskDetailsMock);
component.onChecklistTaskCreated(mockTask);
expect(emitSpy).toHaveBeenCalled();
});
});
describe('Comments', () => {
it('should comments be readonly if the task is complete and no user are involved', () => {
component.showComments = true;
component.showHeaderContent = true;
component.ngOnChanges({ 'taskId': new SimpleChange('123', '456', true) });
component.taskPeople = [];
component.taskDetails = new TaskDetailsModel(taskDetailsMock);
component.taskDetails.endDate = new Date('2017-10-03T17:03:57.311+0000');
fixture.detectChanges();
expect((component.activitiComments as any).readOnly).toBe(true);
});
it('should comments be readonly if the task is complete and user are NOT involved', () => {
component.showComments = true;
component.showHeaderContent = true;
component.ngOnChanges({ 'taskId': new SimpleChange('123', '456', true) });
component.taskPeople = [];
component.taskDetails = new TaskDetailsModel(taskDetailsMock);
component.taskDetails.endDate = new Date('2017-10-03T17:03:57.311+0000');
fixture.detectChanges();
expect((component.activitiComments as any).readOnly).toBe(true);
});
it('should comments NOT be readonly if the task is NOT complete and user are NOT involved', () => {
component.showComments = true;
component.showHeaderContent = true;
component.ngOnChanges({ 'taskId': new SimpleChange('123', '456', true) });
component.taskPeople = [fakeUser];
component.taskDetails = new TaskDetailsModel(taskDetailsMock);
component.taskDetails.endDate = null;
fixture.detectChanges();
expect((component.activitiComments as any).readOnly).toBe(false);
});
it('should comments NOT be readonly if the task is complete and user are involved', () => {
component.showComments = true;
component.showHeaderContent = true;
component.ngOnChanges({ 'taskId': new SimpleChange('123', '456', true) });
component.taskPeople = [fakeUser];
component.taskDetails = new TaskDetailsModel(taskDetailsMock);
component.taskDetails.endDate = new Date('2017-10-03T17:03:57.311+0000');
fixture.detectChanges();
expect((component.activitiComments as any).readOnly).toBe(false);
});
it('should comments be present if showComments is true', () => {
component.showComments = true;
component.showHeaderContent = true;
component.ngOnChanges({ 'taskId': new SimpleChange('123', '456', true) });
component.taskPeople = [];
component.taskDetails = new TaskDetailsModel(taskDetailsMock);
fixture.detectChanges();
expect(component.activitiComments).toBeDefined();
});
it('should comments NOT be present if showComments is false', () => {
component.showComments = false;
component.ngOnChanges({ 'taskId': new SimpleChange('123', '456', true) });
component.taskPeople = [];
component.taskDetails = new TaskDetailsModel(taskDetailsMock);
fixture.detectChanges();
expect(component.activitiComments).not.toBeDefined();
});
});
describe('assign task to user', () => {
beforeEach(() => {
component.taskId = '123';
fixture.detectChanges();
});
it('should return an observable with user search results', (done) => {
spyOn(peopleProcessService, 'getWorkflowUsers').and.returnValue(of([{
id: 1,
firstName: 'fake-test-1',
lastName: 'fake-last-1',
email: 'fake-test-1@test.com'
}, {
id: 2,
firstName: 'fake-test-2',
lastName: 'fake-last-2',
email: 'fake-test-2@test.com'
}]));
component.peopleSearch.subscribe((users) => {
expect(users.length).toBe(2);
expect(users[0].firstName).toBe('fake-test-1');
expect(users[0].lastName).toBe('fake-last-1');
expect(users[0].email).toBe('fake-test-1@test.com');
expect(users[0].id).toBe(1);
done();
});
component.searchUser('fake-search-word');
});
it('should return an empty list for not valid search', (done) => {
spyOn(peopleProcessService, 'getWorkflowUsers').and.returnValue(of([]));
component.peopleSearch.subscribe((users) => {
expect(users.length).toBe(0);
done();
});
component.searchUser('fake-search-word');
});
it('should log error message when search fails', async(() => {
spyOn(peopleProcessService, 'getWorkflowUsers').and.returnValue(throwError(''));
component.peopleSearch.subscribe(() => {
expect(logService.error).toHaveBeenCalledWith('Could not load users');
});
component.searchUser('fake-search');
}));
it('should assign task to user', () => {
component.assignTaskToUser(fakeUser);
expect(assignTaskSpy).toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,503 @@
/*!
* @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 { PeopleProcessService, UserProcessModel } from '@alfresco/adf-core';
import {
AuthenticationService,
CardViewUpdateService,
ClickNotification,
LogService,
UpdateNotification,
CommentsComponent
} from '@alfresco/adf-core';
import {
Component,
EventEmitter,
Input,
OnChanges,
OnInit,
Output,
SimpleChanges,
TemplateRef,
ViewChild,
OnDestroy
} from '@angular/core';
import { MatDialog, MatDialogRef } from '@angular/material';
import { Observable, Observer, Subject } from 'rxjs';
import { ContentLinkModel, FormFieldValidator, FormModel, FormOutcomeEvent } from '@alfresco/adf-core';
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 { share, takeUntil } from 'rxjs/operators';
@Component({
selector: 'adf-task-details',
templateUrl: './task-details.component.html',
styleUrls: ['./task-details.component.scss']
})
export class TaskDetailsComponent implements OnInit, OnChanges, OnDestroy {
@ViewChild('activitiComments')
activitiComments: CommentsComponent;
@ViewChild('activitiChecklist')
activitiChecklist: any;
@ViewChild('errorDialog')
errorDialog: TemplateRef<any>;
/** Toggles debug mode. */
@Input()
debugMode: boolean = false;
/** (**required**) The id of the task whose details we are asking for. */
@Input()
taskId: string;
/** Automatically renders the next task when the current one is completed. */
@Input()
showNextTask: boolean = true;
/** Toggles task details Header component. */
@Input()
showHeader: boolean = true;
/** Toggles collapsed/expanded state of the Header component. */
@Input()
showHeaderContent: boolean = true;
/** Toggles `Involve People` feature for the Header component. */
@Input()
showInvolvePeople: boolean = true;
/** Toggles `Comments` feature for the Header component. */
@Input()
showComments: boolean = true;
/** Toggles `Checklist` feature for the Header component. */
@Input()
showChecklist: boolean = true;
/** 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;
/** 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;
/** Field validators for use with the form. */
@Input()
fieldValidators: FormFieldValidator[] = [];
/** Emitted when the form is submitted with the `Save` or custom outcomes. */
@Output()
formSaved: EventEmitter<FormModel> = new EventEmitter<FormModel>();
/** Emitted when the form is submitted with the `Complete` outcome. */
@Output()
formCompleted: EventEmitter<FormModel> = new EventEmitter<FormModel>();
/** Emitted when the form field content is clicked. */
@Output()
formContentClicked: EventEmitter<ContentLinkModel> = new EventEmitter<ContentLinkModel>();
/** Emitted when the form is loaded or reloaded. */
@Output()
formLoaded: EventEmitter<FormModel> = new EventEmitter<FormModel>();
/** Emitted when a checklist task is created. */
@Output()
taskCreated: EventEmitter<TaskDetailsModel> = new EventEmitter<TaskDetailsModel>();
/** Emitted when a checklist task is deleted. */
@Output()
taskDeleted: EventEmitter<string> = new EventEmitter<string>();
/** Emitted when an error occurs. */
@Output()
error: EventEmitter<any> = new EventEmitter<any>();
/** Emitted when any outcome is executed. Default behaviour can be prevented
* via `event.preventDefault()`.
*/
@Output()
executeOutcome: EventEmitter<FormOutcomeEvent> = new EventEmitter<FormOutcomeEvent>();
/** Emitted when a task is assigned. */
@Output()
assignTask: EventEmitter<void> = new EventEmitter<void>();
/** Emitted when a task is claimed. */
@Output()
claimedTask: EventEmitter<string> = new EventEmitter<string>();
/** Emitted when a task is unclaimed. */
@Output()
unClaimedTask: EventEmitter<string> = new EventEmitter<string>();
taskDetails: TaskDetailsModel;
taskFormName: string = null;
taskPeople: UserProcessModel[] = [];
noTaskDetailsTemplateComponent: TemplateRef<any>;
showAssignee: boolean = false;
showAttachForm: boolean = false;
internalReadOnlyForm: boolean = false;
private peopleSearchObserver: Observer<UserProcessModel[]>;
public errorDialogRef: MatDialogRef<TemplateRef<any>>;
private onDestroy$ = new Subject<boolean>();
peopleSearch: Observable<UserProcessModel[]>;
currentLoggedUser: UserRepresentation;
data: any;
constructor(private taskListService: TaskListService,
private authService: AuthenticationService,
private peopleProcessService: PeopleProcessService,
private logService: LogService,
private cardViewUpdateService: CardViewUpdateService,
private dialog: MatDialog) {
}
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);
}
this.cardViewUpdateService.itemUpdated$
.pipe(takeUntil(this.onDestroy$))
.subscribe(this.updateTaskDetails.bind(this));
this.cardViewUpdateService.itemClicked$
.pipe(takeUntil(this.onDestroy$))
.subscribe(this.clickTaskDetails.bind(this));
}
ngOnDestroy() {
this.onDestroy$.next(true);
this.onDestroy$.complete();
}
ngOnChanges(changes: SimpleChanges): void {
const taskId = changes.taskId;
this.showAssignee = false;
if (taskId && !taskId.currentValue) {
this.reset();
} else if (taskId && taskId.currentValue) {
this.loadDetails(taskId.currentValue);
}
}
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;
}
/**
* Reset the task details
*/
private reset() {
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;
}
/**
* Save a task detail and update it after a successful response
*
* @param updateNotification
*/
private updateTaskDetails(updateNotification: UpdateNotification) {
this.taskListService
.updateTask(this.taskId, updateNotification.changed)
.subscribe(() => this.loadDetails(this.taskId));
}
private clickTaskDetails(clickNotification: ClickNotification) {
if (clickNotification.target.key === 'assignee') {
this.showAssignee = true;
}
if (clickNotification.target.key === 'formName') {
this.showAttachForm = true;
}
}
/**
* Load the activiti task details
* @param taskId
*/
private loadDetails(taskId: string) {
this.taskPeople = [];
this.taskFormName = null;
if (taskId) {
this.taskListService.getTaskDetails(taskId).subscribe(
(res: TaskDetailsModel) => {
this.showAttachForm = false;
this.taskDetails = res;
if (this.taskDetails.name === 'null') {
this.taskDetails.name = 'No name';
}
const endDate: any = res.endDate;
if (endDate && !isNaN(endDate.getTime())) {
this.internalReadOnlyForm = true;
} else {
this.internalReadOnlyForm = this.readOnlyForm;
}
if (this.taskDetails && this.taskDetails.involvedPeople) {
this.taskDetails.involvedPeople.forEach((user) => {
this.taskPeople.push(new UserProcessModel(user));
});
}
});
}
}
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.taskDetails.assignee.email, this.currentLoggedUser.email) :
this.isExternalIdEqual(this.taskDetails.assignee.externalId, this.currentLoggedUser.externalId);
}
private isEmailEqual(assigneeMail, currentLoggedEmail): boolean {
return assigneeMail.toLocaleLowerCase() === currentLoggedEmail.toLocaleLowerCase();
}
private isExternalIdEqual(assigneeExternalId, currentUserExternalId): 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
* @param processDefinitionId
*/
private loadNextTask(processInstanceId: string, processDefinitionId: string): void {
const requestNode = new TaskQueryRequestRepresentationModel(
{
processInstanceId: processInstanceId,
processDefinitionId: processDefinitionId
}
);
this.taskListService.getTasks(requestNode).subscribe(
(response) => {
if (response && response.length > 0) {
this.taskDetails = new TaskDetailsModel(response[0]);
} else {
this.reset();
}
}, (error) => {
this.error.emit(error);
});
}
/**
* Complete button clicked
*/
onComplete(): void {
this.taskListService
.completeTask(this.taskId)
.subscribe(() => this.onFormCompleted(null));
}
onShowAttachForm() {
this.showAttachForm = true;
}
onCancelAttachForm() {
this.showAttachForm = false;
}
onCompleteAttachForm() {
this.showAttachForm = false;
this.loadDetails(this.taskId);
}
onFormContentClick(content: ContentLinkModel): void {
this.formContentClicked.emit(content);
}
onFormSaved(form: FormModel): void {
this.formSaved.emit(form);
}
onFormCompleted(form: FormModel): void {
this.formCompleted.emit(form);
if (this.showNextTask && (this.taskDetails.processInstanceId || this.taskDetails.processDefinitionId)) {
this.loadNextTask(this.taskDetails.processInstanceId, this.taskDetails.processDefinitionId);
}
}
onFormLoaded(form: FormModel): void {
this.taskFormName = (form && form.name ? form.name : null);
this.formLoaded.emit(form);
}
onChecklistTaskCreated(task: TaskDetailsModel): void {
this.taskCreated.emit(task);
}
onChecklistTaskDeleted(taskId: string): void {
this.taskDeleted.emit(taskId);
}
onFormError(error: any): void {
this.errorDialogRef = this.dialog.open(this.errorDialog, { width: '500px' });
this.error.emit(error);
}
onFormExecuteOutcome(event: FormOutcomeEvent): void {
this.executeOutcome.emit(event);
}
closeErrorDialog(): void {
this.dialog.closeAll();
}
onClaimAction(taskId: string): void {
this.claimedTask.emit(taskId);
this.loadDetails(taskId);
}
onUnclaimAction(taskId: string): void {
this.unClaimedTask.emit(taskId);
this.loadDetails(taskId);
}
isCompletedTask(): boolean {
return this.taskDetails && this.taskDetails.endDate ? true : undefined;
}
searchUser(searchedWord: string) {
this.peopleProcessService.getWorkflowUsers(null, searchedWord)
.subscribe(
users => {
users = users.filter((user) => user.id !== this.taskDetails.assignee.id);
this.peopleSearchObserver.next(users);
},
() => this.logService.error('Could not load users')
);
}
onCloseSearch() {
this.showAssignee = false;
}
assignTaskToUser(selectedUser: UserProcessModel) {
this.taskListService
.assignTask(this.taskDetails.id, selectedUser)
.subscribe(() => {
this.logService.info('Task Assigned to ' + selectedUser.email);
this.assignTask.emit();
});
this.showAssignee = false;
}
getTaskHeaderViewClass(): string {
if (this.showAssignee) {
return 'assign-edit-view';
} else {
return 'default-view';
}
}
isReadOnlyComment(): boolean {
return (this.taskDetails && this.taskDetails.isCompleted()) && (this.taskPeople && this.taskPeople.length === 0);
}
}

View File

@@ -0,0 +1,9 @@
<div class="menu-container">
<mat-list class="adf-menu-list">
<mat-list-item (click)="selectFilterAndEmit(filter)" *ngFor="let filter of filters"
class="adf-filters__entry" [class.adf-active]="currentFilter === filter">
<mat-icon *ngIf="showIcon" matListIcon class="adf-filters__entry-icon">{{getFilterIcon(filter.icon)}}</mat-icon>
<span matLine [attr.data-automation-id]="filter.name + '_filter'">{{filter.name}}</span>
</mat-list-item>
</mat-list>
</div>

View File

@@ -0,0 +1,34 @@
@mixin adf-task-list-filters-task-theme($theme) {
$primary: map-get($theme, primary);
.adf {
&-filters__entry {
cursor: pointer;
font-size: 14px!important;
font-weight: bold;
opacity: 0.54;
padding-left: 30px;
.mat-list-item-content {
height: 34px;
}
}
&-filters__entry-icon {
padding-right: 12px !important;
padding-left: 0 !important;
}
&-filters__entry {
&.adf-active, &:hover {
color: mat-color($primary);
opacity: 1;
}
}
&-menu-list {
padding-top: 0px!important;
}
}
}

View File

@@ -0,0 +1,389 @@
/*!
* @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 { SimpleChange } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AppConfigService, AppsProcessService, setupTestBed } from '@alfresco/adf-core';
import { from, of } from 'rxjs';
import { FilterParamsModel, FilterRepresentationModel } from '../models/filter.model';
import { TaskListService } from '../services/tasklist.service';
import { TaskFilterService } from '../services/task-filter.service';
import { TaskFiltersComponent } from './task-filters.component';
import { ProcessTestingModule } from '../../testing/process.testing.module';
import { By } from '@angular/platform-browser';
describe('TaskFiltersComponent', () => {
let taskListService: TaskListService;
let taskFilterService: TaskFilterService;
let appsProcessService: AppsProcessService;
const fakeGlobalFilter = [];
fakeGlobalFilter.push(new FilterRepresentationModel({
name: 'FakeInvolvedTasks',
icon: 'glyphicon-align-left',
id: 10,
filter: { state: 'open', assignment: 'fake-involved' }
}));
fakeGlobalFilter.push(new FilterRepresentationModel({
name: 'FakeMyTasks1',
icon: 'glyphicon-ok-sign',
id: 11,
filter: { state: 'open', assignment: 'fake-assignee' }
}));
fakeGlobalFilter.push(new FilterRepresentationModel({
name: 'FakeMyTasks2',
icon: 'glyphicon-inbox',
id: 12,
filter: { state: 'open', assignment: 'fake-assignee' }
}));
const fakeGlobalFilterPromise = new Promise(function (resolve) {
resolve(fakeGlobalFilter);
});
const fakeGlobalEmptyFilter = {
message: 'invalid data'
};
const fakeGlobalEmptyFilterPromise = new Promise(function (resolve) {
resolve(fakeGlobalEmptyFilter);
});
const mockErrorFilterList = {
error: 'wrong request'
};
const mockErrorFilterPromise = Promise.reject(mockErrorFilterList);
let component: TaskFiltersComponent;
let fixture: ComponentFixture<TaskFiltersComponent>;
setupTestBed({
imports: [
ProcessTestingModule
]
});
beforeEach(() => {
const appConfig: AppConfigService = TestBed.get(AppConfigService);
appConfig.config.bpmHost = 'http://localhost:9876/bpm';
fixture = TestBed.createComponent(TaskFiltersComponent);
component = fixture.componentInstance;
taskListService = TestBed.get(TaskListService);
taskFilterService = TestBed.get(TaskFilterService);
appsProcessService = TestBed.get(AppsProcessService);
});
it('should emit an error with a bad response', (done) => {
spyOn(taskFilterService, 'getTaskListFilters').and.returnValue(from(mockErrorFilterPromise));
const appId = '1';
const change = new SimpleChange(null, appId, true);
component.ngOnChanges({ 'appId': change });
component.error.subscribe((err) => {
expect(err).toBeDefined();
done();
});
});
it('should return the filter task list', (done) => {
spyOn(taskFilterService, 'getTaskListFilters').and.returnValue(from(fakeGlobalFilterPromise));
const appId = '1';
const change = new SimpleChange(null, appId, true);
component.ngOnChanges({ 'appId': change });
component.success.subscribe((res) => {
expect(res).toBeDefined();
expect(component.filters).toBeDefined();
expect(component.filters.length).toEqual(3);
expect(component.filters[0].name).toEqual('FakeInvolvedTasks');
expect(component.filters[1].name).toEqual('FakeMyTasks1');
expect(component.filters[2].name).toEqual('FakeMyTasks2');
done();
});
component.ngOnInit();
});
it('should return the filter task list, filtered By Name', (done) => {
const fakeDeployedApplicationsPromise = new Promise(function (resolve) {
resolve({});
});
spyOn(appsProcessService, 'getDeployedApplicationsByName').and.returnValue(from(fakeDeployedApplicationsPromise));
spyOn(taskFilterService, 'getTaskListFilters').and.returnValue(from(fakeGlobalFilterPromise));
const change = new SimpleChange(null, 'test', true);
component.ngOnChanges({ 'appName': change });
component.success.subscribe((res) => {
const deployApp: any = appsProcessService.getDeployedApplicationsByName;
expect(deployApp.calls.count()).toEqual(1);
expect(res).toBeDefined();
done();
});
component.ngOnInit();
});
it('should select the first filter as default', (done) => {
spyOn(taskFilterService, 'getTaskListFilters').and.returnValue(from(fakeGlobalFilterPromise));
const appId = '1';
const change = new SimpleChange(null, appId, true);
fixture.detectChanges();
component.ngOnChanges({ 'appId': change });
component.success.subscribe((res) => {
expect(res).toBeDefined();
expect(component.currentFilter).toBeDefined();
expect(component.currentFilter.name).toEqual('FakeInvolvedTasks');
done();
});
});
it('should be able to fetch and select the default if the input filter is not valid', (done) => {
spyOn(taskFilterService, 'getTaskListFilters').and.returnValue(from(fakeGlobalEmptyFilterPromise));
spyOn(component, 'createFiltersByAppId').and.stub();
const appId = '1';
const change = new SimpleChange(null, appId, true);
component.ngOnChanges({ 'appId': change });
component.success.subscribe((res) => {
expect(res).toBeDefined();
expect(component.createFiltersByAppId).not.toHaveBeenCalled();
done();
});
});
it('should select the task filter based on the input by name param', (done) => {
spyOn(taskFilterService, 'getTaskListFilters').and.returnValue(from(fakeGlobalFilterPromise));
component.filterParam = new FilterParamsModel({ name: 'FakeMyTasks1' });
const appId = '1';
const change = new SimpleChange(null, appId, true);
fixture.detectChanges();
component.ngOnChanges({ 'appId': change });
component.success.subscribe((res) => {
expect(res).toBeDefined();
expect(component.currentFilter).toBeDefined();
expect(component.currentFilter.name).toEqual('FakeMyTasks1');
done();
});
});
it('should select the default task filter if filter input does not exist', (done) => {
spyOn(taskFilterService, 'getTaskListFilters').and.returnValue(from(fakeGlobalFilterPromise));
component.filterParam = new FilterParamsModel({ name: 'UnexistableFilter' });
const appId = '1';
const change = new SimpleChange(null, appId, true);
fixture.detectChanges();
component.ngOnChanges({ 'appId': change });
component.success.subscribe((res) => {
expect(res).toBeDefined();
expect(component.currentFilter).toBeDefined();
expect(component.currentFilter.name).toEqual('FakeInvolvedTasks');
done();
});
});
it('should select the task filter based on the input by index param', (done) => {
spyOn(taskFilterService, 'getTaskListFilters').and.returnValue(from(fakeGlobalFilterPromise));
component.filterParam = new FilterParamsModel({ index: 2 });
const appId = '1';
const change = new SimpleChange(null, appId, true);
fixture.detectChanges();
component.ngOnChanges({ 'appId': change });
component.success.subscribe((res) => {
expect(res).toBeDefined();
expect(component.currentFilter).toBeDefined();
expect(component.currentFilter.name).toEqual('FakeMyTasks2');
done();
});
});
it('should select the task filter based on the input by id param', (done) => {
spyOn(taskFilterService, 'getTaskListFilters').and.returnValue(from(fakeGlobalFilterPromise));
component.filterParam = new FilterParamsModel({ id: 10 });
const appId = '1';
const change = new SimpleChange(null, appId, true);
fixture.detectChanges();
component.ngOnChanges({ 'appId': change });
component.success.subscribe((res) => {
expect(res).toBeDefined();
expect(component.currentFilter).toBeDefined();
expect(component.currentFilter.name).toEqual('FakeInvolvedTasks');
done();
});
});
it('should emit an event when a filter is selected', (done) => {
const currentFilter = fakeGlobalFilter[0];
component.filters = fakeGlobalFilter;
component.filterClick.subscribe((filter: FilterRepresentationModel) => {
expect(filter).toBeDefined();
expect(filter).toEqual(currentFilter);
expect(component.currentFilter).toEqual(currentFilter);
done();
});
component.selectFilterAndEmit(currentFilter);
});
it('should reload filters by appId on binding changes', () => {
spyOn(component, 'getFiltersByAppId').and.stub();
const appId = '1';
const change = new SimpleChange(null, appId, true);
component.ngOnChanges({ 'appId': change });
expect(component.getFiltersByAppId).toHaveBeenCalledWith(appId);
});
it('should reload filters by appId null on binding changes', () => {
spyOn(component, 'getFiltersByAppId').and.stub();
const appId = null;
const change = new SimpleChange(undefined, appId, true);
component.ngOnChanges({ 'appId': change });
expect(component.getFiltersByAppId).toHaveBeenCalledWith(appId);
});
it('should change current filter when filterParam (id) changes', async () => {
component.filters = fakeGlobalFilter;
component.currentFilter = null;
fixture.whenStable().then(() => {
expect(component.currentFilter.id).toEqual(fakeGlobalFilter[2].id);
});
const change = new SimpleChange(null, { id: fakeGlobalFilter[2].id }, true);
component.ngOnChanges({ 'filterParam': change });
});
it('should change current filter when filterParam (name) changes', async () => {
component.filters = fakeGlobalFilter;
component.currentFilter = null;
fixture.whenStable().then(() => {
expect(component.currentFilter.name).toEqual(fakeGlobalFilter[2].name);
});
const change = new SimpleChange(null, { name: fakeGlobalFilter[2].name }, true);
component.ngOnChanges({ 'filterParam': change });
});
it('should reload filters by app name on binding changes', () => {
spyOn(component, 'getFiltersByAppName').and.stub();
const appName = 'fake-app-name';
const change = new SimpleChange(null, appName, true);
component.ngOnChanges({ 'appName': change });
expect(component.getFiltersByAppName).toHaveBeenCalledWith(appName);
});
it('should return the current filter after one is selected', () => {
const filter = fakeGlobalFilter[1];
component.filters = fakeGlobalFilter;
expect(component.currentFilter).toBeUndefined();
component.selectFilter(filter);
expect(component.getCurrentFilter()).toBe(filter);
});
it('should load default list when app id is null', () => {
spyOn(component, 'getFiltersByAppId').and.stub();
const change = new SimpleChange(undefined, null, true);
component.ngOnChanges({ 'appId': change });
expect(component.getFiltersByAppId).toHaveBeenCalled();
});
it('should not change the current filter if no filter with taskid is found', async(() => {
const filter = new FilterRepresentationModel({
name: 'FakeMyTasks',
filter: { state: 'open', assignment: 'fake-assignee' }
});
component.filters = fakeGlobalFilter;
component.currentFilter = filter;
spyOn(taskListService, 'isTaskRelatedToFilter').and.returnValue(of(null));
component.selectFilterWithTask('111');
expect(component.currentFilter).toBe(filter);
}));
it('should attach specific icon for each filter if showIcon is true', (done) => {
spyOn(taskFilterService, 'getTaskListFilters').and.returnValue(from(fakeGlobalFilterPromise));
component.showIcon = true;
const change = new SimpleChange(undefined, 1, true);
component.ngOnChanges({ 'appId': change });
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(component.filters.length).toBe(3);
const filters: any = fixture.debugElement.queryAll(By.css('.adf-filters__entry-icon'));
expect(filters.length).toBe(3);
expect(filters[0].nativeElement.innerText).toContain('format_align_left');
expect(filters[1].nativeElement.innerText).toContain('check_circle');
expect(filters[2].nativeElement.innerText).toContain('inbox');
done();
});
});
it('should not attach icons for each filter if showIcon is false', (done) => {
spyOn(taskFilterService, 'getTaskListFilters').and.returnValue(from(fakeGlobalFilterPromise));
component.showIcon = false;
const change = new SimpleChange(undefined, 1, true);
component.ngOnChanges({ 'appId': change });
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
const filters: any = fixture.debugElement.queryAll(By.css('.adf-filters__entry-icon'));
expect(filters.length).toBe(0);
done();
});
});
});

View File

@@ -0,0 +1,238 @@
/*!
* @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 { AppsProcessService } from '@alfresco/adf-core';
import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core';
import { Observable } from 'rxjs';
import { FilterParamsModel, FilterRepresentationModel } from '../models/filter.model';
import { TaskFilterService } from './../services/task-filter.service';
import { TaskListService } from './../services/tasklist.service';
import { IconModel } from '../../app-list/icon.model';
@Component({
selector: 'adf-task-filters',
templateUrl: './task-filters.component.html',
styleUrls: ['task-filters.component.scss']
})
export class TaskFiltersComponent implements OnInit, OnChanges {
/** Parameters to use for the task filter. If there is no match then
* the default filter (the first one the list) is selected.
*/
@Input()
filterParam: FilterParamsModel;
/** Emitted when a filter in the list is clicked. */
@Output()
filterClick: EventEmitter<FilterRepresentationModel> = new EventEmitter<FilterRepresentationModel>();
/** Emitted when the list is loaded. */
@Output()
success: EventEmitter<any> = new EventEmitter<any>();
/** Emitted when an error occurs during loading. */
@Output()
error: EventEmitter<any> = new EventEmitter<any>();
/** Display filters available to the current user for the application with the specified ID. */
@Input()
appId: number;
/** Display filters available to the current user for the application with the specified name. */
@Input()
appName: string;
/** Toggles display of the filter's icon. */
@Input()
showIcon: boolean;
filter$: Observable<FilterRepresentationModel>;
currentFilter: FilterRepresentationModel;
filters: FilterRepresentationModel [] = [];
private iconsMDL: IconModel;
constructor(private taskFilterService: TaskFilterService,
private taskListService: TaskListService,
private appsProcessService: AppsProcessService) {
}
ngOnInit() {
this.iconsMDL = new IconModel();
}
ngOnChanges(changes: SimpleChanges) {
const appName = changes['appName'];
const appId = changes['appId'];
const filter = changes['filterParam'];
if (appName && appName.currentValue) {
this.getFiltersByAppName(appName.currentValue);
} else if (appId && appId.currentValue !== appId.previousValue) {
this.getFiltersByAppId(appId.currentValue);
} else if (filter && filter.currentValue !== filter.previousValue) {
this.selectFilter(filter.currentValue);
}
}
/**
* Return the task list filtered by appId or by appName
* @param appId
* @param appName
*/
getFilters(appId?: number, appName?: string) {
appName ? this.getFiltersByAppName(appName) : this.getFiltersByAppId(appId);
}
/**
* Return the filter list filtered by appId
* @param appId - optional
*/
getFiltersByAppId(appId?: number) {
this.taskFilterService.getTaskListFilters(appId).subscribe(
(res: FilterRepresentationModel[]) => {
if (res.length === 0 && this.isFilterListEmpty()) {
this.createFiltersByAppId(appId);
} else {
this.resetFilter();
this.filters = res;
this.selectFilter(this.filterParam);
this.success.emit(res);
}
},
(err: any) => {
this.error.emit(err);
}
);
}
/**
* Return the filter list filtered by appName
* @param appName
*/
getFiltersByAppName(appName: string) {
this.appsProcessService.getDeployedApplicationsByName(appName).subscribe(
(application) => {
this.getFiltersByAppId(application.id);
},
(err) => {
this.error.emit(err);
});
}
/**
* Create default filters by appId
* @param appId
*/
createFiltersByAppId(appId?: number) {
this.taskFilterService.createDefaultFilters(appId).subscribe(
(resDefault: FilterRepresentationModel[]) => {
this.resetFilter();
this.filters = resDefault;
this.selectFilter(this.filterParam);
this.success.emit(resDefault);
},
(errDefault: any) => {
this.error.emit(errDefault);
}
);
}
/**
* Pass the selected filter as next
* @param filter
*/
public selectFilter(newFilter: FilterParamsModel) {
if (newFilter) {
this.currentFilter = this.filters.find( (filter, index) =>
newFilter.index === index ||
newFilter.id === filter.id ||
(newFilter.name &&
(newFilter.name.toLocaleLowerCase() === filter.name.toLocaleLowerCase())
));
}
if (!this.currentFilter) {
this.selectDefaultTaskFilter();
}
}
public selectFilterAndEmit(newFilter: FilterParamsModel) {
this.selectFilter(newFilter);
this.filterClick.emit(this.currentFilter);
}
/**
* Select filter with task
* @param taskId
*/
public selectFilterWithTask(taskId: string) {
const filteredFilterList: FilterRepresentationModel[] = [];
this.taskListService.getFilterForTaskById(taskId, this.filters).subscribe(
(filter: FilterRepresentationModel) => {
filteredFilterList.push(filter);
},
(err) => {
this.error.emit(err);
},
() => {
if (filteredFilterList.length > 0) {
this.selectFilter(filteredFilterList[0]);
this.filterClick.emit(this.currentFilter);
}
});
}
/**
* Select as default task filter the first in the list
* @param filteredFilterList
*/
public selectDefaultTaskFilter() {
if (!this.isFilterListEmpty()) {
this.currentFilter = this.filters[0];
}
}
/**
* Return the current task
*/
getCurrentFilter(): FilterRepresentationModel {
return this.currentFilter;
}
/**
* Check if the filter list is empty
*/
isFilterListEmpty(): boolean {
return this.filters === undefined || (this.filters && this.filters.length === 0);
}
/**
* Reset the filters properties
*/
private resetFilter() {
this.filters = [];
this.currentFilter = undefined;
}
/**
* Return current filter icon
*/
getFilterIcon(icon): string {
return this.iconsMDL.mapGlyphiconToMaterialDesignIcons(icon);
}
}

View File

@@ -0,0 +1,12 @@
<mat-card *ngIf="taskDetails" class="adf-card-container">
<mat-card-content>
<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 }}
</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>
</mat-card-actions>
</mat-card>

View File

@@ -0,0 +1,40 @@
@mixin adf-task-list-header-theme($theme) {
$primary: map-get($theme, primary);
.adf {
&-controls {
display: flex;
justify-content: space-between;
}
&-edit-controls {
display: flex;
justify-content: flex-end;
margin-left: auto;
}
&-switch-to-edit-mode,
&-save-edit-mode {
color: mat-color($primary);
}
&-cancel-edit-mode,
&-claim-controls {
color: rgb(131, 131, 131);
}
&-card-container {
font-family: inherit;
}
}
@media screen and ($mat-small) {
adf-card-view .adf-property-value {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
}
}

View File

@@ -0,0 +1,379 @@
/*!
* @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 { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { AppConfigService, setupTestBed } from '@alfresco/adf-core';
import { BpmUserService } from '@alfresco/adf-core';
import { of } from 'rxjs';
import {
completedTaskDetailsMock,
taskDetailsMock,
claimableTaskDetailsMock,
claimedTaskDetailsMock,
claimedByGroupMemberMock,
taskDetailsWithOutCandidateGroup
} from '../../mock';
import { TaskDetailsModel } from '../models/task-details.model';
import { TaskListService } from './../services/tasklist.service';
import { TaskHeaderComponent } from './task-header.component';
import { ProcessTestingModule } from '../../testing/process.testing.module';
describe('TaskHeaderComponent', () => {
let service: TaskListService;
let component: TaskHeaderComponent;
let fixture: ComponentFixture<TaskHeaderComponent>;
let userBpmService: BpmUserService;
let appConfigService: AppConfigService;
const fakeBpmAssignedUser = {
id: 1001,
apps: [],
capabilities: 'fake-capability',
company: 'fake-company',
created: 'fake-create-date',
email: 'wilbur@app.activiti.com',
externalId: 'fake-external-id',
firstName: 'Wilbur',
lastName: 'Adams',
fullname: 'Wilbur Adams',
groups: []
};
setupTestBed({
imports: [
ProcessTestingModule
]
});
beforeEach(() => {
fixture = TestBed.createComponent(TaskHeaderComponent);
component = fixture.componentInstance;
service = TestBed.get(TaskListService);
userBpmService = TestBed.get(BpmUserService);
spyOn(userBpmService, 'getCurrentUserInfo').and.returnValue(of(fakeBpmAssignedUser));
component.taskDetails = new TaskDetailsModel(taskDetailsMock);
appConfigService = TestBed.get(AppConfigService);
});
it('should render empty component if no task details provided', async(() => {
component.taskDetails = undefined;
fixture.detectChanges();
expect(fixture.debugElement.children.length).toBe(0);
}));
it('should display assignee', async(() => {
component.refreshData();
fixture.detectChanges();
fixture.whenStable().then(() => {
const formNameEl = fixture.debugElement.query(By.css('[data-automation-id="header-assignee"] .adf-textitem-clickable-value'));
expect(formNameEl.nativeElement.innerText).toBe('Wilbur Adams');
});
}));
it('should display placeholder if no assignee', async(() => {
component.taskDetails.assignee = null;
component.refreshData();
fixture.detectChanges();
fixture.whenStable().then(() => {
const valueEl = fixture.debugElement.query(By.css('[data-automation-id="header-assignee"] .adf-textitem-clickable-value'));
expect(valueEl.nativeElement.innerText).toBe('ADF_TASK_LIST.PROPERTIES.ASSIGNEE_DEFAULT');
});
}));
it('should display priority', async(() => {
component.taskDetails.priority = 27;
component.refreshData();
fixture.detectChanges();
fixture.whenStable().then(() => {
const formNameEl = fixture.debugElement.query(By.css('[data-automation-id="card-textitem-value-priority"]'));
expect(formNameEl.nativeElement.innerText).toBe('27');
});
}));
it('should set editable to false if the task has already completed', async(() => {
component.taskDetails.endDate = new Date('05/05/2002');
component.refreshData();
fixture.detectChanges();
fixture.whenStable().then(() => {
const datePicker = fixture.debugElement.query(By.css(`[data-automation-id="datepicker-dueDate"]`));
expect(datePicker).toBeNull('Datepicker should NOT be in DOM');
});
}));
it('should set editable to true if the task has not completed yet', async(() => {
component.taskDetails.endDate = undefined;
component.refreshData();
fixture.detectChanges();
fixture.whenStable().then(() => {
const datePicker = fixture.debugElement.query(By.css(`[data-automation-id="datepicker-dueDate"]`));
expect(datePicker).not.toBeNull('Datepicker should be in DOM');
});
}));
describe('Claiming', () => {
it('should display the claim button if no assignee', async(() => {
component.taskDetails = new TaskDetailsModel(claimableTaskDetailsMock);
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 display the claim button if the task is claimable', async(() => {
component.taskDetails = new TaskDetailsModel(claimableTaskDetailsMock);
component.refreshData();
fixture.detectChanges();
fixture.whenStable().then(() => {
const claimButton = fixture.debugElement.query(By.css('[data-automation-id="header-claim-button"]'));
expect(component.isTaskClaimable()).toBeTruthy();
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(() => {
component.taskDetails = new TaskDetailsModel(taskDetailsWithOutCandidateGroup);
component.refreshData();
fixture.detectChanges();
fixture.whenStable().then(() => {
const claimButton = fixture.debugElement.query(By.css('[data-automation-id="header-claim-button"]'));
const unclaimButton = fixture.debugElement.query(By.css('[data-automation-id="header-unclaim-button"]'));
expect(component.isTaskClaimable()).toBeFalsy();
expect(component.isTaskClaimedByCandidateMember()).toBeFalsy();
expect(unclaimButton).toBeNull();
expect(claimButton).toBeNull();
});
}));
});
it('should display the requeue button if task is claimed by the current logged-in user', async(() => {
component.taskDetails = new TaskDetailsModel(claimedTaskDetailsMock);
component.refreshData();
fixture.detectChanges();
fixture.whenStable().then(() => {
const unclaimButton = fixture.debugElement.query(By.css('[data-automation-id="header-unclaim-button"]'));
expect(component.isTaskClaimedByCandidateMember()).toBeTruthy();
expect(unclaimButton.nativeElement.innerText).toBe('ADF_TASK_LIST.DETAILS.BUTTON.UNCLAIM');
});
}));
it('should not display the requeue button to logged in user if task is claimed by other candidate member', async(() => {
component.taskDetails = new TaskDetailsModel(claimedByGroupMemberMock);
component.refreshData();
fixture.detectChanges();
fixture.whenStable().then(() => {
const unclaimButton = fixture.debugElement.query(By.css('[data-automation-id="header-unclaim-button"]'));
expect(component.isTaskClaimedByCandidateMember()).toBeFalsy();
expect(unclaimButton).toBeNull();
});
}));
it('should display the claim button if the task is claimable by candidates members', async(() => {
component.taskDetails = new TaskDetailsModel(claimableTaskDetailsMock);
component.refreshData();
fixture.detectChanges();
fixture.whenStable().then(() => {
const claimButton = fixture.debugElement.query(By.css('[data-automation-id="header-claim-button"]'));
expect(component.isTaskClaimable()).toBeTruthy();
expect(component.isTaskClaimedByCandidateMember()).toBeFalsy();
expect(claimButton.nativeElement.innerText).toBe('ADF_TASK_LIST.DETAILS.BUTTON.CLAIM');
});
}));
it('should not display the requeue button if the task is completed', async(() => {
component.taskDetails = new TaskDetailsModel(completedTaskDetailsMock);
component.refreshData();
fixture.detectChanges();
fixture.whenStable().then(() => {
const claimButton = fixture.debugElement.query(By.css('[data-automation-id="header-claim-button"]'));
const unclaimButton = fixture.debugElement.query(By.css('[data-automation-id="header-unclaim-button"]'));
expect(claimButton).toBeNull();
expect(unclaimButton).toBeNull();
});
}));
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();
fixture.detectChanges();
fixture.whenStable().then(() => {
const unclaimButton = fixture.debugElement.query(By.css('[data-automation-id="header-unclaim-button"]'));
unclaimButton.triggerEventHandler('click', {});
expect(service.unclaimTask).toHaveBeenCalledWith('91');
});
}));
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();
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();
});
}));
it('should display due date', async(() => {
component.taskDetails.dueDate = new Date('2016-11-03');
component.refreshData();
fixture.detectChanges();
fixture.whenStable().then(() => {
const valueEl = fixture.debugElement.query(By.css('[data-automation-id="header-dueDate"] .adf-property-value'));
expect(valueEl.nativeElement.innerText.trim()).toBe('Nov 3, 2016');
});
}));
it('should display placeholder if no due date', async(() => {
component.taskDetails.dueDate = null;
component.refreshData();
fixture.detectChanges();
fixture.whenStable().then(() => {
const valueEl = fixture.debugElement.query(By.css('[data-automation-id="header-dueDate"] .adf-property-value'));
expect(valueEl.nativeElement.innerText.trim()).toBe('ADF_TASK_LIST.PROPERTIES.DUE_DATE_DEFAULT');
});
}));
it('should display form name', async(() => {
component.formName = 'test form';
component.refreshData();
fixture.detectChanges();
fixture.whenStable().then(() => {
const valueEl = fixture.debugElement.query(By.css('[data-automation-id="header-formName"] .adf-textitem-clickable-value'));
expect(valueEl.nativeElement.innerText).toBe('test form');
});
}));
it('should set clickable to false if the task has already completed', async(() => {
component.taskDetails.endDate = new Date('05/05/2002');
component.formName = 'test form';
component.refreshData();
fixture.detectChanges();
fixture.whenStable().then(() => {
const clickableForm = fixture.debugElement.query(By.css('[data-automation-id="header-formName"] .adf-textitem-clickable-value'));
expect(clickableForm).toBeNull();
const readOnlyForm = fixture.debugElement.query(By.css('[data-automation-id="header-formName"] .adf-textitem-ellipsis'));
expect(readOnlyForm.nativeElement.innerText).toBe('test form');
});
}));
it('should display the default parent value if is undefined', async(() => {
component.taskDetails.processInstanceId = null;
component.refreshData();
fixture.detectChanges();
fixture.whenStable().then(() => {
const valueEl = fixture.debugElement.query(By.css('[data-automation-id="header-parentName"] .adf-property-value'));
expect(valueEl.nativeElement.innerText.trim()).toEqual('ADF_TASK_LIST.PROPERTIES.PARENT_NAME_DEFAULT');
});
}));
it('should display the Parent name value', async(() => {
component.taskDetails.processInstanceId = '1';
component.taskDetails.processDefinitionName = 'Parent Name';
component.refreshData();
fixture.detectChanges();
fixture.whenStable().then(() => {
const valueEl = fixture.debugElement.query(By.css('[data-automation-id="header-parentName"] .adf-property-value'));
expect(valueEl.nativeElement.innerText.trim()).toEqual('Parent Name');
});
}));
it('should not display form name if no form name provided', async(() => {
component.refreshData();
fixture.detectChanges();
fixture.whenStable().then(() => {
const valueEl = fixture.debugElement.query(By.css('[data-automation-id="header-formName"] .adf-property-value'));
expect(valueEl.nativeElement.innerText).toBe('ADF_TASK_LIST.PROPERTIES.FORM_NAME_DEFAULT');
});
}));
describe('Config Filtering', () => {
it('should show only the properties from the configuration file', async(() => {
spyOn(appConfigService, 'get').and.returnValue(['assignee', 'status']);
component.taskDetails.processInstanceId = '1';
component.taskDetails.processDefinitionName = 'Parent Name';
component.refreshData();
fixture.detectChanges();
const propertyList = fixture.debugElement.queryAll(By.css('.adf-property-list .adf-property'));
fixture.whenStable().then(() => {
expect(propertyList).toBeDefined();
expect(propertyList).not.toBeNull();
expect(propertyList.length).toBe(2);
expect(propertyList[0].nativeElement.textContent).toContain('ADF_TASK_LIST.PROPERTIES.ASSIGNEE');
expect(propertyList[1].nativeElement.textContent).toContain('ADF_TASK_LIST.PROPERTIES.STATUS');
});
}));
it('should show all the default properties if there is no configuration', async(() => {
spyOn(appConfigService, 'get').and.returnValue(null);
component.taskDetails.processInstanceId = '1';
component.taskDetails.processDefinitionName = 'Parent Name';
component.refreshData();
fixture.detectChanges();
fixture.whenStable().then(() => {
const propertyList = fixture.debugElement.queryAll(By.css('.adf-property-list .adf-property'));
expect(propertyList).toBeDefined();
expect(propertyList).not.toBeNull();
expect(propertyList.length).toBe(component.properties.length);
expect(propertyList[0].nativeElement.textContent).toContain('ADF_TASK_LIST.PROPERTIES.ASSIGNEE');
expect(propertyList[1].nativeElement.textContent).toContain('ADF_TASK_LIST.PROPERTIES.STATUS');
});
}));
});
});

View File

@@ -0,0 +1,321 @@
/*!
* @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, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core';
import {
BpmUserService,
CardViewDateItemModel,
CardViewItem,
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({
selector: 'adf-task-header',
templateUrl: './task-header.component.html',
styleUrls: ['./task-header.component.scss']
})
export class TaskHeaderComponent implements OnChanges, OnInit {
/** The name of the form. */
@Input()
formName: string = null;
/** (required) Details related to the task. */
@Input()
taskDetails: TaskDetailsModel;
/** Emitted when the task is claimed. */
@Output()
claim: EventEmitter<any> = new EventEmitter<any>();
/** Emitted when the task is unclaimed (ie, requeued). */
@Output()
unclaim: EventEmitter<any> = new EventEmitter<any>();
private currentUserId: number;
properties: CardViewItem [];
inEdit: boolean = false;
displayDateClearAction = false;
dateFormat: string;
dateLocale: string;
constructor(private activitiTaskService: TaskListService,
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');
}
ngOnInit() {
this.loadCurrentBpmUserId();
}
ngOnChanges() {
this.refreshData();
}
private initDefaultProperties(parentInfoMap) {
return [
new CardViewTextItemModel(
{
label: 'ADF_TASK_LIST.PROPERTIES.ASSIGNEE',
value: this.taskDetails.getFullName(),
key: 'assignee',
default: this.translationService.instant('ADF_TASK_LIST.PROPERTIES.ASSIGNEE_DEFAULT'),
clickable: !this.isCompleted(),
icon: 'create'
}
),
new CardViewTextItemModel(
{
label: 'ADF_TASK_LIST.PROPERTIES.STATUS',
value: this.getTaskStatus(),
key: 'status'
}
),
new CardViewTextItemModel(
{
label: 'ADF_TASK_LIST.PROPERTIES.PRIORITY',
value: this.taskDetails.priority,
key: 'priority',
editable: true
}
),
new CardViewDateItemModel(
{
label: 'ADF_TASK_LIST.PROPERTIES.DUE_DATE',
value: this.taskDetails.dueDate,
key: 'dueDate',
default: this.translationService.instant('ADF_TASK_LIST.PROPERTIES.DUE_DATE_DEFAULT'),
editable: true,
format: this.dateFormat,
locale: this.dateLocale
}
),
new CardViewTextItemModel(
{
label: 'ADF_TASK_LIST.PROPERTIES.CATEGORY',
value: this.taskDetails.category,
key: 'category',
default: this.translationService.instant('ADF_TASK_LIST.PROPERTIES.CATEGORY_DEFAULT')
}
),
new CardViewMapItemModel(
{
label: 'ADF_TASK_LIST.PROPERTIES.PARENT_NAME',
value: parentInfoMap,
key: 'parentName',
default: this.translationService.instant('ADF_TASK_LIST.PROPERTIES.PARENT_NAME_DEFAULT'),
clickable: true
}
),
new CardViewDateItemModel(
{
label: 'ADF_TASK_LIST.PROPERTIES.CREATED',
value: this.taskDetails.created,
key: 'created',
format: this.dateFormat,
locale: this.dateLocale
}
),
new CardViewTextItemModel(
{
label: 'ADF_TASK_LIST.PROPERTIES.DURATION',
value: this.getTaskDuration(),
key: 'duration'
}
),
new CardViewTextItemModel(
{
label: 'ADF_TASK_LIST.PROPERTIES.PARENT_TASK_ID',
value: this.taskDetails.parentTaskId,
key: 'parentTaskId'
}
),
new CardViewDateItemModel(
{
label: 'ADF_TASK_LIST.PROPERTIES.END_DATE',
value: this.taskDetails.endDate,
key: 'endDate',
format: this.dateFormat,
locale: this.dateLocale
}
),
new CardViewTextItemModel(
{
label: 'ADF_TASK_LIST.PROPERTIES.ID',
value: this.taskDetails.id,
key: 'id'
}
),
new CardViewTextItemModel(
{
label: 'ADF_TASK_LIST.PROPERTIES.DESCRIPTION',
value: this.taskDetails.description,
key: 'description',
default: this.translationService.instant('ADF_TASK_LIST.PROPERTIES.DESCRIPTION_DEFAULT'),
multiline: true,
editable: true,
validators: [new TaskDescriptionValidator()]
}
),
new CardViewTextItemModel(
{
label: 'ADF_TASK_LIST.PROPERTIES.FORM_NAME',
value: this.formName,
key: 'formName',
default: this.translationService.instant('ADF_TASK_LIST.PROPERTIES.FORM_NAME_DEFAULT'),
clickable: this.isFormClickable(),
icon: 'create'
}
)
];
}
/**
* Refresh the card data
*/
refreshData() {
if (this.taskDetails) {
const parentInfoMap = this.getParentInfo();
const defaultProperties = this.initDefaultProperties(parentInfoMap);
const filteredProperties: string[] = this.appConfig.get('adf-task-header.presets.properties');
this.properties = defaultProperties.filter((cardItem) => this.isValidSelection(filteredProperties, cardItem));
}
}
private isValidSelection(filteredProperties: string[], cardItem: CardViewBaseItemModel): boolean {
return filteredProperties ? filteredProperties.indexOf(cardItem.key) >= 0 : true;
}
/**
* Loads current bpm userId
*/
private loadCurrentBpmUserId(): void {
this.bpmUserService.getCurrentUserInfo().subscribe((res) => {
this.currentUserId = res ? +res.id : null;
});
}
/**
* Return the process parent information
*/
getParentInfo() {
if (this.taskDetails.processInstanceId && this.taskDetails.processDefinitionName) {
return new Map([[this.taskDetails.processInstanceId, this.taskDetails.processDefinitionName]]);
}
}
/**
* Does the task have an assignee
*/
public hasAssignee(): boolean {
return !!this.taskDetails.assignee ? true : false;
}
/**
* Returns true if the task is assigned to logged in user
*/
public isAssignedTo(userId): boolean {
return this.hasAssignee() ? this.taskDetails.assignee.id === userId : false;
}
/**
* Return true if the task assigned
*/
public isAssignedToCurrentUser(): boolean {
return this.hasAssignee() && this.isAssignedTo(this.currentUserId);
}
/**
* Return true if the user is a candidate member
*/
isCandidateMember() {
return this.taskDetails.managerOfCandidateGroup || this.taskDetails.memberOfCandidateGroup || this.taskDetails.memberOfCandidateUsers;
}
/**
* Return true if the task claimable
*/
public isTaskClaimable(): boolean {
return !this.hasAssignee() && this.isCandidateMember();
}
/**
* Return true if the task claimed by candidate member.
*/
public isTaskClaimedByCandidateMember(): boolean {
return this.isCandidateMember() && this.isAssignedToCurrentUser() && !this.isCompleted();
}
/**
* Returns task's status
*/
getTaskStatus(): string {
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);
});
}
/**
* Unclaim task
*
* @param taskId
*/
unclaimTask(taskId: string) {
this.activitiTaskService.unclaimTask(taskId).subscribe(() => {
this.logService.info('Task unclaimed');
this.unclaim.emit(taskId);
});
}
/**
* Returns true if the task is completed
*/
isCompleted(): boolean {
return this.taskDetails && !!this.taskDetails.endDate;
}
isFormClickable(): boolean {
return !!this.formName && !this.isCompleted();
}
getTaskDuration(): string {
return this.taskDetails.duration ? `${this.taskDetails.duration} ms` : '';
}
}

View File

@@ -0,0 +1,4 @@
.adf-task-list-loading-margin {
margin-left: calc((100% - 100px) / 2);
margin-right: calc((100% - 100px) / 2);
}

View File

@@ -0,0 +1,38 @@
<div *ngIf="!requestNode">{{ 'ADF_TASK_LIST.FILTERS.MESSAGES.NONE' | translate }}</div>
<ng-container *ngIf="requestNode">
<adf-datatable
[data]="data"
[rows]="rows"
[columns]="columns"
[sorting]="sorting"
[loading]="isLoading"
[multiselect]="multiselect"
[selectionMode]="selectionMode"
(row-select)="onRowSelect($event)"
(row-unselect)="onRowUnselect($event)"
(rowClick)="onRowClick($event)"
(row-keyup)="onRowKeyUp($event)">
<adf-loading-content-template>
<ng-template>
<!--Add your custom loading template here-->
<mat-progress-spinner
*ngIf="!customLoadingContent"
class="adf-task-list-loading-margin"
[color]="'primary'"
[mode]="'indeterminate'">
</mat-progress-spinner>
<ng-content select="adf-custom-loading-content-template"></ng-content>
</ng-template>
</adf-loading-content-template>
<adf-no-content-template>
<ng-template>
<adf-empty-content *ngIf="!customEmptyContent"
icon="assignment"
[title]="'ADF_TASK_LIST.LIST.MESSAGES.TITLE' | translate"
[subtitle]="'ADF_TASK_LIST.LIST.MESSAGES.SUBTITLE' | translate">
</adf-empty-content>
<ng-content select="adf-custom-empty-content-template"></ng-content>
</ng-template>
</adf-no-content-template>
</adf-datatable>
</ng-container>

View File

@@ -0,0 +1,614 @@
/*!
* @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, SimpleChange, ViewChild } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { AppConfigService, setupTestBed, CoreModule, DataTableModule } from '@alfresco/adf-core';
import { DataRowEvent, ObjectDataRow } from '@alfresco/adf-core';
import { TaskListService } from '../services/tasklist.service';
import { TaskListComponent } from './task-list.component';
import { ProcessTestingModule } from '../../testing/process.testing.module';
import { fakeGlobalTask, fakeCustomSchema, fakeEmptyTask } from '../../mock';
import { TranslateService } from '@ngx-translate/core';
import { of } from 'rxjs';
import { TaskListModule } from '../task-list.module';
declare let jasmine: any;
describe('TaskListComponent', () => {
let component: TaskListComponent;
let fixture: ComponentFixture<TaskListComponent>;
let appConfig: AppConfigService;
setupTestBed({
imports: [
ProcessTestingModule
]
});
beforeEach(() => {
appConfig = TestBed.get(AppConfigService);
appConfig.config.bpmHost = 'http://localhost:9876/bpm';
fixture = TestBed.createComponent(TaskListComponent);
component = fixture.componentInstance;
appConfig.config = Object.assign(appConfig.config, {
'adf-task-list': {
'presets': {
'fakeCustomSchema': [
{
'key': 'fakeName',
'type': 'text',
'title': 'ADF_TASK_LIST.PROPERTIES.FAKE',
'sortable': true
},
{
'key': 'fakeTaskName',
'type': 'text',
'title': 'ADF_TASK_LIST.PROPERTIES.TASK_FAKE',
'sortable': true
}
]
}
}
});
});
beforeEach(() => {
jasmine.Ajax.install();
});
afterEach(() => {
jasmine.Ajax.uninstall();
fixture.destroy();
});
it('should use the default schemaColumn as default', () => {
component.ngAfterContentInit();
expect(component.columns).toBeDefined();
expect(component.columns.length).toEqual(3);
});
it('should use the custom schemaColumn from app.config.json', () => {
component.presetColumn = 'fakeCustomSchema';
component.ngAfterContentInit();
fixture.detectChanges();
expect(component.columns).toEqual(fakeCustomSchema);
});
it('should fetch custom schemaColumn when the input presetColumn is defined', () => {
component.presetColumn = 'fakeCustomSchema';
fixture.detectChanges();
expect(component.columns).toBeDefined();
expect(component.columns.length).toEqual(2);
});
it('should return an empty task list when no input parameters are passed', () => {
component.ngAfterContentInit();
expect(component.rows).toBeDefined();
expect(component.isListEmpty()).toBeTruthy();
});
it('should return the filtered task list when the input parameters are passed', (done) => {
const state = new SimpleChange(null, 'open', true);
const processDefinitionKey = new SimpleChange(null, null, true);
const assignment = new SimpleChange(null, 'fake-assignee', true);
component.success.subscribe((res) => {
expect(res).toBeDefined();
expect(component.rows).toBeDefined();
expect(component.isListEmpty()).not.toBeTruthy();
expect(component.rows.length).toEqual(2);
expect(component.rows[0]['name']).toEqual('nameFake1');
expect(component.rows[0]['description']).toEqual('descriptionFake1');
expect(component.rows[0]['category']).toEqual('categoryFake1');
expect(component.rows[0]['assignee'].id).toEqual(2);
expect(component.rows[0]['assignee'].firstName).toEqual('firstNameFake1');
expect(component.rows[0]['assignee'].lastName).toEqual('lastNameFake1');
expect(component.rows[0][('assignee')].email).toEqual('emailFake1');
expect(component.rows[0]['created'].toISOString()).toEqual('2017-03-01T12:25:17.189Z');
expect(component.rows[0]['dueDate'].toISOString()).toEqual('2017-04-02T12:25:17.189Z');
expect(component.rows[0]['endDate'].toISOString()).toEqual('2017-05-03T12:25:31.129Z');
expect(component.rows[0]['duration']).toEqual(13940);
expect(component.rows[0]['priority']).toEqual(50);
expect(component.rows[0]['parentTaskId']).toEqual(1);
expect(component.rows[0]['parentTaskName']).toEqual('parentTaskNameFake');
expect(component.rows[0]['processInstanceId']).toEqual(2511);
expect(component.rows[0]['processInstanceName']).toEqual('processInstanceNameFake');
expect(component.rows[0]['processDefinitionId']).toEqual('myprocess:1:4');
expect(component.rows[0]['processDefinitionName']).toEqual('processDefinitionNameFake');
expect(component.rows[0]['processDefinitionDescription']).toEqual('processDefinitionDescriptionFake');
expect(component.rows[0]['processDefinitionKey']).toEqual('myprocess');
expect(component.rows[0]['processDefinitionCategory']).toEqual('http://www.activiti.org/processdef');
done();
});
component.ngAfterContentInit();
component.ngOnChanges({ 'state': state, 'processDefinitionKey': processDefinitionKey, 'assignment': assignment });
fixture.detectChanges();
jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200,
contentType: 'application/json',
responseText: JSON.stringify(fakeGlobalTask)
});
});
it('should return the filtered task list by processDefinitionKey', (done) => {
const state = new SimpleChange(null, 'open', true);
/* cspell:disable-next-line */
const processDefinitionKey = new SimpleChange(null, 'fakeprocess', true);
const assignment = new SimpleChange(null, 'fake-assignee', true);
component.success.subscribe((res) => {
expect(res).toBeDefined();
expect(component.rows).toBeDefined();
expect(component.isListEmpty()).not.toBeTruthy();
expect(component.rows.length).toEqual(2);
expect(component.rows[0]['name']).toEqual('nameFake1');
done();
});
component.ngAfterContentInit();
component.ngOnChanges({ 'state': state, 'processDefinitionKey': processDefinitionKey, 'assignment': assignment });
fixture.detectChanges();
jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200,
contentType: 'application/json',
responseText: JSON.stringify(fakeGlobalTask)
});
});
it('should return the filtered task list by processInstanceId', (done) => {
const state = new SimpleChange(null, 'open', true);
const processInstanceId = new SimpleChange(null, 'fakeprocessId', true);
const assignment = new SimpleChange(null, 'fake-assignee', true);
component.success.subscribe((res) => {
expect(res).toBeDefined();
expect(component.rows).toBeDefined();
expect(component.isListEmpty()).not.toBeTruthy();
expect(component.rows.length).toEqual(2);
expect(component.rows[0]['name']).toEqual('nameFake1');
expect(component.rows[0]['processInstanceId']).toEqual(2511);
done();
});
component.ngAfterContentInit();
component.ngOnChanges({ 'state': state, 'processInstanceId': processInstanceId, 'assignment': assignment });
fixture.detectChanges();
jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200,
contentType: 'application/json',
responseText: JSON.stringify(fakeGlobalTask)
});
});
it('should return the filtered task list by processDefinitionId', (done) => {
const state = new SimpleChange(null, 'open', true);
const processDefinitionId = new SimpleChange(null, 'fakeprocessDefinitionId', true);
const assignment = new SimpleChange(null, 'fake-assignee', true);
component.success.subscribe((res) => {
expect(res).toBeDefined();
expect(component.rows).toBeDefined();
expect(component.isListEmpty()).not.toBeTruthy();
expect(component.rows.length).toEqual(2);
expect(component.rows[0]['name']).toEqual('nameFake1');
expect(component.rows[0]['processDefinitionId']).toEqual('myprocess:1:4');
done();
});
component.ngAfterContentInit();
component.ngOnChanges({ 'state': state, 'processDefinitionId': processDefinitionId, 'assignment': assignment });
fixture.detectChanges();
jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200,
contentType: 'application/json',
responseText: JSON.stringify(fakeGlobalTask)
});
});
it('should return the filtered task list by created date', (done) => {
const state = new SimpleChange(null, 'open', true);
const afterDate = new SimpleChange(null, '28-02-2017', true);
component.success.subscribe((res) => {
expect(res).toBeDefined();
expect(component.rows).toBeDefined();
expect(component.isListEmpty()).not.toBeTruthy();
expect(component.rows.length).toEqual(2);
expect(component.rows[0]['name']).toEqual('nameFake1');
expect(component.rows[0]['processDefinitionId']).toEqual('myprocess:1:4');
done();
});
component.ngAfterContentInit();
component.ngOnChanges({ 'state': state, 'afterDate': afterDate });
fixture.detectChanges();
jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200,
contentType: 'application/json',
responseText: JSON.stringify(fakeGlobalTask)
});
});
it('should return the filtered task list for all state', (done) => {
const state = new SimpleChange(null, 'all', true);
/* cspell:disable-next-line */
const processInstanceId = new SimpleChange(null, 'fakeprocessId', true);
component.success.subscribe((res) => {
expect(res).toBeDefined();
expect(component.rows).toBeDefined();
expect(component.isListEmpty()).not.toBeTruthy();
expect(component.rows.length).toEqual(2);
expect(component.rows[0]['name']).toEqual('nameFake1');
expect(component.rows[0]['processInstanceId']).toEqual(2511);
expect(component.rows[0]['endDate']).toBeDefined();
expect(component.rows[1]['name']).toEqual('No name');
expect(component.rows[1]['endDate']).toBeUndefined();
done();
});
component.ngAfterContentInit();
component.ngOnChanges({ 'state': state, 'processInstanceId': processInstanceId });
fixture.detectChanges();
jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200,
contentType: 'application/json',
responseText: JSON.stringify(fakeGlobalTask)
});
});
it('should return a currentId null when the taskList is empty', () => {
component.selectTask(null);
expect(component.getCurrentId()).toBeNull();
});
it('should return selected id for the selected task', () => {
component.rows = [
{ id: '999', name: 'Fake-name' },
{ id: '888', name: 'Fake-name-888' }
];
component.selectTask('888');
expect(component.rows).toBeDefined();
expect(component.currentInstanceId).toEqual('888');
});
it('should reload tasks when reload() is called', (done) => {
component.state = 'open';
component.assignment = 'fake-assignee';
component.ngAfterContentInit();
component.success.subscribe((res) => {
expect(res).toBeDefined();
expect(component.rows).toBeDefined();
expect(component.isListEmpty()).not.toBeTruthy();
expect(component.rows.length).toEqual(2);
expect(component.rows[0]['name']).toEqual('nameFake1');
done();
});
fixture.detectChanges();
component.reload();
jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200,
contentType: 'application/json',
responseText: JSON.stringify(fakeGlobalTask)
});
});
it('should emit row click event', (done) => {
const row = new ObjectDataRow({
id: '999'
});
const rowEvent = new DataRowEvent(row, null);
component.rowClick.subscribe((taskId) => {
expect(taskId).toEqual('999');
expect(component.getCurrentId()).toEqual('999');
done();
});
component.onRowClick(rowEvent);
});
describe('component changes', () => {
beforeEach(() => {
component.rows = fakeGlobalTask.data;
fixture.detectChanges();
});
it('should NOT reload the tasks if the loadingTaskId is the same of the current task', () => {
spyOn(component, 'reload').and.stub();
component.currentInstanceId = '999';
component.rows = [{ id: '999', name: 'Fake-name' }];
const landingTaskId = '999';
const change = new SimpleChange(null, landingTaskId, true);
component.ngOnChanges({'landingTaskId': change});
expect(component.reload).not.toHaveBeenCalled();
expect(component.rows.length).toEqual(1);
});
it('should reload the tasks if the loadingTaskId is different from the current task', (done) => {
component.currentInstanceId = '999';
component.rows = [{ id: '999', name: 'Fake-name' }];
const landingTaskId = '888';
const change = new SimpleChange(null, landingTaskId, true);
component.success.subscribe((res) => {
expect(res).toBeDefined();
expect(component.rows).toBeDefined();
expect(component.rows.length).toEqual(2);
done();
});
component.ngOnChanges({'landingTaskId': change});
jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200,
contentType: 'application/json',
responseText: JSON.stringify(fakeGlobalTask)
});
});
it('should NOT reload the task list when no parameters changed', () => {
component.rows = null;
component.ngOnChanges({});
fixture.detectChanges();
expect(component.isListEmpty()).toBeTruthy();
});
it('should reload the list when the appId parameter changes', (done) => {
const appId = '1';
const change = new SimpleChange(null, appId, true);
component.success.subscribe((res) => {
expect(res).toBeDefined();
expect(component.rows).toBeDefined();
expect(component.isListEmpty()).not.toBeTruthy();
expect(component.rows.length).toEqual(2);
expect(component.rows[1]['name']).toEqual('No name');
done();
});
component.ngOnChanges({ 'appId': change });
jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200,
contentType: 'application/json',
responseText: JSON.stringify(fakeGlobalTask)
});
});
it('should reload the list when the processDefinitionKey parameter changes', (done) => {
const processDefinitionKey = 'fakeprocess';
const change = new SimpleChange(null, processDefinitionKey, true);
component.success.subscribe((res) => {
expect(res).toBeDefined();
expect(component.rows).toBeDefined();
expect(component.isListEmpty()).not.toBeTruthy();
expect(component.rows.length).toEqual(2);
expect(component.rows[1]['name']).toEqual('No name');
done();
});
component.ngOnChanges({ 'processDefinitionKey': change });
jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200,
contentType: 'application/json',
responseText: JSON.stringify(fakeGlobalTask)
});
});
it('should reload the list when the state parameter changes', (done) => {
const state = 'open';
const change = new SimpleChange(null, state, true);
component.success.subscribe((res) => {
expect(res).toBeDefined();
expect(component.rows).toBeDefined();
expect(component.isListEmpty()).not.toBeTruthy();
expect(component.rows.length).toEqual(2);
expect(component.rows[1]['name']).toEqual('No name');
done();
});
component.ngOnChanges({ 'state': change });
jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200,
contentType: 'application/json',
responseText: JSON.stringify(fakeGlobalTask)
});
});
it('should reload the list when the sort parameter changes', (done) => {
const sort = 'desc';
const change = new SimpleChange(null, sort, true);
component.success.subscribe((res) => {
expect(res).toBeDefined();
expect(component.rows).toBeDefined();
expect(component.isListEmpty()).not.toBeTruthy();
expect(component.rows.length).toEqual(2);
expect(component.rows[1]['name']).toEqual('No name');
done();
});
component.ngOnChanges({ 'sort': change });
jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200,
contentType: 'application/json',
responseText: JSON.stringify(fakeGlobalTask)
});
});
it('should reload the process list when the name parameter changes', (done) => {
const name = 'FakeTaskName';
const change = new SimpleChange(null, name, true);
component.success.subscribe((res) => {
expect(res).toBeDefined();
expect(component.rows).toBeDefined();
expect(component.isListEmpty()).not.toBeTruthy();
expect(component.rows.length).toEqual(2);
expect(component.rows[1]['name']).toEqual('No name');
done();
});
component.ngOnChanges({ 'name': change });
jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200,
contentType: 'application/json',
responseText: JSON.stringify(fakeGlobalTask)
});
});
it('should reload the list when the assignment parameter changes', (done) => {
const assignment = 'assignee';
const change = new SimpleChange(null, assignment, true);
component.success.subscribe((res) => {
expect(res).toBeDefined();
expect(component.rows).toBeDefined();
expect(component.isListEmpty()).not.toBeTruthy();
expect(component.rows.length).toEqual(2);
expect(component.rows[1]['name']).toEqual('No name');
done();
});
component.ngOnChanges({ 'assignment': change });
jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200,
contentType: 'application/json',
responseText: JSON.stringify(fakeGlobalTask)
});
});
});
});
@Component({
template: `
<adf-tasklist #taskList>
<data-columns>
<data-column key="name" title="ADF_TASK_LIST.PROPERTIES.NAME" class="full-width name-column"></data-column>
<data-column key="created" title="ADF_TASK_LIST.PROPERTIES.CREATED" class="hidden"></data-column>
<data-column key="startedBy" title="ADF_TASK_LIST.PROPERTIES.CREATED" class="desktop-only dw-dt-col-3 ellipsis-cell">
<ng-template let-entry="$implicit">
<div>{{entry.row.obj.startedBy | fullName}}</div>
</ng-template>
</data-column>
</data-columns>
</adf-tasklist>`
})
class CustomTaskListComponent {
@ViewChild(TaskListComponent)
taskList: TaskListComponent;
}
describe('CustomTaskListComponent', () => {
let fixture: ComponentFixture<CustomTaskListComponent>;
let component: CustomTaskListComponent;
setupTestBed({
imports: [CoreModule.forRoot()],
declarations: [TaskListComponent, CustomTaskListComponent],
providers: [TaskListService]
});
beforeEach(() => {
fixture = TestBed.createComponent(CustomTaskListComponent);
fixture.detectChanges();
component = fixture.componentInstance;
});
afterEach(() => {
fixture.destroy();
});
it('should create instance of CustomTaskListComponent', () => {
expect(component instanceof CustomTaskListComponent).toBe(true, 'should create CustomTaskListComponent');
});
it('should fetch custom schemaColumn from html', () => {
fixture.detectChanges();
expect(component.taskList.columnList).toBeDefined();
expect(component.taskList.columns[0]['title']).toEqual('ADF_TASK_LIST.PROPERTIES.NAME');
expect(component.taskList.columns[1]['title']).toEqual('ADF_TASK_LIST.PROPERTIES.CREATED');
expect(component.taskList.columns.length).toEqual(3);
});
});
@Component({
template: `
<adf-tasklist [appId]="1">
<adf-custom-empty-content-template>
<p id="custom-id">CUSTOM EMPTY</p>
</adf-custom-empty-content-template>
</adf-tasklist>
`
})
class EmptyTemplateComponent {
}
describe('Task List: Custom EmptyTemplateComponent', () => {
let fixture: ComponentFixture<EmptyTemplateComponent>;
let translateService: TranslateService;
let taskListService: TaskListService;
setupTestBed({
imports: [ProcessTestingModule, TaskListModule, DataTableModule],
declarations: [EmptyTemplateComponent]
});
beforeEach(() => {
translateService = TestBed.get(TranslateService);
taskListService = TestBed.get(TaskListService);
spyOn(translateService, 'get').and.callFake((key) => {
return of(key);
});
spyOn(taskListService, 'findTasksByState').and.returnValue(of(fakeEmptyTask));
fixture = TestBed.createComponent(EmptyTemplateComponent);
fixture.detectChanges();
});
afterEach(() => {
fixture.destroy();
});
it('should render the custom template', (done) => {
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(fixture.debugElement.query(By.css('#custom-id'))).not.toBeNull();
expect(fixture.debugElement.query(By.css('.adf-empty-content'))).toBeNull();
done();
});
});
});

View File

@@ -0,0 +1,399 @@
/*!
* @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 { DataRowEvent, DataTableAdapter, DataTableSchema, CustomEmptyContentTemplateDirective, CustomLoadingContentTemplateDirective } from '@alfresco/adf-core';
import {
AppConfigService, PaginationComponent, PaginatedComponent,
UserPreferencesService, UserPreferenceValues, PaginationModel } from '@alfresco/adf-core';
import {
AfterContentInit, Component, ContentChild, EventEmitter,
Input, OnChanges, Output, SimpleChanges, OnDestroy, OnInit } from '@angular/core';
import { Observable, BehaviorSubject, Subject } from 'rxjs';
import { TaskQueryRequestRepresentationModel } from '../models/filter.model';
import { TaskListModel } from '../models/task-list.model';
import { taskPresetsDefaultModel } from '../models/task-preset.model';
import { TaskListService } from './../services/tasklist.service';
import moment from 'moment-es6';
import { takeUntil } from 'rxjs/operators';
@Component({
selector: 'adf-tasklist',
templateUrl: './task-list.component.html',
styleUrls: ['./task-list.component.css']
})
export class TaskListComponent extends DataTableSchema implements OnChanges, AfterContentInit, PaginatedComponent, OnDestroy, OnInit {
static PRESET_KEY = 'adf-task-list.presets';
@ContentChild(CustomEmptyContentTemplateDirective)
customEmptyContent: CustomEmptyContentTemplateDirective;
@ContentChild(CustomLoadingContentTemplateDirective)
customLoadingContent: CustomLoadingContentTemplateDirective;
requestNode: TaskQueryRequestRepresentationModel;
/** The id of the app. */
@Input()
appId: number;
/** The Instance Id of the process. */
@Input()
processInstanceId: string;
/** The Definition Id of the process. */
@Input()
processDefinitionId: string;
/** Current state of the process. Possible values are: `completed`, `active`. */
@Input()
state: string;
/** The assignment of the process. Possible values are: "assignee" (the current user
* is the assignee), "candidate" (the current user is a task candidate, "group_x" (the task
* is assigned to a group where the current user is a member,
* no value (the current user is involved).
*/
@Input()
assignment: string;
/** Define the sort order of the tasks. Possible values are : `created-desc`,
* `created-asc`, `due-desc`, `due-asc`
*/
@Input()
sort: string;
/** Name of the tasklist. */
@Input()
name: string;
/** Define which task id should be selected after reloading. If the task id doesn't
* exist or nothing is passed then the first task will be selected.
*/
@Input()
landingTaskId: string;
/**
* Data source object that represents the number and the type of the columns that
* you want to show.
*/
@Input()
data: DataTableAdapter;
/** Row selection mode. Can be none, `single` or `multiple`. For `multiple` mode,
* you can use Cmd (macOS) or Ctrl (Win) modifier key to toggle selection for
* multiple rows.
*/
@Input()
selectionMode: string = 'single'; // none|single|multiple
/** Toggles multiple row selection, renders checkboxes at the beginning of each row */
@Input()
multiselect: boolean = false;
/** Toggles default selection of the first row */
@Input()
selectFirstRow: boolean = true;
/** The id of a task */
@Input()
taskId: string;
/** Toggles inclusion of Process Instances */
@Input()
includeProcessInstance: boolean;
/** Starting point of the list within the full set of tasks. */
@Input()
start: number;
/** Emitted when a task in the list is clicked */
@Output()
rowClick: EventEmitter<string> = new EventEmitter<string>();
/** Emitted when rows are selected/unselected */
@Output()
rowsSelected: EventEmitter<any[]> = new EventEmitter<any[]>();
/** Emitted when the task list is loaded */
@Output()
success: EventEmitter<any> = new EventEmitter<any>();
/** Emitted when an error occurs. */
@Output()
error: EventEmitter<any> = new EventEmitter<any>();
currentInstanceId: string;
selectedInstances: any[];
pagination: BehaviorSubject<PaginationModel>;
/** The page number of the tasks to fetch. */
@Input()
page: number = 0;
/** The number of tasks to fetch. Default value: 25. */
@Input()
size: number = PaginationComponent.DEFAULT_PAGINATION.maxItems;
/** Filter the tasks. Display only tasks with `created_date` after `dueAfter`. */
@Input()
dueAfter: string;
/** Filter the tasks. Display only tasks with `created_date` before `dueBefore`. */
@Input()
dueBefore: string;
rows: any[] = [];
isLoading: boolean = true;
sorting: any[] = ['created', 'desc'];
/**
* Toggles custom data source mode.
* When enabled the component reloads data from it's current source instead of the server side.
* This allows generating and displaying custom data sets (i.e. filtered out content).
*
* @memberOf TaskListComponent
*/
hasCustomDataSource: boolean = false;
private onDestroy$ = new Subject<boolean>();
constructor(private taskListService: TaskListService,
appConfigService: AppConfigService,
private userPreferences: UserPreferencesService) {
super(appConfigService, TaskListComponent.PRESET_KEY, taskPresetsDefaultModel);
this.pagination = new BehaviorSubject<PaginationModel>(<PaginationModel> {
maxItems: this.size,
skipCount: 0,
totalItems: 0
});
}
ngAfterContentInit() {
this.createDatatableSchema();
if (this.data && this.data.getColumns().length === 0) {
this.data.setColumns(this.columns);
}
if (this.appId) {
this.reload();
}
}
ngOnInit() {
this.userPreferences
.select(UserPreferenceValues.PaginationSize)
.pipe(takeUntil(this.onDestroy$))
.subscribe(pageSize => this.size = pageSize);
}
ngOnDestroy() {
this.onDestroy$.next(true);
this.onDestroy$.complete();
}
setCustomDataSource(rows: any[]): void {
if (rows) {
this.rows = rows;
this.hasCustomDataSource = true;
}
}
ngOnChanges(changes: SimpleChanges) {
if (this.isPropertyChanged(changes)) {
if (this.isSortChanged(changes)) {
this.sorting = this.sort ? this.sort.split('-') : this.sorting;
}
this.reload();
}
}
private isSortChanged(changes: SimpleChanges): boolean {
const actualSort = changes['sort'];
return actualSort && actualSort.currentValue && actualSort.currentValue !== actualSort.previousValue;
}
private isPropertyChanged(changes: SimpleChanges): boolean {
let changed: boolean = true;
const landingTaskId = changes['landingTaskId'];
const page = changes['page'];
const size = changes['size'];
if (landingTaskId && landingTaskId.currentValue && this.isEqualToCurrentId(landingTaskId.currentValue)) {
changed = false;
} else if (page && page.currentValue !== page.previousValue) {
changed = true;
} else if (size && size.currentValue !== size.previousValue) {
changed = true;
}
return changed;
}
reload(): void {
if (!this.hasCustomDataSource) {
this.requestNode = this.createRequestNode();
this.load();
} else {
this.isLoading = false;
}
}
private load() {
this.isLoading = true;
this.loadTasksByState().subscribe(
(tasks) => {
this.rows = this.optimizeTaskDetails(tasks.data);
this.selectTask(this.landingTaskId);
this.success.emit(tasks);
this.isLoading = false;
this.pagination.next({
count: tasks.data.length,
maxItems: this.size,
skipCount: this.page * this.size,
totalItems: tasks.total
});
}, (error) => {
this.error.emit(error);
this.isLoading = false;
});
}
private loadTasksByState(): Observable<TaskListModel> {
return this.requestNode.state === 'all'
? this.taskListService.findAllTasksWithoutState(this.requestNode)
: this.taskListService.findTasksByState(this.requestNode);
}
/**
* Select the task given in input if present
*/
selectTask(taskIdSelected: string): void {
if (!this.isListEmpty()) {
let dataRow = null;
if (taskIdSelected) {
dataRow = this.rows.find((currentRow: any) => {
return currentRow['id'] === taskIdSelected;
});
}
if (!dataRow && this.selectFirstRow) {
dataRow = this.rows[0];
}
if (dataRow) {
dataRow.isSelected = true;
this.currentInstanceId = dataRow['id'];
}
} else {
this.currentInstanceId = null;
}
}
/**
* Return the current id
*/
getCurrentId(): string {
return this.currentInstanceId;
}
/**
* Check if the taskId is the same of the selected task
* @param taskId
*/
isEqualToCurrentId(taskId: string): boolean {
return this.currentInstanceId === taskId;
}
/**
* Check if the list is empty
*/
isListEmpty(): boolean {
return !this.rows || this.rows.length === 0;
}
onRowClick(item: DataRowEvent) {
this.currentInstanceId = item.value.getValue('id');
this.rowClick.emit(this.currentInstanceId);
}
onRowSelect(event: CustomEvent) {
this.selectedInstances = [...event.detail.selection];
this.rowsSelected.emit(this.selectedInstances);
}
onRowUnselect(event: CustomEvent) {
this.selectedInstances = [...event.detail.selection];
this.rowsSelected.emit(this.selectedInstances);
}
onRowKeyUp(event: CustomEvent) {
if (event.detail.keyboardEvent.key === 'Enter') {
event.preventDefault();
this.currentInstanceId = event.detail.row.getValue('id');
this.rowClick.emit(this.currentInstanceId);
}
}
/**
* Optimize name field
* @param instances
*/
private optimizeTaskDetails(instances: any[]): any[] {
instances = instances.map((task) => {
if (!task.name) {
task.name = 'No name';
}
return task;
});
return instances;
}
private createRequestNode() {
const requestNode = {
appDefinitionId: this.appId,
dueAfter: this.dueAfter ? moment(this.dueAfter).toDate() : null,
dueBefore: this.dueBefore ? moment(this.dueBefore).toDate() : null,
processInstanceId: this.processInstanceId,
processDefinitionId: this.processDefinitionId,
text: this.name,
assignment: this.assignment,
state: this.state,
sort: this.sort,
page: this.page,
size: this.size,
start: this.start,
taskId: this.taskId,
includeProcessInstance: this.includeProcessInstance
};
return new TaskQueryRequestRepresentationModel(requestNode);
}
updatePagination(params: PaginationModel) {
const needsReload = params.maxItems || params.skipCount;
this.size = params.maxItems;
this.page = this.currentPage(params.skipCount, params.maxItems);
if (needsReload) {
this.reload();
}
}
currentPage(skipCount: number, maxItems: number): number {
return (skipCount && maxItems) ? Math.floor(skipCount / maxItems) : 0;
}
}

View File

@@ -0,0 +1,27 @@
<mat-card class="adf-message-card">
<mat-card-content>
<div class="adf-no-form-message-container">
<div class="adf-no-form-message-list">
<div *ngIf="!isCompleted; else completedMessage" class="adf-no-form-message">
<span id="adf-no-form-message">{{'ADF_TASK_LIST.STANDALONE_TASK.NO_FORM_MESSAGE' | translate}}</span>
</div>
<ng-template #completedMessage>
<div id="adf-completed-form-message" class="adf-no-form-message">
<p>{{'ADF_TASK_LIST.STANDALONE_TASK.COMPLETE_TASK_MESSAGE' | translate : {taskName : taskName} }}</p>
</div>
<div class="adf-no-form-submessage">
{{'ADF_TASK_LIST.STANDALONE_TASK.COMPLETE_TASK_SUB_MESSAGE' | translate}}
</div>
</ng-template>
</div>
</div>
</mat-card-content>
<mat-card-actions class="adf-no-form-mat-card-actions">
<button mat-button *ngIf="hasAttachFormButton()" id="adf-no-form-attach-form-button" (click)="onShowAttachForm()">{{ 'ADF_TASK_LIST.START_TASK.FORM.LABEL.ATTACHFORM' | translate }}</button>
<div>
<button mat-button *ngIf="hasCancelButton()" id="adf-no-form-cancel-button" (click)="onCancelButtonClick()">{{ 'ADF_TASK_LIST.START_TASK.FORM.ACTION.CANCEL' | translate }}</button>
<button mat-button *ngIf="hasCompleteButton()" id="adf-no-form-complete-button" color="primary" (click)="onCompleteButtonClick()">{{ 'ADF_TASK_LIST.DETAILS.BUTTON.COMPLETE' | translate }}</button>
</div>
</mat-card-actions>
</mat-card>

View File

@@ -0,0 +1,54 @@
@mixin adf-task-standalone-component-theme($theme) {
$config: mat-typography-config();
$background: map-get($theme, background);
.adf {
&-message-card {
width: 60%;
box-sizing: border-box;
margin: 16px auto;
.mat-card-actions {
border-top: solid 1px mat-color($background, status-bar);
}
}
&-no-form-message-container {
height: 256px;
width: 100%;
display: table;
}
&-no-form-message-list {
display: table-cell;
vertical-align: middle;
text-align: center !important;
}
&-no-form-message {
padding-bottom: 10px;
font-size: mat-font-size($config, display-1);
line-height: 36px;
letter-spacing: -1.3px;
opacity: 0.54;
margin: auto;
width: fit-content !important;
}
&-no-form-submessage {
font-size: mat-font-size($config, subheading-2);
opacity: 0.54;
margin: auto;
width: fit-content !important;
}
&-no-form-mat-card-actions.mat-card-actions {
display: flex;
justify-content: space-between;
& .mat-button {
text-transform: uppercase;
border-radius: 5px;
}
& .mat-button-wrapper {
opacity: 0.54;
font-size: mat-font-size($config, button);
font-weight: bold;
}
}
}
}

View File

@@ -0,0 +1,136 @@
/*!
* @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 { TaskStandaloneComponent } from './task-standalone.component';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { setupTestBed } from '@alfresco/adf-core';
import { ProcessTestingModule } from '../../testing/process.testing.module';
describe('TaskStandaloneComponent', () => {
let component: TaskStandaloneComponent;
let fixture: ComponentFixture<TaskStandaloneComponent>;
let element: HTMLElement;
setupTestBed({
imports: [
ProcessTestingModule
]
});
beforeEach(() => {
fixture = TestBed.createComponent(TaskStandaloneComponent);
component = fixture.componentInstance;
element = fixture.nativeElement;
fixture.detectChanges();
});
afterEach(() => {
fixture.destroy();
});
it('should show Completed message if isCompleted is true', async(() => {
component.isCompleted = true;
fixture.detectChanges();
const completedFormElement = fixture.debugElement.nativeElement.querySelector('#adf-completed-form-message');
const completedFormSubElement = fixture.debugElement.nativeElement.querySelector('.adf-no-form-submessage');
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(element.querySelector('#adf-no-form-message')).toBeNull();
expect(completedFormElement).toBeDefined();
expect(completedFormElement.innerText.trim()).toBe('ADF_TASK_LIST.STANDALONE_TASK.COMPLETE_TASK_MESSAGE');
expect(completedFormSubElement).toBeDefined();
expect(completedFormSubElement.innerText).toBe('ADF_TASK_LIST.STANDALONE_TASK.COMPLETE_TASK_SUB_MESSAGE');
expect(element.querySelector('.adf-no-form-mat-card-actions')).toBeDefined();
});
}));
it('should show No form message if isCompleted is false', async(() => {
component.isCompleted = false;
fixture.detectChanges();
const noFormElement = fixture.debugElement.nativeElement.querySelector('#adf-no-form-message');
fixture.whenStable().then(() => {
expect(noFormElement).toBeDefined();
expect(noFormElement.innerText).toBe('ADF_TASK_LIST.STANDALONE_TASK.NO_FORM_MESSAGE');
expect(element.querySelector('#adf-completed-form-message')).toBeNull();
expect(element.querySelector('.adf-no-form-submessage')).toBeNull();
});
}));
it('should hide Cancel button by default', async(() => {
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(element.querySelector('#adf-no-form-cancel-button')).toBeNull();
});
}));
it('should emit cancel event if clicked on Cancel Button ', async(() => {
component.hideCancelButton = false;
component.isCompleted = false;
fixture.detectChanges();
fixture.whenStable().then(() => {
const emitSpy = spyOn(component.cancel, 'emit');
const el = fixture.nativeElement.querySelector('#adf-no-form-cancel-button');
el.click();
expect(emitSpy).toHaveBeenCalled();
});
}));
it('should hide Cancel button if hideCancelButton is true', async(() => {
component.hideCancelButton = true;
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(element.querySelector('#adf-no-form-cancel-button')).toBeNull();
});
}));
it('should hide Cancel button if isCompleted is true', async(() => {
component.isCompleted = true;
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(element.querySelector('#adf-no-form-cancel-button')).toBeNull();
});
}));
it('should emit complete event if clicked on Complete Button', async(() => {
component.hasCompletePermission = true;
component.isCompleted = false;
fixture.detectChanges();
fixture.whenStable().then(() => {
const emitSpy = spyOn(component.complete, 'emit');
expect(element.querySelector('#adf-no-form-complete-button')).toBeDefined();
const el = fixture.nativeElement.querySelector('#adf-no-form-complete-button');
el.click();
expect(emitSpy).toHaveBeenCalled();
});
}));
it('should hide Complete button if isCompleted is true', async(() => {
component.isCompleted = true;
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(element.querySelector('#adf-no-form-complete-button')).toBeNull();
});
}));
it('should hide Complete button if hasCompletePermission is false', async(() => {
component.hasCompletePermission = false;
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(element.querySelector('#adf-no-form-complete-button')).toBeNull();
});
}));
});

View File

@@ -0,0 +1,87 @@
/*!
* @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, EventEmitter, Input, Output, ViewEncapsulation } from '@angular/core';
@Component({
selector: 'adf-task-standalone',
templateUrl: './task-standalone.component.html',
styleUrls: ['./task-standalone.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class TaskStandaloneComponent {
/** Name of the task. */
@Input()
taskName;
/** Id of the task. */
@Input()
taskId;
/** If true then Task completed message is shown and `Complete` and `Cancel` buttons are hidden. */
@Input()
isCompleted: boolean = false;
/** Toggles rendering of the `Complete` button. */
@Input()
hasCompletePermission: boolean = true;
// TODO: rename all with show prefix
/** Toggles rendering of the `Cancel` button. */
@Input()
hideCancelButton: boolean = true;
/** Emitted when the "Cancel" button is clicked. */
@Output()
cancel: EventEmitter<void> = new EventEmitter<void>();
/** Emitted when the form associated with the task is completed. */
@Output()
complete: EventEmitter<void> = new EventEmitter<void>();
/** Emitted when the form associated with the form task is attached. */
@Output()
showAttachForm: EventEmitter<void> = new EventEmitter<void>();
constructor() { }
onCancelButtonClick(): void {
this.cancel.emit();
}
onCompleteButtonClick(): void {
this.complete.emit();
}
hasCompleteButton(): boolean {
return this.hasCompletePermission && !this.isCompleted;
}
hasCancelButton(): boolean {
return !this.hideCancelButton && !this.isCompleted;
}
hasAttachFormButton(): boolean {
return !this.isCompleted;
}
onShowAttachForm() {
this.showAttachForm.emit();
}
}

View File

@@ -0,0 +1,18 @@
/*!
* @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.
*/
export * from './public-api';

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 { TaskFilterRepresentation, UserTaskFilterRepresentation, TaskQueryRepresentation } from '@alfresco/js-api';
export class AppDefinitionRepresentationModel {
defaultAppId: string;
deploymentId: string;
name: string;
description: string;
theme: string;
icon: string;
id: number;
modelId: number;
tenantId: number;
constructor(obj?: any) {
if (obj) {
this.defaultAppId = obj.defaultAppId ? obj.defaultAppId : null;
this.deploymentId = obj.deploymentId ? obj.deploymentId : null;
this.name = obj.name ? obj.name : null;
this.description = obj.description ? obj.description : null;
this.theme = obj.theme ? obj.theme : null;
this.icon = obj.icon ? obj.icon : null;
this.id = obj.id ? obj.id : null;
this.modelId = obj.modelId ? obj.modelId : null;
this.tenantId = obj.tenantId ? obj.tenantId : null;
}
}
}
export class FilterParamsModel {
id: number;
name: string;
index: number;
constructor(obj?: any) {
if (obj) {
this.id = obj.id || null;
this.name = obj.name || null;
this.index = obj.index;
}
}
}
export class FilterRepresentationModel implements UserTaskFilterRepresentation {
id: number;
appId: number;
name: string;
recent: boolean;
icon: string;
filter: TaskFilterRepresentation;
index: number;
constructor(obj?: any) {
if (obj) {
this.id = obj.id || null;
this.appId = obj.appId || null;
this.name = obj.name || null;
this.recent = !!obj.recent;
this.icon = obj.icon || null;
this.filter = new UserTaskFilterRepresentation(obj.filter);
this.index = obj.index;
}
}
hasFilter() {
return this.filter ? true : false;
}
}
export class TaskQueryRequestRepresentationModel extends TaskQueryRepresentation {
}

View File

@@ -0,0 +1,30 @@
/*!
* @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.
*/
/**
* This object represent of the Form.
*/
export class Form {
id: number;
name: string;
constructor(id: number, name: string) {
this.name = name;
this.id = id;
}
}

View File

@@ -0,0 +1,40 @@
/*!
* @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.
*/
/**
* This object represent of the StartTaskModel.
*/
import { UserProcessModel } from '@alfresco/adf-core';
export class StartTaskModel {
name: string;
description: string;
assignee: UserProcessModel;
dueDate: any;
formKey: any;
category: string;
constructor(obj?: any) {
this.name = obj && obj.name || null;
this.description = obj && obj.description || null;
this.assignee = obj && obj.assignee ? new UserProcessModel(obj.assignee) : null;
this.dueDate = obj && obj.dueDate || null;
this.formKey = obj && obj.formKey || null;
this.category = obj && obj.category || null;
}
}

View File

@@ -0,0 +1,40 @@
/*!
* @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 { TaskDetailsModel } from './task-details.model';
export class TaskDetailsEvent {
private _value: TaskDetailsModel;
private _defaultPrevented: boolean = false;
get value(): TaskDetailsModel {
return this._value;
}
get defaultPrevented() {
return this._defaultPrevented;
}
constructor(value: TaskDetailsModel) {
this._value = value;
}
preventDefault() {
this._defaultPrevented = true;
}
}

View File

@@ -0,0 +1,112 @@
/*!
* @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.
*/
/**
* This object represent the details of a task.
*/
import { UserProcessModel } from '@alfresco/adf-core';
import { TaskRepresentation } from '@alfresco/js-api';
import { UserGroupModel } from './user-group.model';
export class TaskDetailsModel implements TaskRepresentation {
id?: string;
name?: string;
assignee?: UserProcessModel;
priority?: number;
adhocTaskCanBeReassigned?: boolean;
category?: string;
created?: Date;
description?: string;
parentName?: string;
dueDate?: Date;
duration?: number;
endDate?: Date;
executionId?: string;
formKey?: string;
initiatorCanCompleteTask?: boolean;
managerOfCandidateGroup?: boolean;
memberOfCandidateGroup?: boolean;
memberOfCandidateUsers?: boolean;
involvedGroups?: UserGroupModel [];
involvedPeople?: UserProcessModel [];
parentTaskId?: string;
parentTaskName?: string;
processDefinitionCategory?: string;
processDefinitionDeploymentId?: string;
processDefinitionDescription?: string;
processDefinitionId?: string;
processDefinitionKey?: string;
processDefinitionName?: string;
processDefinitionVersion?: number = 0;
processInstanceId?: string;
processInstanceName?: string;
processInstanceStartUserId?: string;
taskDefinitionKey?: string;
constructor(obj?: any) {
if (obj) {
this.id = obj.id || null;
this.name = obj.name || null;
this.priority = obj.priority;
this.assignee = obj.assignee ? new UserProcessModel(obj.assignee) : null;
this.adhocTaskCanBeReassigned = obj.adhocTaskCanBeReassigned;
this.category = obj.category || null;
this.created = obj.created || null;
this.description = obj.description || null;
this.dueDate = obj.dueDate || null;
this.duration = obj.duration || null;
this.endDate = obj.endDate || null;
this.executionId = obj.executionId || null;
this.formKey = obj.formKey || null;
this.initiatorCanCompleteTask = !!obj.initiatorCanCompleteTask;
this.managerOfCandidateGroup = !!obj.managerOfCandidateGroup;
this.memberOfCandidateGroup = !!obj.memberOfCandidateGroup;
this.memberOfCandidateUsers = !!obj.memberOfCandidateUsers;
this.involvedGroups = obj.involvedGroups;
this.involvedPeople = obj.involvedPeople;
this.parentTaskId = obj.parentTaskId || null;
this.parentTaskName = obj.parentTaskName || null;
this.processDefinitionCategory = obj.processDefinitionCategory || null;
this.processDefinitionDeploymentId = obj.processDefinitionDeploymentId || null;
this.processDefinitionDescription = obj.processDefinitionDescription || null;
this.processDefinitionId = obj.processDefinitionId || null;
this.processDefinitionKey = obj.processDefinitionKey || null;
this.processDefinitionName = obj.processDefinitionName || null;
this.processDefinitionVersion = obj.processDefinitionVersion || 0;
this.processInstanceId = obj.processInstanceId || null;
this.processInstanceName = obj.processInstanceName || null;
this.processInstanceStartUserId = obj.processInstanceStartUserId || null;
this.taskDefinitionKey = obj.taskDefinitionKey || null;
}
}
getFullName(): string {
let fullName: string = '';
if (this.assignee) {
const firstName: string = this.assignee.firstName ? this.assignee.firstName : '';
const lastName: string = this.assignee.lastName ? this.assignee.lastName : '';
fullName = `${firstName} ${lastName}`;
}
return fullName.trim();
}
isCompleted(): boolean {
return !!this.endDate;
}
}

View File

@@ -0,0 +1,37 @@
/*!
* @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 { TaskDetailsModel } from './task-details.model';
export class TaskListModel {
size?: number;
total?: number;
start?: number;
length?: number;
data?: TaskDetailsModel[] = [];
constructor(input?: any) {
if (input) {
Object.assign(this, input);
if (input.data) {
this.data = input.data.map((item: any) => {
return new TaskDetailsModel(item);
});
}
}
}
}

View File

@@ -0,0 +1,41 @@
/*!
* @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.
*/
export let taskPresetsDefaultModel = {
'default': [
{
'key': 'name',
'type': 'text',
'title': 'ADF_TASK_LIST.PROPERTIES.NAME',
'sortable': true
},
{
'key': 'created',
'type': 'text',
'title': 'ADF_TASK_LIST.PROPERTIES.CREATED',
'cssClass': 'hidden',
'sortable': true
},
{
'key': 'assignee',
'type': 'text',
'title': 'ADF_TASK_LIST.PROPERTIES.ASSIGNEE',
'cssClass': 'hidden',
'sortable': true
}
]
};

View File

@@ -0,0 +1,29 @@
/*!
* @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.
*/
/**
* This object represent the User Event.
*/
export class UserEventModel {
type: string = '';
value: any = {};
constructor(obj?: any) {
this.type = obj && obj.type;
this.value = obj && obj.value || {};
}
}

View File

@@ -0,0 +1,36 @@
/*!
* @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.
*/
/**
* This object represent the process service user group.*
*/
export class UserGroupModel {
id?: number;
name?: string;
externalId?: string;
status?: string;
groups?: any = {};
constructor(obj?: any) {
this.id = obj && obj.id;
this.name = obj && obj.name;
this.externalId = obj && obj.externalId;
this.status = obj && obj.status;
this.groups = obj && obj.groups;
}
}

View File

@@ -0,0 +1,43 @@
/*!
* @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.
*/
export * from './components/task-list.component';
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-details.component';
export * from './components/task-audit.directive';
export * from './components/start-task.component';
export * from './components/task-standalone.component';
export * from './components/attach-form.component';
export * from './services/tasklist.service';
export * from './services/process-upload.service';
export * from './services/task-upload.service';
export * from './services/task-filter.service';
export * from './models/filter.model';
export * from './models/form.model';
export * from './models/start-task.model';
export * from './models/task-details.event';
export * from './models/task-details.model';
export * from './models/task-list.model';
export * from './models/user-event.model';
export * from './models/user-group.model';
export * from './task-list.module';

View File

@@ -0,0 +1,47 @@
/*!
* @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 { AlfrescoApiService, AppConfigService, UploadService } from '@alfresco/adf-core';
import { Injectable } from '@angular/core';
import { throwError } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class ProcessUploadService extends UploadService {
constructor(apiService: AlfrescoApiService, appConfigService: AppConfigService) {
super(apiService, appConfigService);
}
getUploadPromise(file: any): any {
const opts = {
isRelatedContent: true
};
const processInstanceId = file.options.parentId;
const promise = this.apiService.getInstance().activiti.contentApi.createRelatedContentOnProcessInstance(processInstanceId, file.file, opts);
promise.catch((err) => this.handleError(err));
return promise;
}
private handleError(error: any) {
return throwError(error || 'Server error');
}
}

View File

@@ -0,0 +1,201 @@
/*!
* @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 { async } from '@angular/core/testing';
import { fakeAppFilter, fakeAppPromise, fakeFilters } from '../../mock';
import { FilterRepresentationModel } from '../models/filter.model';
import { TaskFilterService } from './task-filter.service';
import { AlfrescoApiServiceMock, LogService, AppConfigService, setupTestBed, CoreModule, StorageService } from '@alfresco/adf-core';
declare let jasmine: any;
describe('Activiti Task filter Service', () => {
let service: TaskFilterService;
setupTestBed({
imports: [
CoreModule.forRoot()
]
});
beforeEach(async(() => {
service = new TaskFilterService(
new AlfrescoApiServiceMock(new AppConfigService(null), new StorageService()),
new LogService(new AppConfigService(null)));
jasmine.Ajax.install();
}));
afterEach(() => {
jasmine.Ajax.uninstall();
});
describe('Content tests', () => {
it('should return the task list filters', (done) => {
service.getTaskListFilters().subscribe((res) => {
expect(res).toBeDefined();
expect(res.length).toEqual(2);
expect(res[0].name).toEqual('FakeInvolvedTasks');
expect(res[1].name).toEqual('FakeMyTasks');
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200,
contentType: 'application/json',
responseText: JSON.stringify(fakeFilters)
});
});
it('should return the task filter by id', (done) => {
service.getTaskFilterById(2).subscribe((taskFilter: FilterRepresentationModel) => {
expect(taskFilter).toBeDefined();
expect(taskFilter.id).toEqual(2);
expect(taskFilter.name).toEqual('FakeMyTasks');
expect(taskFilter.filter.sort).toEqual('created-desc');
expect(taskFilter.filter.state).toEqual('open');
expect(taskFilter.filter.assignment).toEqual('fake-assignee');
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200,
contentType: 'application/json',
responseText: JSON.stringify(fakeFilters)
});
});
it('should return the task filter by name', (done) => {
service.getTaskFilterByName('FakeMyTasks').subscribe((res: FilterRepresentationModel) => {
expect(res).toBeDefined();
expect(res.id).toEqual(2);
expect(res.name).toEqual('FakeMyTasks');
expect(res.filter.sort).toEqual('created-desc');
expect(res.filter.state).toEqual('open');
expect(res.filter.assignment).toEqual('fake-assignee');
done();
}
);
jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200,
contentType: 'application/json',
responseText: JSON.stringify(fakeFilters)
});
});
it('should call the api with the appId', (done) => {
spyOn(service, 'callApiTaskFilters').and.returnValue((fakeAppPromise));
const appId = 1;
service.getTaskListFilters(appId).subscribe(() => {
expect(service.callApiTaskFilters).toHaveBeenCalledWith(appId);
done();
});
});
it('should return the app filter by id', (done) => {
const appId = 1;
service.getTaskListFilters(appId).subscribe((res) => {
expect(res).toBeDefined();
expect(res.length).toEqual(1);
expect(res[0].name).toEqual('FakeInvolvedTasks');
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200,
contentType: 'application/json',
responseText: JSON.stringify(fakeAppFilter)
});
});
it('should return the default filters', (done) => {
service.createDefaultFilters(1234).subscribe((res: FilterRepresentationModel []) => {
expect(res).toBeDefined();
expect(res.length).toEqual(4);
expect(res[0].name).toEqual('Involved Tasks');
expect(res[0].id).toEqual(111);
expect(res[1].name).toEqual('My Tasks');
expect(res[1].id).toEqual(222);
expect(res[2].name).toEqual('Queued Tasks');
expect(res[2].id).toEqual(333);
expect(res[3].name).toEqual('Completed Tasks');
expect(res[3].id).toEqual(444);
done();
});
jasmine.Ajax.requests.at(0).respondWith({
'status': 200,
contentType: 'application/json',
responseText: JSON.stringify({
appId: 1001, id: 111, name: 'Involved Tasks', icon: 'fake-icon', recent: false
})
});
jasmine.Ajax.requests.at(1).respondWith({
'status': 200,
contentType: 'application/json',
responseText: JSON.stringify({
appId: 1001, id: 222, name: 'My Tasks', icon: 'fake-icon', recent: false
})
});
jasmine.Ajax.requests.at(2).respondWith({
'status': 200,
contentType: 'application/json',
responseText: JSON.stringify({
appId: 1001, id: 333, name: 'Queued Tasks', icon: 'fake-icon', recent: false
})
});
jasmine.Ajax.requests.at(3).respondWith({
'status': 200,
contentType: 'application/json',
responseText: JSON.stringify({
appId: 1001, id: 444, name: 'Completed Tasks', icon: 'fake-icon', recent: false
})
});
});
it('should add a filter', (done) => {
const filterFake = new FilterRepresentationModel({
name: 'FakeNameFilter',
assignment: 'fake-assignment'
});
service.addFilter(filterFake).subscribe((res: FilterRepresentationModel) => {
expect(res).toBeDefined();
expect(res.id).not.toEqual(null);
expect(res.name).toEqual('FakeNameFilter');
expect(res.filter.assignment).toEqual('fake-assignment');
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200,
contentType: 'application/json',
responseText: JSON.stringify({
id: '2233', name: 'FakeNameFilter', filter: { assignment: 'fake-assignment' }
})
});
});
});
});

View File

@@ -0,0 +1,223 @@
/*!
* @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 { AlfrescoApiService, LogService } from '@alfresco/adf-core';
import { Injectable } from '@angular/core';
import { Observable, forkJoin, from, throwError } from 'rxjs';
import { FilterRepresentationModel } from '../models/filter.model';
import { map, catchError } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class TaskFilterService {
constructor(private apiService: AlfrescoApiService,
private logService: LogService) {
}
/**
* Creates and returns the default filters for a process app.
* @param appId ID of the target app
* @returns Array of default filters just created
*/
public createDefaultFilters(appId: number): Observable<FilterRepresentationModel[]> {
const involvedTasksFilter = this.getInvolvedTasksFilterInstance(appId);
const involvedObservable = this.addFilter(involvedTasksFilter);
const myTasksFilter = this.getMyTasksFilterInstance(appId);
const myTaskObservable = this.addFilter(myTasksFilter);
const queuedTasksFilter = this.getQueuedTasksFilterInstance(appId);
const queuedObservable = this.addFilter(queuedTasksFilter);
const completedTasksFilter = this.getCompletedTasksFilterInstance(appId);
const completeObservable = this.addFilter(completedTasksFilter);
return new Observable((observer) => {
forkJoin(
involvedObservable,
myTaskObservable,
queuedObservable,
completeObservable
).subscribe(
(res) => {
const filters: FilterRepresentationModel[] = [];
res.forEach((filter) => {
if (filter.name === involvedTasksFilter.name) {
involvedTasksFilter.id = filter.id;
filters.push(involvedTasksFilter);
} else if (filter.name === myTasksFilter.name) {
myTasksFilter.id = filter.id;
filters.push(myTasksFilter);
} else if (filter.name === queuedTasksFilter.name) {
queuedTasksFilter.id = filter.id;
filters.push(queuedTasksFilter);
} else if (filter.name === completedTasksFilter.name) {
completedTasksFilter.id = filter.id;
filters.push(completedTasksFilter);
}
});
observer.next(filters);
observer.complete();
},
(err: any) => {
this.logService.error(err);
});
});
}
/**
* Gets all task filters for a process app.
* @param appId Optional ID for a specific app
* @returns Array of task filter details
*/
getTaskListFilters(appId?: number): Observable<FilterRepresentationModel[]> {
return from(this.callApiTaskFilters(appId))
.pipe(
map((response: any) => {
const filters: FilterRepresentationModel[] = [];
response.data.forEach((filter: FilterRepresentationModel) => {
const filterModel = new FilterRepresentationModel(filter);
filters.push(filterModel);
});
return filters;
}),
catchError((err) => this.handleError(err))
);
}
/**
* Gets a task filter by ID.
* @param filterId ID of the filter
* @param appId ID of the app for the filter
* @returns Details of task filter
*/
getTaskFilterById(filterId: number, appId?: number): Observable<FilterRepresentationModel> {
return from(this.callApiTaskFilters(appId)).pipe(
map((response) => response.data.find((filter) => filter.id === filterId)),
catchError((err) => this.handleError(err))
);
}
/**
* Gets a task filter by name.
* @param taskName Name of the filter
* @param appId ID of the app for the filter
* @returns Details of task filter
*/
getTaskFilterByName(taskName: string, appId?: number): Observable<FilterRepresentationModel> {
return from(this.callApiTaskFilters(appId)).pipe(
map((response) => response.data.find((filter) => filter.name === taskName)),
catchError((err) => this.handleError(err))
);
}
/**
* Adds a new task filter
* @param filter The new filter to add
* @returns Details of task filter just added
*/
addFilter(filter: FilterRepresentationModel): Observable<FilterRepresentationModel> {
return from(this.apiService.getInstance().activiti.userFiltersApi.createUserTaskFilter(filter))
.pipe(
map((response: FilterRepresentationModel) => {
return response;
}),
catchError((err) => this.handleError(err))
);
}
/**
* Calls `getUserTaskFilters` from the Alfresco JS API.
* @param appId ID of the target app
* @returns List of task filters
*/
callApiTaskFilters(appId?: number): Promise<any> {
if (appId) {
return this.apiService.getInstance().activiti.userFiltersApi.getUserTaskFilters({appId: appId});
} else {
return this.apiService.getInstance().activiti.userFiltersApi.getUserTaskFilters();
}
}
/**
* Creates and returns a filter for "Involved" task instances.
* @param appId ID of the target app
* @returns The newly created filter
*/
getInvolvedTasksFilterInstance(appId: number): FilterRepresentationModel {
return new FilterRepresentationModel({
'name': 'Involved Tasks',
'appId': appId,
'recent': false,
'icon': 'glyphicon-align-left',
'filter': {'sort': 'created-desc', 'name': '', 'state': 'open', 'assignment': 'involved'}
});
}
/**
* Creates and returns a filter for "My Tasks" task instances.
* @param appId ID of the target app
* @returns The newly created filter
*/
getMyTasksFilterInstance(appId: number): FilterRepresentationModel {
return new FilterRepresentationModel({
'name': 'My Tasks',
'appId': appId,
'recent': false,
'icon': 'glyphicon-inbox',
'filter': {'sort': 'created-desc', 'name': '', 'state': 'open', 'assignment': 'assignee'}
});
}
/**
* Creates and returns a filter for "Queued Tasks" task instances.
* @param appId ID of the target app
* @returns The newly created filter
*/
getQueuedTasksFilterInstance(appId: number): FilterRepresentationModel {
return new FilterRepresentationModel({
'name': 'Queued Tasks',
'appId': appId,
'recent': false,
'icon': 'glyphicon-record',
'filter': {'sort': 'created-desc', 'name': '', 'state': 'open', 'assignment': 'candidate'}
});
}
/**
* Creates and returns a filter for "Completed" task instances.
* @param appId ID of the target app
* @returns The newly created filter
*/
getCompletedTasksFilterInstance(appId: number): FilterRepresentationModel {
return new FilterRepresentationModel({
'name': 'Completed Tasks',
'appId': appId,
'recent': true,
'icon': 'glyphicon-ok-sign',
'filter': {'sort': 'created-desc', 'name': '', 'state': 'completed', 'assignment': 'involved'}
});
}
private handleError(error: any) {
this.logService.error(error);
return throwError(error || 'Server error');
}
}

View File

@@ -0,0 +1,47 @@
/*!
* @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 { AlfrescoApiService, AppConfigService, UploadService } from '@alfresco/adf-core';
import { Injectable } from '@angular/core';
import { throwError } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class TaskUploadService extends UploadService {
constructor(apiService: AlfrescoApiService, appConfigService: AppConfigService) {
super(apiService, appConfigService);
}
getUploadPromise(file: any): any {
const opts = {
isRelatedContent: true
};
const taskId = file.options.parentId;
const promise = this.apiService.getInstance().activiti.contentApi.createRelatedContentOnTask(taskId, file.file, opts);
promise.catch((err) => this.handleError(err));
return promise;
}
private handleError(error: any) {
return throwError(error || 'Server error');
}
}

View File

@@ -0,0 +1,540 @@
/*!
* @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 { async } from '@angular/core/testing';
import { UserProcessModel, setupTestBed, CoreModule, StorageService } from '@alfresco/adf-core';
import { of } from 'rxjs';
import {
fakeCompletedTaskList,
fakeFilter,
fakeFormList,
fakeOpenTaskList,
fakeRepresentationFilter1,
fakeRepresentationFilter2,
fakeTaskDetails,
fakeTaskList,
fakeTasksChecklist,
fakeUser1,
fakeUser2,
secondFakeTaskList
} from '../../mock';
import { FilterRepresentationModel, TaskQueryRequestRepresentationModel } from '../models/filter.model';
import { TaskDetailsModel } from '../models/task-details.model';
import { TaskListService } from './tasklist.service';
import { AlfrescoApiServiceMock, LogService, AppConfigService } from '@alfresco/adf-core';
declare let jasmine: any;
describe('Activiti TaskList Service', () => {
let service: TaskListService;
setupTestBed({
imports: [
CoreModule.forRoot()
]
});
beforeEach(async(() => {
service = new TaskListService(
new AlfrescoApiServiceMock(new AppConfigService(null), new StorageService()),
new LogService(new AppConfigService(null)));
jasmine.Ajax.install();
}));
afterEach(() => {
jasmine.Ajax.uninstall();
});
describe('Content tests', () => {
it('should return the task list filtered', (done) => {
service.getTasks(<TaskQueryRequestRepresentationModel> fakeFilter).subscribe((res) => {
expect(res).toBeDefined();
expect(res.size).toEqual(1);
expect(res.start).toEqual(0);
expect(res.data).toBeDefined();
expect(res.data.length).toEqual(1);
expect(res.data[0].name).toEqual('FakeNameTask');
expect(res.data[0].assignee.email).toEqual('fake-email@dom.com');
expect(res.data[0].assignee.firstName).toEqual('firstName');
expect(res.data[0].assignee.lastName).toEqual('lastName');
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200,
contentType: 'application/json',
responseText: JSON.stringify(fakeTaskList)
});
});
it('should return the task list with all tasks filtered by state', (done) => {
spyOn(service, 'getTasks').and.returnValue(of(fakeTaskList));
spyOn(service, 'getTotalTasks').and.returnValue(of(fakeTaskList));
service.findAllTaskByState(<TaskQueryRequestRepresentationModel> fakeFilter, 'open').subscribe((res) => {
expect(res).toBeDefined();
expect(res.size).toEqual(1);
expect(res.start).toEqual(0);
expect(res.data).toBeDefined();
expect(res.data.length).toEqual(1);
expect(res.data[0].name).toEqual('FakeNameTask');
expect(res.data[0].assignee.email).toEqual('fake-email@dom.com');
expect(res.data[0].assignee.firstName).toEqual('firstName');
expect(res.data[0].assignee.lastName).toEqual('lastName');
done();
});
});
it('should return the task list with all tasks filtered', (done) => {
spyOn(service, 'getTasks').and.returnValue(of(fakeTaskList));
spyOn(service, 'getTotalTasks').and.returnValue(of(fakeTaskList));
service.findAllTaskByState(<TaskQueryRequestRepresentationModel> fakeFilter).subscribe((res) => {
expect(res).toBeDefined();
expect(res.size).toEqual(1);
expect(res.start).toEqual(0);
expect(res.data).toBeDefined();
expect(res.data.length).toEqual(1);
expect(res.data[0].name).toEqual('FakeNameTask');
expect(res.data[0].assignee.email).toEqual('fake-email@dom.com');
expect(res.data[0].assignee.firstName).toEqual('firstName');
expect(res.data[0].assignee.lastName).toEqual('lastName');
done();
});
});
it('should return the task list filtered by state', (done) => {
service.findTasksByState(<TaskQueryRequestRepresentationModel> fakeFilter, 'open').subscribe((res) => {
expect(res).toBeDefined();
expect(res.size).toEqual(1);
expect(res.start).toEqual(0);
expect(res.data).toBeDefined();
expect(res.data.length).toEqual(1);
expect(res.data[0].name).toEqual('FakeNameTask');
expect(res.data[0].assignee.email).toEqual('fake-email@dom.com');
expect(res.data[0].assignee.firstName).toEqual('firstName');
expect(res.data[0].assignee.lastName).toEqual('lastName');
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200,
contentType: 'application/json',
responseText: JSON.stringify(fakeTaskList)
});
});
it('should return the task list filtered', (done) => {
service.findTasksByState(<TaskQueryRequestRepresentationModel> fakeFilter).subscribe((res) => {
expect(res.size).toEqual(1);
expect(res.start).toEqual(0);
expect(res.data).toBeDefined();
expect(res.data.length).toEqual(1);
expect(res.data[0].name).toEqual('FakeNameTask');
expect(res.data[0].assignee.email).toEqual('fake-email@dom.com');
expect(res.data[0].assignee.firstName).toEqual('firstName');
expect(res.data[0].assignee.lastName).toEqual('lastName');
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200,
contentType: 'application/json',
responseText: JSON.stringify(fakeTaskList)
});
});
it('should return the task list with all tasks filtered without state', (done) => {
spyOn(service, 'getTasks').and.returnValue(of(fakeTaskList));
spyOn(service, 'getTotalTasks').and.returnValue(of(fakeTaskList));
service.findAllTasksWithoutState(<TaskQueryRequestRepresentationModel> fakeFilter).subscribe((res) => {
expect(res).toBeDefined();
expect(res.data).toBeDefined();
expect(res.data.length).toEqual(2);
expect(res.data[0].name).toEqual('FakeNameTask');
expect(res.data[0].assignee.email).toEqual('fake-email@dom.com');
expect(res.data[0].assignee.firstName).toEqual('firstName');
expect(res.data[0].assignee.lastName).toEqual('lastName');
expect(res.data[1].name).toEqual('FakeNameTask');
expect(res.data[1].assignee.email).toEqual('fake-email@dom.com');
expect(res.data[1].assignee.firstName).toEqual('firstName');
expect(res.data[1].assignee.lastName).toEqual('lastName');
done();
});
});
it('Should return both open and completed task', (done) => {
spyOn(service, 'findTasksByState').and.returnValue(of(fakeOpenTaskList));
spyOn(service, 'findAllTaskByState').and.returnValue(of(fakeCompletedTaskList));
service.findAllTasksWithoutState(<TaskQueryRequestRepresentationModel> fakeFilter).subscribe((res) => {
expect(res).toBeDefined();
expect(res.data).toBeDefined();
expect(res.data.length).toEqual(4);
expect(res.data[0].name).toEqual('FakeOpenTask1');
expect(res.data[1].assignee.email).toEqual('fake-open-email@dom.com');
expect(res.data[2].name).toEqual('FakeCompletedTaskName1');
expect(res.data[2].assignee.email).toEqual('fake-completed-email@dom.com');
expect(res.data[3].name).toEqual('FakeCompletedTaskName2');
done();
});
});
it('should add the task list to the tasklistSubject with all tasks filtered without state', (done) => {
spyOn(service, 'getTasks').and.returnValue(of(fakeTaskList));
spyOn(service, 'getTotalTasks').and.returnValue(of(fakeTaskList));
service.findAllTasksWithoutState(<TaskQueryRequestRepresentationModel> fakeFilter).subscribe((res) => {
expect(res).toBeDefined();
expect(res.data).toBeDefined();
expect(res.data.length).toEqual(2);
expect(res.data[0].name).toEqual('FakeNameTask');
expect(res.data[0].assignee.email).toEqual('fake-email@dom.com');
expect(res.data[1].name).toEqual('FakeNameTask');
expect(res.data[1].assignee.email).toEqual('fake-email@dom.com');
done();
});
});
it('should return the task details ', (done) => {
service.getTaskDetails('999').subscribe((res: TaskDetailsModel) => {
expect(res).toBeDefined();
expect(res.id).toEqual('999');
expect(res.name).toEqual('fake-task-name');
expect(res.formKey).toEqual('99');
expect(res.assignee).toBeDefined();
expect(res.assignee.email).toEqual('fake-email@dom.com');
expect(res.assignee.firstName).toEqual('firstName');
expect(res.assignee.lastName).toEqual('lastName');
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200,
contentType: 'application/json',
responseText: JSON.stringify(fakeTaskDetails)
});
});
it('should return the tasks checklists ', (done) => {
service.getTaskChecklist('999').subscribe((res: TaskDetailsModel[]) => {
expect(res).toBeDefined();
expect(res.length).toEqual(2);
expect(res[0].name).toEqual('FakeCheckTask1');
expect(res[0].assignee.email).toEqual('fake-email@dom.com');
expect(res[0].assignee.firstName).toEqual('firstName');
expect(res[0].assignee.lastName).toEqual('lastName');
expect(res[1].name).toEqual('FakeCheckTask2');
expect(res[1].assignee.email).toEqual('fake-email@dom.com');
expect(res[1].assignee.firstName).toEqual('firstName');
expect(res[0].assignee.lastName).toEqual('lastName');
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200,
contentType: 'application/json',
responseText: JSON.stringify(fakeTasksChecklist)
});
});
it('should add a task ', (done) => {
const taskFake = new TaskDetailsModel({
id: 123,
parentTaskId: 456,
name: 'FakeNameTask',
description: null,
category: null,
assignee: fakeUser1,
created: ''
});
service.addTask(taskFake).subscribe((res: TaskDetailsModel) => {
expect(res).toBeDefined();
expect(res.id).not.toEqual('');
expect(res.name).toEqual('FakeNameTask');
expect(res.created).not.toEqual(null);
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200,
contentType: 'application/json',
responseText: JSON.stringify({
id: '777', name: 'FakeNameTask', description: null, category: null,
assignee: fakeUser1,
created: '2016-07-15T11:19:17.440+0000'
})
});
});
it('should remove a checklist task ', (done) => {
service.deleteTask('999').subscribe(() => {
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200,
contentType: 'application/json'
});
});
it('should complete the task', (done) => {
service.completeTask('999').subscribe((res: any) => {
expect(res).toBeDefined();
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200,
contentType: 'application/json',
responseText: JSON.stringify({})
});
});
it('should return the total number of tasks', (done) => {
service.getTotalTasks(<TaskQueryRequestRepresentationModel> fakeFilter).subscribe((res: any) => {
expect(res).toBeDefined();
expect(res.size).toEqual(1);
expect(res.total).toEqual(1);
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200,
contentType: 'application/json',
responseText: JSON.stringify(fakeTaskList)
});
});
it('should create a new standalone task ', (done) => {
const taskFake = new TaskDetailsModel({
name: 'FakeNameTask',
description: 'FakeDescription',
category: '3'
});
service.createNewTask(taskFake).subscribe((res: TaskDetailsModel) => {
expect(res).toBeDefined();
expect(res.id).not.toEqual('');
expect(res.name).toEqual('FakeNameTask');
expect(res.description).toEqual('FakeDescription');
expect(res.category).toEqual('3');
expect(res.created).not.toEqual(null);
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200,
contentType: 'application/json',
responseText: JSON.stringify({
id: '777',
name: 'FakeNameTask',
description: 'FakeDescription',
category: '3',
assignee: fakeUser1,
created: '2016-07-15T11:19:17.440+0000'
})
});
});
it('should assign task to a user', (done) => {
const testTaskId = '8888';
service.assignTask(testTaskId, fakeUser2).subscribe((res: TaskDetailsModel) => {
expect(res).toBeDefined();
expect(res.id).toEqual(testTaskId);
expect(res.name).toEqual('FakeNameTask');
expect(res.description).toEqual('FakeDescription');
expect(res.category).toEqual('3');
expect(res.created).not.toEqual(null);
expect(res.adhocTaskCanBeReassigned).toBe(true);
expect(res.assignee).toEqual(new UserProcessModel(fakeUser2));
expect(res.involvedPeople[0].email).toEqual(fakeUser1.email);
expect(res.involvedPeople[0].firstName).toEqual(fakeUser1.firstName);
expect(res.involvedPeople[0].lastName).toEqual(fakeUser1.lastName);
expect(res.involvedPeople[0].id).toEqual(fakeUser1.id);
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200,
contentType: 'application/json',
responseText: JSON.stringify({
id: testTaskId,
name: 'FakeNameTask',
description: 'FakeDescription',
adhocTaskCanBeReassigned: true,
category: '3',
assignee: fakeUser2,
involvedPeople: [fakeUser1],
created: '2016-07-15T11:19:17.440+0000'
})
});
});
it('should assign task to a userId', (done) => {
const testTaskId = '8888';
service.assignTaskByUserId(testTaskId, fakeUser2.id.toString()).subscribe((res: TaskDetailsModel) => {
expect(res).toBeDefined();
expect(res.id).toEqual(testTaskId);
expect(res.name).toEqual('FakeNameTask');
expect(res.description).toEqual('FakeDescription');
expect(res.category).toEqual('3');
expect(res.created).not.toEqual(null);
expect(res.adhocTaskCanBeReassigned).toBe(true);
expect(res.assignee).toEqual(new UserProcessModel(fakeUser2));
expect(res.involvedPeople[0].email).toEqual(fakeUser1.email);
expect(res.involvedPeople[0].firstName).toEqual(fakeUser1.firstName);
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200,
contentType: 'application/json',
responseText: JSON.stringify({
id: testTaskId,
name: 'FakeNameTask',
description: 'FakeDescription',
adhocTaskCanBeReassigned: true,
category: '3',
assignee: fakeUser2,
involvedPeople: [fakeUser1],
created: '2016-07-15T11:19:17.440+0000'
})
});
});
it('should claim a task', (done) => {
const taskId = '111';
service.claimTask(taskId).subscribe(() => {
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200,
contentType: 'application/json',
responseText: JSON.stringify({})
});
});
it('should unclaim a task', (done) => {
const taskId = '111';
service.unclaimTask(taskId).subscribe(() => {
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200,
contentType: 'application/json',
responseText: JSON.stringify({})
});
});
it('should update a task', (done) => {
const taskId = '111';
service.updateTask(taskId, { property: 'value' }).subscribe(() => {
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200,
contentType: 'application/json',
responseText: JSON.stringify({})
});
});
it('should return the filter if it contains task id', (done) => {
const taskId = '1';
const filterFake = new FilterRepresentationModel({
name: 'FakeNameFilter',
assignment: 'fake-assignment',
filter: {
processDefinitionKey: '1',
assignment: 'fake',
state: 'none',
sort: 'asc'
}
});
service.isTaskRelatedToFilter(taskId, filterFake).subscribe((res: any) => {
expect(res).toBeDefined();
expect(res).not.toBeNull();
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200,
contentType: 'application/json',
responseText: JSON.stringify(fakeTaskList)
});
});
it('should return the filters if it contains task id', (done) => {
const taskId = '1';
const fakeFilterList: FilterRepresentationModel[] = [];
fakeFilterList.push(fakeRepresentationFilter1, fakeRepresentationFilter2);
let resultFilter: FilterRepresentationModel = null;
service.getFilterForTaskById(taskId, fakeFilterList).subscribe((res: FilterRepresentationModel) => {
resultFilter = res;
}, () => {
}, () => {
expect(resultFilter).toBeDefined();
expect(resultFilter).not.toBeNull();
expect(resultFilter.name).toBe('CONTAIN FILTER');
done();
});
jasmine.Ajax.requests.at(0).respondWith({
'status': 200,
contentType: 'application/json',
responseText: JSON.stringify(fakeTaskList)
});
jasmine.Ajax.requests.at(1).respondWith({
'status': 200,
contentType: 'application/json',
responseText: JSON.stringify(secondFakeTaskList)
});
});
it('should get possible form list', (done) => {
service.getFormList().subscribe((res: any) => {
expect(res).toBeDefined();
expect(res.length).toBe(2);
expect(res[0].id).toBe(1);
expect(res[0].name).toBe('form with all widgets');
expect(res[1].id).toBe(2);
expect(res[1].name).toBe('uppy');
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200,
contentType: 'application/json',
responseText: JSON.stringify(fakeFormList)
});
});
});
});

View File

@@ -0,0 +1,436 @@
/*!
* @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 { AlfrescoApiService, LogService } from '@alfresco/adf-core';
import { Injectable } from '@angular/core';
import { Observable, from, forkJoin, throwError, of } from 'rxjs';
import { map, catchError, switchMap, flatMap, filter } from 'rxjs/operators';
import { FilterRepresentationModel, TaskQueryRequestRepresentationModel } from '../models/filter.model';
import { Form } from '../models/form.model';
import { TaskDetailsModel } from '../models/task-details.model';
import { TaskListModel } from '../models/task-list.model';
import {
TaskQueryRepresentation,
AssigneeIdentifierRepresentation
} from '@alfresco/js-api';
@Injectable({
providedIn: 'root'
})
export class TaskListService {
constructor(private apiService: AlfrescoApiService,
private logService: LogService) {
}
/**
* Gets all the filters in the list that belong to a task.
* @param taskId ID of the target task
* @param filterList List of filters to search through
* @returns Filters belonging to the task
*/
getFilterForTaskById(taskId: string, filterList: FilterRepresentationModel[]): Observable<FilterRepresentationModel> {
return from(filterList)
.pipe(
flatMap((data: FilterRepresentationModel) => this.isTaskRelatedToFilter(taskId, data)),
filter((data: FilterRepresentationModel) => data != null)
);
}
/**
* Gets the search query for a task based on the supplied filter.
* @param filter The filter to use
* @returns The search query
*/
private generateTaskRequestNodeFromFilter(filterModel: FilterRepresentationModel): TaskQueryRequestRepresentationModel {
const requestNode = {
appDefinitionId: filterModel.appId,
assignment: filterModel.filter.assignment,
state: filterModel.filter.state,
sort: filterModel.filter.sort
};
return new TaskQueryRequestRepresentationModel(requestNode);
}
/**
* Checks if a taskId is filtered with the given filter.
* @param taskId ID of the target task
* @param filterModel The filter you want to check
* @returns The filter if it is related or null otherwise
*/
isTaskRelatedToFilter(taskId: string, filterModel: FilterRepresentationModel): Observable<FilterRepresentationModel> {
const requestNodeForFilter = this.generateTaskRequestNodeFromFilter(filterModel);
return from(this.callApiTasksFiltered(requestNodeForFilter))
.pipe(
map((res: any) => {
return res.data.find((element) => element.id === taskId) ? filterModel : null;
}),
catchError((err) => this.handleError(err))
);
}
/**
* Gets all the tasks matching the supplied query.
* @param requestNode Query to search for tasks
* @returns List of tasks
*/
getTasks(requestNode: TaskQueryRequestRepresentationModel): Observable<TaskListModel> {
return from(this.callApiTasksFiltered(requestNode))
.pipe(
catchError((err) => this.handleError(err))
);
}
/**
* Gets tasks matching a query and state value.
* @param requestNode Query to search for tasks
* @param state Task state. Can be "open" or "completed".
* @returns List of tasks
*/
findTasksByState(requestNode: TaskQueryRequestRepresentationModel, state?: string): Observable<TaskListModel> {
if (state) {
requestNode.state = state;
}
return this.getTasks(requestNode)
.pipe(catchError(() => of(new TaskListModel())));
}
/**
* Gets all tasks matching a query and state value.
* @param requestNode Query to search for tasks.
* @param state Task state. Can be "open" or "completed".
* @returns List of tasks
*/
findAllTaskByState(requestNode: TaskQueryRequestRepresentationModel, state?: string): Observable<TaskListModel> {
if (state) {
requestNode.state = state;
}
return this.getTotalTasks(requestNode)
.pipe(
switchMap((res: any) => {
requestNode.size = res.total;
return this.getTasks(requestNode);
})
);
}
/**
* Gets all tasks matching the supplied query but ignoring the task state.
* @param requestNode Query to search for tasks
* @returns List of tasks
*/
findAllTasksWithoutState(requestNode: TaskQueryRequestRepresentationModel): Observable<TaskListModel> {
return forkJoin(
this.findTasksByState(requestNode, 'open'),
this.findAllTaskByState(requestNode, 'completed'),
(activeTasks: TaskListModel, completedTasks: TaskListModel) => {
const tasks = Object.assign({}, activeTasks);
tasks.total += completedTasks.total;
tasks.data = tasks.data.concat(completedTasks.data);
return tasks;
}
);
}
/**
* Gets details for a task.
* @param taskId ID of the target task.
* @returns Task details
*/
getTaskDetails(taskId: string): Observable<TaskDetailsModel> {
return from(this.callApiTaskDetails(taskId))
.pipe(
map((details: any) => {
return new TaskDetailsModel(details);
}),
catchError((err) => this.handleError(err))
);
}
/**
* Gets the checklist for a task.
* @param id ID of the target task
* @returns Array of checklist task details
*/
getTaskChecklist(id: string): Observable<TaskDetailsModel[]> {
return from(this.callApiTaskChecklist(id))
.pipe(
map((response: any) => {
const checklists: TaskDetailsModel[] = [];
response.data.forEach((checklist) => {
checklists.push(new TaskDetailsModel(checklist));
});
return checklists;
}),
catchError((err) => this.handleError(err))
);
}
/**
* Gets all available reusable forms.
* @returns Array of form details
*/
getFormList(): Observable<Form[]> {
const opts = {
'filter': 'myReusableForms', // String | filter
'sort': 'modifiedDesc', // String | sort
'modelType': 2 // Integer | modelType
};
return from(this.apiService.getInstance().activiti.modelsApi.getModels(opts))
.pipe(
map((response: any) => {
const forms: Form[] = [];
response.data.forEach((form) => {
forms.push(new Form(form.id, form.name));
});
return forms;
}),
catchError((err) => this.handleError(err))
);
}
/**
* Attaches a form to a task.
* @param taskId ID of the target task
* @param formId ID of the form to add
* @returns Null response notifying when the operation is complete
*/
attachFormToATask(taskId: string, formId: number): Observable<any> {
return from(this.apiService.taskApi.attachForm(taskId, { 'formId': formId }))
.pipe(
catchError((err) => this.handleError(err))
);
}
/**
* Adds a subtask (ie, a checklist task) to a parent task.
* @param task The task to add
* @returns The subtask that was added
*/
addTask(task: TaskDetailsModel): Observable<TaskDetailsModel> {
return from(this.callApiAddTask(task))
.pipe(
map((response: TaskDetailsModel) => {
return new TaskDetailsModel(response);
}),
catchError((err) => this.handleError(err))
);
}
/**
* Deletes a subtask (ie, a checklist task) from a parent task.
* @param taskId The task to delete
* @returns Null response notifying when the operation is complete
*/
deleteTask(taskId: string): Observable<TaskDetailsModel> {
return from(this.callApiDeleteTask(taskId))
.pipe(
catchError((err) => this.handleError(err))
);
}
/**
* Deletes a form from a task.
* @param taskId Task id related to form
* @returns Null response notifying when the operation is complete
*/
deleteForm(taskId: string): Observable<TaskDetailsModel> {
return from(this.callApiDeleteForm(taskId))
.pipe(
catchError((err) => this.handleError(err))
);
}
/**
* Gives completed status to a task.
* @param taskId ID of the target task
* @returns Null response notifying when the operation is complete
*/
completeTask(taskId: string) {
return from(this.apiService.taskApi.completeTask(taskId))
.pipe(
catchError((err) => this.handleError(err))
);
}
/**
* Gets the total number of the tasks found by a query.
* @param requestNode Query to search for tasks
* @returns Number of tasks
*/
public getTotalTasks(requestNode: TaskQueryRequestRepresentationModel): Observable<any> {
requestNode.size = 0;
return from(this.callApiTasksFiltered(requestNode))
.pipe(
map((res: any) => {
return res;
}),
catchError((err) => this.handleError(err))
);
}
/**
* Creates a new standalone task.
* @param task Details of the new task
* @returns Details of the newly created task
*/
createNewTask(task: TaskDetailsModel): Observable<TaskDetailsModel> {
return from(this.callApiCreateTask(task))
.pipe(
map((response: TaskDetailsModel) => {
return new TaskDetailsModel(response);
}),
catchError((err) => this.handleError(err))
);
}
/**
* Assigns a task to a user or group.
* @param taskId The task to assign
* @param requestNode User or group to assign the task to
* @returns Details of the assigned task
*/
assignTask(taskId: string, requestNode: any): Observable<TaskDetailsModel> {
const assignee = { assignee: requestNode.id };
return from(this.callApiAssignTask(taskId, assignee))
.pipe(
map((response: TaskDetailsModel) => {
return new TaskDetailsModel(response);
}),
catchError((err) => this.handleError(err))
);
}
/**
* Assigns a task to a user.
* @param taskId ID of the task to assign
* @param userId ID of the user to assign the task to
* @returns Details of the assigned task
*/
assignTaskByUserId(taskId: string, userId: string): Observable<TaskDetailsModel> {
const assignee = <AssigneeIdentifierRepresentation> { assignee: userId };
return from(this.callApiAssignTask(taskId, assignee))
.pipe(
map((response: TaskDetailsModel) => {
return new TaskDetailsModel(response);
}),
catchError((err) => this.handleError(err))
);
}
/**
* Claims a task for the current user.
* @param taskId ID of the task to claim
* @returns Details of the claimed task
*/
claimTask(taskId: string): Observable<TaskDetailsModel> {
return from(this.apiService.taskApi.claimTask(taskId))
.pipe(
catchError((err) => this.handleError(err))
);
}
/**
* Un-claims a task for the current user.
* @param taskId ID of the task to unclaim
* @returns Null response notifying when the operation is complete
*/
unclaimTask(taskId: string): Observable<TaskDetailsModel> {
return from(this.apiService.taskApi.unclaimTask(taskId))
.pipe(
catchError((err) => this.handleError(err))
);
}
/**
* Updates the details (name, description, due date) for a task.
* @param taskId ID of the task to update
* @param updated Data to update the task (as a `TaskUpdateRepresentation` instance).
* @returns Updated task details
*/
updateTask(taskId: any, updated): Observable<TaskDetailsModel> {
return from(this.apiService.taskApi.updateTask(taskId, updated))
.pipe(
map((result) => <TaskDetailsModel> result),
catchError((err) => this.handleError(err))
);
}
/**
* Fetches the Task Audit information in PDF format.
* @param taskId ID of the target task
* @returns Binary PDF data
*/
fetchTaskAuditPdfById(taskId: string): Observable<Blob> {
return from(this.apiService.taskApi.getTaskAuditPdf(taskId))
.pipe(
map((data) => <Blob> data),
catchError((err) => this.handleError(err))
);
}
/**
* Fetch the Task Audit information in JSON format
* @param taskId ID of the target task
* @returns JSON data
*/
fetchTaskAuditJsonById(taskId: string): Observable<any> {
return from(this.apiService.taskApi.getTaskAuditJson(taskId))
.pipe(
catchError((err) => this.handleError(err))
);
}
private callApiTasksFiltered(requestNode: TaskQueryRepresentation): Promise<TaskListModel> {
return this.apiService.taskApi.listTasks(requestNode);
}
private callApiTaskDetails(taskId: string): Promise<TaskDetailsModel> {
return this.apiService.taskApi.getTask(taskId);
}
private callApiAddTask(task: TaskDetailsModel): Promise<TaskDetailsModel> {
return this.apiService.taskApi.addSubtask(task.parentTaskId, task);
}
private callApiDeleteTask(taskId: string): Promise<any> {
return this.apiService.taskApi.deleteTask(taskId);
}
private callApiDeleteForm(taskId: string): Promise<any> {
return this.apiService.taskApi.removeForm(taskId);
}
private callApiTaskChecklist(taskId: string): Promise<TaskListModel> {
return this.apiService.taskApi.getChecklist(taskId);
}
private callApiCreateTask(task: TaskDetailsModel): Promise<TaskDetailsModel> {
return this.apiService.taskApi.createNewTask(task);
}
private callApiAssignTask(taskId: string, requestNode: AssigneeIdentifierRepresentation): Promise<TaskDetailsModel> {
return this.apiService.taskApi.assignTask(taskId, requestNode);
}
private handleError(error: any) {
this.logService.error(error);
return throwError(error || 'Server error');
}
}

View File

@@ -0,0 +1,80 @@
/*!
* @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 { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FlexLayoutModule } from '@angular/flex-layout';
import { CoreModule } from '@alfresco/adf-core';
import { ProcessCommentsModule } from '../process-comments/process-comments.module';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MaterialModule } from '../material.module';
import { PeopleModule } from '../people/people.module';
import { ContentWidgetModule } from '../content-widget/content-widget.module';
import { ChecklistComponent } from './components/checklist.component';
import { NoTaskDetailsTemplateDirective } from './components/no-task-detail-template.directive';
import { StartTaskComponent } from './components/start-task.component';
import { TaskAuditDirective } from './components/task-audit.directive';
import { TaskDetailsComponent } from './components/task-details.component';
import { TaskFiltersComponent } from './components/task-filters.component';
import { TaskHeaderComponent } from './components/task-header.component';
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';
@NgModule({
imports: [
CommonModule,
FlexLayoutModule,
MaterialModule,
FormsModule,
FormModule,
ReactiveFormsModule,
CoreModule,
PeopleModule,
ProcessCommentsModule,
ContentWidgetModule
],
declarations: [
NoTaskDetailsTemplateDirective,
TaskFiltersComponent,
TaskListComponent,
TaskDetailsComponent,
TaskAuditDirective,
ChecklistComponent,
TaskHeaderComponent,
StartTaskComponent,
TaskStandaloneComponent,
AttachFormComponent
],
exports: [
NoTaskDetailsTemplateDirective,
TaskFiltersComponent,
TaskListComponent,
TaskDetailsComponent,
TaskAuditDirective,
ChecklistComponent,
TaskHeaderComponent,
StartTaskComponent,
TaskStandaloneComponent,
AttachFormComponent
]
})
export class TaskListModule {
}

View File

@@ -0,0 +1,29 @@
/*!
* @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 { CardViewItemValidator } from '@alfresco/adf-core';
export class TaskDescriptionValidator implements CardViewItemValidator {
message: string = 'ADF_CLOUD_TASK_HEADER.FORM_VALIDATION.INVALID_FIELD';
isValid(value: any): boolean {
const isWhitespace = (value || '').trim().length === 0;
return value.length === 0 || !isWhitespace;
}
}