mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2025-10-08 14:51:32 +00:00
[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:
committed by
Eugenio Romano
parent
90b2cee70d
commit
cd7e21a23d
@@ -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>
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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();
|
||||
});
|
||||
}));
|
||||
});
|
@@ -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');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@@ -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>
|
@@ -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;
|
||||
}
|
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
@@ -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 = '';
|
||||
}
|
||||
}
|
@@ -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);
|
||||
});
|
||||
|
||||
});
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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>
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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();
|
||||
});
|
||||
});
|
@@ -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');
|
||||
}
|
||||
}
|
@@ -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();
|
||||
|
||||
}));
|
||||
|
||||
});
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
@@ -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>
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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>
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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>
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -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');
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
@@ -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` : '';
|
||||
}
|
||||
}
|
@@ -0,0 +1,4 @@
|
||||
.adf-task-list-loading-margin {
|
||||
margin-left: calc((100% - 100px) / 2);
|
||||
margin-right: calc((100% - 100px) / 2);
|
||||
}
|
@@ -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>
|
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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>
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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();
|
||||
});
|
||||
}));
|
||||
});
|
@@ -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();
|
||||
}
|
||||
}
|
18
lib/process-services/src/lib/task-list/index.ts
Normal file
18
lib/process-services/src/lib/task-list/index.ts
Normal 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';
|
@@ -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 {
|
||||
|
||||
}
|
30
lib/process-services/src/lib/task-list/models/form.model.ts
Normal file
30
lib/process-services/src/lib/task-list/models/form.model.ts
Normal 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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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
|
||||
}
|
||||
]
|
||||
};
|
@@ -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 || {};
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
43
lib/process-services/src/lib/task-list/public-api.ts
Normal file
43
lib/process-services/src/lib/task-list/public-api.ts
Normal 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';
|
@@ -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');
|
||||
}
|
||||
|
||||
}
|
@@ -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' }
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
@@ -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');
|
||||
}
|
||||
|
||||
}
|
@@ -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');
|
||||
}
|
||||
|
||||
}
|
@@ -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)
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@@ -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');
|
||||
}
|
||||
|
||||
}
|
80
lib/process-services/src/lib/task-list/task-list.module.ts
Normal file
80
lib/process-services/src/lib/task-list/task-list.module.ts
Normal 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 {
|
||||
}
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user