mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2025-07-24 17:32:15 +00:00
@@ -0,0 +1,170 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2016 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 { Observable } from 'rxjs/Rx';
|
||||
import { ProcessService } from './../services/process.service';
|
||||
import { ProcessAuditDirective } from './process-audit.directive';
|
||||
|
||||
declare let jasmine: any;
|
||||
|
||||
describe('ProcessAuditDirective', () => {
|
||||
|
||||
let fixture: ComponentFixture<BasicButtonComponent>;
|
||||
let component: BasicButtonComponent;
|
||||
let service: ProcessService;
|
||||
|
||||
function createFakePdfBlob(): Blob {
|
||||
let 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'});
|
||||
}
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [BasicButtonComponent, ProcessAuditDirective],
|
||||
providers: [ProcessService]
|
||||
});
|
||||
|
||||
TestBed.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(BasicButtonComponent);
|
||||
component = fixture.componentInstance;
|
||||
service = TestBed.get(ProcessService);
|
||||
|
||||
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';
|
||||
let blob = createFakePdfBlob();
|
||||
spyOn(service, 'fetchProcessAuditPdfById').and.returnValue(Observable.of(blob));
|
||||
spyOn(component, 'onAuditClick').and.callThrough();
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
let 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 = {
|
||||
processInstanceId: 42516, processInstanceName: 'Fake Process - August 3rd 2017',
|
||||
processDefinitionName: 'Claim Approval Process', processDefinitionVersion: 1, processInstanceStartTime: 'Thu Aug 03 15:32:47 UTC 2017', processInstanceEndTime: null,
|
||||
processInstanceDurationInMillis: null,
|
||||
processInstanceInitiator: 'MyName MyLastname',
|
||||
entries: [{
|
||||
index: 1, type: 'startForm',
|
||||
selectedOutcome: null, formData: [{
|
||||
fieldName: 'User Name',
|
||||
fieldId: 'username', value: 'dsassd'
|
||||
},
|
||||
{ fieldName: 'Claim Amount', fieldId: 'claimamount', value: '22' }], taskName: null, taskAssignee: null, activityId: null,
|
||||
activityName: null, activityType: null, startTime: null, endTime: null, durationInMillis: null
|
||||
}
|
||||
], decisionInfo: { calculatedValues: [], appliedRules: [] }
|
||||
};
|
||||
spyOn(service, 'fetchProcessAuditJsonById').and.returnValue(Observable.of(auditJson));
|
||||
spyOn(component, 'onAuditClick').and.callThrough();
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
let 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 UNKNOW', fakeAsync(() => {
|
||||
component.fileName = 'FakeAuditName';
|
||||
component.format = 'fakeFormat';
|
||||
let blob = createFakePdfBlob();
|
||||
spyOn(service, 'fetchProcessAuditPdfById').and.returnValue(Observable.of(blob));
|
||||
spyOn(component, 'onAuditClick').and.callThrough();
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
let button = fixture.nativeElement.querySelector('#auditButton');
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
expect(component.onAuditClick).toHaveBeenCalledWith({ format: 'pdf', value: blob, fileName: 'FakeAuditName' });
|
||||
});
|
||||
|
||||
button.click();
|
||||
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
@Component({
|
||||
selector: 'adf-basic-button',
|
||||
template: `
|
||||
<button id="auditButton"
|
||||
adf-process-audit
|
||||
[process-id]="currentProcessId"
|
||||
[download]="download"
|
||||
[fileName]="fileName"
|
||||
[format]="format"
|
||||
(clicked)="onAuditClick($event)">My button
|
||||
</button>`
|
||||
})
|
||||
class BasicButtonComponent {
|
||||
|
||||
download: boolean = false;
|
||||
fileName: string;
|
||||
format: string;
|
||||
constructor() {
|
||||
|
||||
}
|
||||
|
||||
onAuditClick(event: any) {
|
||||
}
|
||||
}
|
@@ -0,0 +1,120 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2016 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 { ContentService } from '@alfresco/core';
|
||||
import { Directive, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
|
||||
import { ProcessService } from './../services/process.service';
|
||||
|
||||
const JSON_FORMAT: string = 'json';
|
||||
const PDF_FORMAT: string = 'pdf';
|
||||
|
||||
@Directive({
|
||||
selector: 'button[adf-process-audit]',
|
||||
host: {
|
||||
'role': 'button',
|
||||
'(click)': 'onClickAudit()'
|
||||
}
|
||||
})
|
||||
export class ProcessAuditDirective implements OnChanges {
|
||||
|
||||
@Input('process-id')
|
||||
processId: string;
|
||||
|
||||
@Input()
|
||||
fileName: string = 'Audit';
|
||||
|
||||
@Input()
|
||||
format: string = 'pdf';
|
||||
|
||||
@Input()
|
||||
download: boolean = true;
|
||||
|
||||
@Output()
|
||||
clicked: EventEmitter<any> = new EventEmitter<any>();
|
||||
|
||||
@Output()
|
||||
error: EventEmitter<any> = new EventEmitter<any>();
|
||||
|
||||
public audit: any;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param translateService
|
||||
* @param processListService
|
||||
*/
|
||||
constructor(private contentService: ContentService,
|
||||
private processListService: ProcessService) {
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): 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.processListService.fetchProcessAuditPdfById(this.processId).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.processListService.fetchProcessAuditJsonById(this.processId).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,9 @@
|
||||
<div class="menu-container">
|
||||
<mat-list>
|
||||
<mat-list-item (click)="selectFilter(filter)" *ngFor="let filter of filters"
|
||||
class="adf-filters__entry" [class.active]="currentFilter === filter">
|
||||
<mat-icon *ngIf="hasIcon" matListIcon class="adf-filters__entry-icon">assignment</mat-icon>
|
||||
<span matLine [attr.data-automation-id]="filter.name + '_filter'">{{filter.name}}</span>
|
||||
</mat-list-item>
|
||||
</mat-list>
|
||||
</div>
|
@@ -0,0 +1,29 @@
|
||||
@mixin adf-process-filters-theme($theme) {
|
||||
$primary: map-get($theme, primary);
|
||||
|
||||
.adf {
|
||||
|
||||
&-filters__entry {
|
||||
cursor: pointer;
|
||||
font-size: 14px!important;
|
||||
font-weight: bold;
|
||||
opacity: .54;
|
||||
padding-left: 30px;
|
||||
|
||||
.mat-list-item-content {
|
||||
height: 34px;
|
||||
}
|
||||
&.active, &:hover {
|
||||
color: mat-color($primary);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&-filters__entry-icon {
|
||||
padding-right: 12px !important;
|
||||
padding-left: 0px !important;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,210 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2016 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 { AppsProcessService } from '@alfresco/core';
|
||||
import { Observable } from 'rxjs/Rx';
|
||||
import { FilterProcessRepresentationModel } from '../models/filter-process.model';
|
||||
import { ProcessFilterService } from '../services/process-filter.service';
|
||||
import { ProcessFiltersComponent } from './process-filters.component';
|
||||
|
||||
describe('ActivitiFilters', () => {
|
||||
|
||||
let filterList: ProcessFiltersComponent;
|
||||
let processFilterService: ProcessFilterService;
|
||||
let appsProcessService: AppsProcessService;
|
||||
|
||||
let fakeGlobalFilter = [];
|
||||
fakeGlobalFilter.push(new FilterProcessRepresentationModel({
|
||||
name: 'FakeInvolvedTasks',
|
||||
filter: { state: 'open', assignment: 'fake-involved' }
|
||||
}));
|
||||
fakeGlobalFilter.push(new FilterProcessRepresentationModel({
|
||||
name: 'FakeMyTasks',
|
||||
filter: { state: 'open', assignment: 'fake-assignee' }
|
||||
}));
|
||||
|
||||
fakeGlobalFilter.push(new FilterProcessRepresentationModel({
|
||||
name: 'Running',
|
||||
filter: { state: 'open', assignment: 'fake-running' }
|
||||
}));
|
||||
|
||||
let fakeGlobalFilterPromise = new Promise(function (resolve, reject) {
|
||||
resolve(fakeGlobalFilter);
|
||||
});
|
||||
|
||||
let fakeErrorFilterList = {
|
||||
error: 'wrong request'
|
||||
};
|
||||
|
||||
let fakeErrorFilterPromise = new Promise(function (resolve, reject) {
|
||||
reject(fakeErrorFilterList);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
processFilterService = new ProcessFilterService(null, null);
|
||||
appsProcessService = new AppsProcessService(null, null);
|
||||
filterList = new ProcessFiltersComponent(processFilterService, appsProcessService);
|
||||
});
|
||||
|
||||
it('should return the filter task list', (done) => {
|
||||
spyOn(processFilterService, 'getProcessFilters').and.returnValue(Observable.fromPromise(fakeGlobalFilterPromise));
|
||||
const appId = '1';
|
||||
let change = new SimpleChange(null, appId, true);
|
||||
filterList.ngOnChanges({ 'appId': change });
|
||||
|
||||
filterList.success.subscribe((res) => {
|
||||
expect(res).toBeDefined();
|
||||
expect(filterList.filters).toBeDefined();
|
||||
expect(filterList.filters.length).toEqual(3);
|
||||
expect(filterList.filters[0].name).toEqual('FakeInvolvedTasks');
|
||||
expect(filterList.filters[1].name).toEqual('FakeMyTasks');
|
||||
expect(filterList.filters[2].name).toEqual('Running');
|
||||
done();
|
||||
});
|
||||
|
||||
filterList.ngOnInit();
|
||||
});
|
||||
|
||||
it('should select the Running process filter', (done) => {
|
||||
spyOn(processFilterService, 'getProcessFilters').and.returnValue(Observable.fromPromise(fakeGlobalFilterPromise));
|
||||
const appId = '1';
|
||||
let change = new SimpleChange(null, appId, true);
|
||||
filterList.ngOnChanges({ 'appId': change });
|
||||
|
||||
expect(filterList.currentFilter).toBeUndefined();
|
||||
|
||||
filterList.success.subscribe((res) => {
|
||||
filterList.selectRunningFilter();
|
||||
expect(filterList.currentFilter.name).toEqual('Running');
|
||||
done();
|
||||
});
|
||||
|
||||
filterList.ngOnInit();
|
||||
});
|
||||
|
||||
it('should return the filter task list, filtered By Name', (done) => {
|
||||
|
||||
let fakeDeployedApplicationsPromise = new Promise(function (resolve, reject) {
|
||||
resolve({ id: 1 });
|
||||
});
|
||||
|
||||
spyOn(appsProcessService, 'getDeployedApplicationsByName').and.returnValue(Observable.fromPromise(fakeDeployedApplicationsPromise));
|
||||
spyOn(processFilterService, 'getProcessFilters').and.returnValue(Observable.fromPromise(fakeGlobalFilterPromise));
|
||||
|
||||
let change = new SimpleChange(null, 'test', true);
|
||||
filterList.ngOnChanges({ 'appName': change });
|
||||
|
||||
filterList.success.subscribe((res) => {
|
||||
let deployApp: any = appsProcessService.getDeployedApplicationsByName;
|
||||
expect(deployApp.calls.count()).toEqual(1);
|
||||
expect(res).toBeDefined();
|
||||
done();
|
||||
});
|
||||
|
||||
filterList.ngOnInit();
|
||||
});
|
||||
|
||||
it('should emit an error with a bad response', (done) => {
|
||||
spyOn(processFilterService, 'getProcessFilters').and.returnValue(Observable.fromPromise(fakeErrorFilterPromise));
|
||||
|
||||
const appId = '1';
|
||||
let change = new SimpleChange(null, appId, true);
|
||||
filterList.ngOnChanges({ 'appId': change });
|
||||
|
||||
filterList.error.subscribe((err) => {
|
||||
expect(err).toBeDefined();
|
||||
done();
|
||||
});
|
||||
|
||||
filterList.ngOnInit();
|
||||
});
|
||||
|
||||
it('should emit an error with a bad response', (done) => {
|
||||
spyOn(appsProcessService, 'getDeployedApplicationsByName').and.returnValue(Observable.fromPromise(fakeErrorFilterPromise));
|
||||
|
||||
const appId = 'fake-app';
|
||||
let change = new SimpleChange(null, appId, true);
|
||||
filterList.ngOnChanges({ 'appName': change });
|
||||
|
||||
filterList.error.subscribe((err) => {
|
||||
expect(err).toBeDefined();
|
||||
done();
|
||||
});
|
||||
|
||||
filterList.ngOnInit();
|
||||
});
|
||||
|
||||
it('should emit an event when a filter is selected', (done) => {
|
||||
let currentFilter = new FilterProcessRepresentationModel({
|
||||
filter: {
|
||||
state: 'open',
|
||||
assignment: 'fake-involved'
|
||||
}
|
||||
});
|
||||
|
||||
filterList.filterClick.subscribe((filter: FilterProcessRepresentationModel) => {
|
||||
expect(filter).toBeDefined();
|
||||
expect(filter).toEqual(currentFilter);
|
||||
expect(filterList.currentFilter).toEqual(currentFilter);
|
||||
done();
|
||||
});
|
||||
|
||||
filterList.selectFilter(currentFilter);
|
||||
});
|
||||
|
||||
it('should reload filters by appId on binding changes', () => {
|
||||
spyOn(filterList, 'getFiltersByAppId').and.stub();
|
||||
const appId = '1';
|
||||
|
||||
let change = new SimpleChange(null, appId, true);
|
||||
filterList.ngOnChanges({ 'appId': change });
|
||||
|
||||
expect(filterList.getFiltersByAppId).toHaveBeenCalledWith(appId);
|
||||
});
|
||||
|
||||
it('should reload filters by appId null on binding changes', () => {
|
||||
spyOn(filterList, 'getFiltersByAppId').and.stub();
|
||||
const appId = null;
|
||||
|
||||
let change = new SimpleChange(null, appId, true);
|
||||
filterList.ngOnChanges({ 'appId': change });
|
||||
|
||||
expect(filterList.getFiltersByAppId).toHaveBeenCalledWith(appId);
|
||||
});
|
||||
|
||||
it('should reload filters by app name on binding changes', () => {
|
||||
spyOn(filterList, 'getFiltersByAppName').and.stub();
|
||||
const appName = 'fake-app-name';
|
||||
|
||||
let change = new SimpleChange(null, appName, true);
|
||||
filterList.ngOnChanges({ 'appName': change });
|
||||
|
||||
expect(filterList.getFiltersByAppName).toHaveBeenCalledWith(appName);
|
||||
});
|
||||
|
||||
it('should return the current filter after one is selected', () => {
|
||||
let filter = new FilterProcessRepresentationModel({
|
||||
name: 'FakeMyTasks',
|
||||
filter: { state: 'open', assignment: 'fake-assignee' }
|
||||
});
|
||||
expect(filterList.currentFilter).toBeUndefined();
|
||||
filterList.selectFilter(filter);
|
||||
expect(filterList.getCurrentFilter()).toBe(filter);
|
||||
});
|
||||
|
||||
});
|
@@ -0,0 +1,204 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2016 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/core';
|
||||
import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core';
|
||||
import { ProcessInstanceFilterRepresentation } from 'alfresco-js-api';
|
||||
import { Observable, Observer } from 'rxjs/Rx';
|
||||
import { FilterProcessRepresentationModel } from '../models/filter-process.model';
|
||||
import { ProcessFilterService } from './../services/process-filter.service';
|
||||
|
||||
@Component({
|
||||
selector: 'adf-process-instance-filters',
|
||||
templateUrl: './process-filters.component.html',
|
||||
styleUrls: ['process-filters.component.scss']
|
||||
})
|
||||
export class ProcessFiltersComponent implements OnInit, OnChanges {
|
||||
|
||||
@Input()
|
||||
filterParam: FilterProcessRepresentationModel;
|
||||
|
||||
@Output()
|
||||
filterClick: EventEmitter<ProcessInstanceFilterRepresentation> = new EventEmitter<ProcessInstanceFilterRepresentation>();
|
||||
|
||||
@Output()
|
||||
success: EventEmitter<ProcessInstanceFilterRepresentation[]> = new EventEmitter<ProcessInstanceFilterRepresentation[]>();
|
||||
|
||||
@Output()
|
||||
error: EventEmitter<any> = new EventEmitter<any>();
|
||||
|
||||
@Input()
|
||||
appId: number;
|
||||
|
||||
@Input()
|
||||
appName: string;
|
||||
|
||||
@Input()
|
||||
showIcon: boolean = true;
|
||||
|
||||
private filterObserver: Observer<ProcessInstanceFilterRepresentation>;
|
||||
filter$: Observable<ProcessInstanceFilterRepresentation>;
|
||||
|
||||
currentFilter: ProcessInstanceFilterRepresentation;
|
||||
|
||||
filters: ProcessInstanceFilterRepresentation [] = [];
|
||||
|
||||
constructor(private processFilterService: ProcessFilterService, private appsProcessService: AppsProcessService) {
|
||||
this.filter$ = new Observable<ProcessInstanceFilterRepresentation>(observer => this.filterObserver = observer).share();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.filter$.subscribe((filter: ProcessInstanceFilterRepresentation) => {
|
||||
this.filters.push(filter);
|
||||
});
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
let appId = changes['appId'];
|
||||
if (appId && (appId.currentValue || appId.currentValue === null)) {
|
||||
this.getFiltersByAppId(appId.currentValue);
|
||||
return;
|
||||
}
|
||||
let appName = changes['appName'];
|
||||
if (appName && appName.currentValue) {
|
||||
this.getFiltersByAppName(appName.currentValue);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the filter list filtered by appId
|
||||
* @param appId - optional
|
||||
*/
|
||||
getFiltersByAppId(appId?: number) {
|
||||
this.processFilterService.getProcessFilters(appId).subscribe(
|
||||
(res: ProcessInstanceFilterRepresentation[]) => {
|
||||
if (res.length === 0 && this.isFilterListEmpty()) {
|
||||
this.processFilterService.createDefaultFilters(appId).subscribe(
|
||||
(resDefault: ProcessInstanceFilterRepresentation[]) => {
|
||||
this.resetFilter();
|
||||
resDefault.forEach((filter) => {
|
||||
this.filterObserver.next(filter);
|
||||
});
|
||||
|
||||
this.selectProcessFilter(this.filterParam);
|
||||
this.success.emit(resDefault);
|
||||
},
|
||||
(errDefault: any) => {
|
||||
this.error.emit(errDefault);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
this.resetFilter();
|
||||
res.forEach((filter) => {
|
||||
this.filterObserver.next(filter);
|
||||
});
|
||||
|
||||
this.selectProcessFilter(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);
|
||||
this.selectProcessFilter(this.filterParam);
|
||||
},
|
||||
(err) => {
|
||||
this.error.emit(err);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Pass the selected filter as next
|
||||
* @param filter
|
||||
*/
|
||||
public selectFilter(filter: ProcessInstanceFilterRepresentation) {
|
||||
this.currentFilter = filter;
|
||||
this.filterClick.emit(filter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Select the first filter of a list if present
|
||||
*/
|
||||
public selectProcessFilter(filterParam: FilterProcessRepresentationModel) {
|
||||
if (filterParam) {
|
||||
this.filters.filter((processFilter: ProcessInstanceFilterRepresentation, index) => {
|
||||
if (filterParam.name && filterParam.name.toLowerCase() === processFilter.name.toLowerCase() || filterParam.index === index) {
|
||||
this.currentFilter = processFilter;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (this.isCurrentFilterEmpty()) {
|
||||
this.selectDefaultTaskFilter();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Select the Running filter
|
||||
*/
|
||||
public selectRunningFilter() {
|
||||
this.selectProcessFilter(this.processFilterService.getRunningFilterInstance(null));
|
||||
}
|
||||
|
||||
/**
|
||||
* Select as default task filter the first in the list
|
||||
*/
|
||||
public selectDefaultTaskFilter() {
|
||||
if (!this.isFilterListEmpty()) {
|
||||
this.currentFilter = this.filters[0];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current task
|
||||
* @returns {ProcessInstanceFilterRepresentation}
|
||||
*/
|
||||
getCurrentFilter(): ProcessInstanceFilterRepresentation {
|
||||
return this.currentFilter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the filter list is empty
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isFilterListEmpty(): boolean {
|
||||
return this.filters === undefined || (this.filters && this.filters.length === 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the filters properties
|
||||
*/
|
||||
private resetFilter() {
|
||||
this.filters = [];
|
||||
this.currentFilter = undefined;
|
||||
}
|
||||
|
||||
private isCurrentFilterEmpty(): boolean {
|
||||
return this.currentFilter === undefined || null ? true : false;
|
||||
}
|
||||
}
|
@@ -0,0 +1,21 @@
|
||||
:host {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.activiti-process-container {
|
||||
width: 100%;
|
||||
min-height: 100px;
|
||||
overflow: visible;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.adf-comments-dialog {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
transform: translate(0, -50%);
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.adf-in-medias-res-button {
|
||||
margin: 16px 0;
|
||||
}
|
@@ -0,0 +1,37 @@
|
||||
<div *ngIf="!processInstanceDetails">{{ 'ADF_PROCESS_LIST.DETAILS.MESSAGES.NONE'|translate }}</div>
|
||||
<mat-card *ngIf="processInstanceDetails">
|
||||
<mat-card-header>
|
||||
<mat-card-title>{{ getProcessNameOrDescription('medium') }}</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<adf-process-instance-header
|
||||
[processInstance]="processInstanceDetails"
|
||||
(showProcessDiagram)="onShowProcessDiagram($event)">
|
||||
</adf-process-instance-header>
|
||||
|
||||
<button class="adf-in-medias-res-button" mat-button id="show-diagram-button" type="button" mat-button mat-raised-button [disabled]="isDiagramDisabled()" (click)="onShowProcessDiagram(processInstanceId)">{{ 'ADF_PROCESS_LIST.DETAILS.BUTTON.SHOW_DIAGRAM' | translate }}</button>
|
||||
|
||||
<mat-card>
|
||||
<mat-card-content>
|
||||
<adf-process-instance-tasks
|
||||
[processInstanceDetails]="processInstanceDetails"
|
||||
(taskClick)="onTaskClicked($event)">
|
||||
</adf-process-instance-tasks>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
||||
<div data-automation-id="header-status" *ngIf="isRunning()" class="adf-in-medias-res-button">
|
||||
<button mat-button type="button" (click)="cancelProcess()">{{ 'ADF_PROCESS_LIST.DETAILS.BUTTON.CANCEL' | translate }}</button>
|
||||
</div>
|
||||
|
||||
<mat-card>
|
||||
<mat-card-content>
|
||||
<adf-process-instance-comments #activiticomments
|
||||
[readOnly]="false"
|
||||
[processInstanceId]="processInstanceDetails.id">
|
||||
</adf-process-instance-comments>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
||||
</mat-card-content>
|
||||
</mat-card>
|
@@ -0,0 +1,145 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2016 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 { DebugElement, NO_ERRORS_SCHEMA, SimpleChange } from '@angular/core';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { MaterialModule } from '../../material.module';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { Observable } from 'rxjs/Rx';
|
||||
|
||||
import { FormModule, FormService } from '@alfresco/core';
|
||||
import { TaskListModule } from '../../task-list';
|
||||
|
||||
import { ProcessInstance } from '../models/process-instance.model';
|
||||
import { exampleProcess, exampleProcessNoName } from './../../mock';
|
||||
import { ProcessService } from './../services/process.service';
|
||||
import { ProcessInstanceDetailsComponent } from './process-instance-details.component';
|
||||
|
||||
describe('ProcessInstanceDetailsComponent', () => {
|
||||
|
||||
let service: ProcessService;
|
||||
let formService: FormService;
|
||||
let component: ProcessInstanceDetailsComponent;
|
||||
let fixture: ComponentFixture<ProcessInstanceDetailsComponent>;
|
||||
let getProcessSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
MaterialModule,
|
||||
FormModule,
|
||||
TaskListModule
|
||||
],
|
||||
declarations: [
|
||||
ProcessInstanceDetailsComponent
|
||||
],
|
||||
providers: [
|
||||
ProcessService
|
||||
],
|
||||
schemas: [ NO_ERRORS_SCHEMA ]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
fixture = TestBed.createComponent(ProcessInstanceDetailsComponent);
|
||||
component = fixture.componentInstance;
|
||||
service = fixture.debugElement.injector.get(ProcessService);
|
||||
formService = fixture.debugElement.injector.get(FormService);
|
||||
|
||||
getProcessSpy = spyOn(service, 'getProcess').and.returnValue(Observable.of(exampleProcess));
|
||||
});
|
||||
|
||||
it('should not load task details when no processInstanceId is specified', () => {
|
||||
fixture.detectChanges();
|
||||
expect(getProcessSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should set a placeholder message when processInstanceId not initialised', () => {
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.innerText).toBe('ADF_PROCESS_LIST.DETAILS.MESSAGES.NONE');
|
||||
});
|
||||
|
||||
it('should display a header when the processInstanceId is provided', async(() => {
|
||||
fixture.detectChanges();
|
||||
component.ngOnChanges({ 'processInstanceId': new SimpleChange(null, '123', true) });
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
let headerEl: DebugElement = fixture.debugElement.query(By.css('.mat-card-title '));
|
||||
expect(headerEl).not.toBeNull();
|
||||
expect(headerEl.nativeElement.innerText).toBe('Process 123');
|
||||
});
|
||||
}));
|
||||
|
||||
it('should display default details when the process instance has no name', async(() => {
|
||||
getProcessSpy = getProcessSpy.and.returnValue(Observable.of(exampleProcessNoName));
|
||||
fixture.detectChanges();
|
||||
component.ngOnChanges({ 'processInstanceId': new SimpleChange(null, '123', true) });
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
let headerEl: DebugElement = fixture.debugElement.query(By.css('.mat-card-title '));
|
||||
expect(headerEl).not.toBeNull();
|
||||
expect(headerEl.nativeElement.innerText).toBe('My Process - Nov 10, 2016, 3:37:30 AM');
|
||||
});
|
||||
}));
|
||||
|
||||
describe('change detection', () => {
|
||||
|
||||
let change = new SimpleChange('123', '456', true);
|
||||
let nullChange = new SimpleChange('123', null, true);
|
||||
|
||||
beforeEach(async(() => {
|
||||
component.processInstanceId = '123';
|
||||
fixture.detectChanges();
|
||||
component.tasksList = jasmine.createSpyObj('tasksList', ['load']);
|
||||
fixture.whenStable().then(() => {
|
||||
getProcessSpy.calls.reset();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should fetch new process details when processInstanceId changed', () => {
|
||||
component.ngOnChanges({ 'processInstanceId': change });
|
||||
expect(getProcessSpy).toHaveBeenCalledWith('456');
|
||||
});
|
||||
|
||||
it('should NOT fetch new process details when empty changeset made', () => {
|
||||
component.ngOnChanges({});
|
||||
expect(getProcessSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should NOT fetch new process details when processInstanceId changed to null', () => {
|
||||
component.ngOnChanges({ 'processInstanceId': nullChange });
|
||||
expect(getProcessSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should set a placeholder message when processInstanceId changed to null', () => {
|
||||
component.ngOnChanges({ 'processInstanceId': nullChange });
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.innerText).toBe('ADF_PROCESS_LIST.DETAILS.MESSAGES.NONE');
|
||||
});
|
||||
|
||||
it('should display cancel button if process is running', () => {
|
||||
component.processInstanceDetails = new ProcessInstance({
|
||||
ended : null
|
||||
});
|
||||
fixture.detectChanges();
|
||||
let buttonEl = fixture.debugElement.query(By.css('[data-automation-id="header-status"] button'));
|
||||
expect(buttonEl).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
@@ -0,0 +1,146 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2016 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 } from '@alfresco/core';
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges, ViewChild } from '@angular/core';
|
||||
import { TaskDetailsEvent } from '../../task-list';
|
||||
|
||||
import { ProcessInstance } from '../models/process-instance.model';
|
||||
import { ProcessService } from './../services/process.service';
|
||||
import { ProcessInstanceHeaderComponent } from './process-instance-header.component';
|
||||
import { ProcessInstanceTasksComponent } from './process-instance-tasks.component';
|
||||
|
||||
@Component({
|
||||
selector: 'adf-process-instance-details',
|
||||
templateUrl: './process-instance-details.component.html',
|
||||
styleUrls: ['./process-instance-details.component.css']
|
||||
})
|
||||
export class ProcessInstanceDetailsComponent implements OnChanges {
|
||||
|
||||
@Input()
|
||||
processInstanceId: string;
|
||||
|
||||
@ViewChild(ProcessInstanceHeaderComponent)
|
||||
processInstanceHeader: ProcessInstanceHeaderComponent;
|
||||
|
||||
@ViewChild(ProcessInstanceTasksComponent)
|
||||
tasksList: ProcessInstanceTasksComponent;
|
||||
|
||||
@Input()
|
||||
showTitle: boolean = true;
|
||||
|
||||
@Input()
|
||||
showRefreshButton: boolean = true;
|
||||
|
||||
@Output()
|
||||
processCancelled: EventEmitter<any> = new EventEmitter<any>();
|
||||
|
||||
@Output()
|
||||
error: EventEmitter<any> = new EventEmitter<any>();
|
||||
|
||||
@Output()
|
||||
taskClick: EventEmitter<TaskDetailsEvent> = new EventEmitter<TaskDetailsEvent>();
|
||||
|
||||
processInstanceDetails: ProcessInstance;
|
||||
|
||||
@Output()
|
||||
showProcessDiagram: EventEmitter<any> = new EventEmitter<any>();
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param translate Translation service
|
||||
* @param activitiProcess Process service
|
||||
*/
|
||||
constructor(private activitiProcess: ProcessService,
|
||||
private logService: LogService) {
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
let processInstanceId = changes['processInstanceId'];
|
||||
if (processInstanceId && !processInstanceId.currentValue) {
|
||||
this.reset();
|
||||
return;
|
||||
}
|
||||
if (processInstanceId && processInstanceId.currentValue) {
|
||||
this.load(processInstanceId.currentValue);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the task detail to undefined
|
||||
*/
|
||||
reset() {
|
||||
this.processInstanceDetails = null;
|
||||
}
|
||||
|
||||
load(processId: string) {
|
||||
if (processId) {
|
||||
this.activitiProcess.getProcess(processId).subscribe(
|
||||
(res: ProcessInstance) => {
|
||||
this.processInstanceDetails = res;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
isRunning(): boolean {
|
||||
return this.processInstanceDetails && !this.processInstanceDetails.ended;
|
||||
}
|
||||
|
||||
isDiagramDisabled(): boolean {
|
||||
return !this.isRunning() ? true : undefined;
|
||||
}
|
||||
|
||||
cancelProcess() {
|
||||
this.activitiProcess.cancelProcess(this.processInstanceId).subscribe(
|
||||
(data) => {
|
||||
this.processCancelled.emit(data);
|
||||
}, (err) => {
|
||||
this.error.emit(err);
|
||||
});
|
||||
}
|
||||
|
||||
// bubbles (taskClick) event
|
||||
onTaskClicked(event: TaskDetailsEvent) {
|
||||
this.taskClick.emit(event);
|
||||
}
|
||||
|
||||
getProcessNameOrDescription(dateFormat): string {
|
||||
let name = '';
|
||||
if (this.processInstanceDetails) {
|
||||
name = this.processInstanceDetails.name ||
|
||||
this.processInstanceDetails.processDefinitionName + ' - ' + this.getFormatDate(this.processInstanceDetails.started, dateFormat);
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
getFormatDate(value, format: string) {
|
||||
let datePipe = new DatePipe('en-US');
|
||||
try {
|
||||
return datePipe.transform(value, format);
|
||||
} catch (err) {
|
||||
this.logService.error(`ProcessListInstanceHeader: error parsing date ${value} to format ${format}`);
|
||||
}
|
||||
}
|
||||
|
||||
onShowProcessDiagram(processInstanceId: any) {
|
||||
this.showProcessDiagram.emit({value: this.processInstanceId});
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
:host {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.adf-card-container {
|
||||
font-family: inherit;
|
||||
}
|
@@ -0,0 +1,5 @@
|
||||
<mat-card *ngIf="processInstance" class="adf-card-container">
|
||||
<mat-card-content>
|
||||
<adf-card-view [properties]="properties"></adf-card-view>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
@@ -0,0 +1,166 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2016 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 { CardViewUpdateService } from '@alfresco/core';
|
||||
import { MaterialModule } from '../../material.module';
|
||||
|
||||
import { ProcessInstance } from '../models/process-instance.model';
|
||||
import { exampleProcess } from '../../mock';
|
||||
import { ProcessService } from './../services/process.service';
|
||||
import { ProcessInstanceHeaderComponent } from './process-instance-header.component';
|
||||
|
||||
describe('ProcessInstanceHeaderComponent', () => {
|
||||
|
||||
let service: ProcessService;
|
||||
let component: ProcessInstanceHeaderComponent;
|
||||
let fixture: ComponentFixture<ProcessInstanceHeaderComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
MaterialModule
|
||||
],
|
||||
declarations: [
|
||||
ProcessInstanceHeaderComponent
|
||||
],
|
||||
providers: [
|
||||
ProcessService,
|
||||
CardViewUpdateService
|
||||
]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
fixture = TestBed.createComponent(ProcessInstanceHeaderComponent);
|
||||
component = fixture.componentInstance;
|
||||
service = TestBed.get(ProcessService);
|
||||
|
||||
component.processInstance = new ProcessInstance(exampleProcess);
|
||||
});
|
||||
|
||||
it('should render empty component if no process details provided', () => {
|
||||
component.processInstance = undefined;
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.children.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should display status as running when process is not complete', () => {
|
||||
component.processInstance.ended = null;
|
||||
component.ngOnChanges({});
|
||||
fixture.detectChanges();
|
||||
let valueEl = fixture.nativeElement.querySelector('[data-automation-id="card-textitem-value-status"]');
|
||||
expect(valueEl.innerText).toBe('Running');
|
||||
});
|
||||
|
||||
it('should display status as completed when process is complete', () => {
|
||||
component.processInstance.ended = new Date('2016-11-03');
|
||||
component.ngOnChanges({});
|
||||
fixture.detectChanges();
|
||||
let valueEl = fixture.nativeElement.querySelector('[data-automation-id="card-textitem-value-status"]');
|
||||
expect(valueEl.innerText).toBe('Completed');
|
||||
});
|
||||
|
||||
it('should display due date', () => {
|
||||
component.processInstance.ended = new Date('2016-11-03');
|
||||
component.ngOnChanges({});
|
||||
fixture.detectChanges();
|
||||
let valueEl = fixture.nativeElement.querySelector('[data-automation-id="card-dateitem-dueDate"]');
|
||||
expect(valueEl.innerText).toBe('Nov 03 2016');
|
||||
});
|
||||
|
||||
it('should display placeholder if no due date', () => {
|
||||
component.processInstance.ended = null;
|
||||
component.ngOnChanges({});
|
||||
fixture.detectChanges();
|
||||
let valueEl = fixture.nativeElement.querySelector('[data-automation-id="card-dateitem-dueDate"]');
|
||||
expect(valueEl.innerText).toBe('ADF_PROCESS_LIST.PROPERTIES.DUE_DATE_DEFAULT');
|
||||
});
|
||||
|
||||
it('should display process category', () => {
|
||||
component.processInstance.processDefinitionCategory = 'Accounts';
|
||||
component.ngOnChanges({});
|
||||
fixture.detectChanges();
|
||||
let valueEl = fixture.nativeElement.querySelector('[data-automation-id="card-textitem-value-category"]');
|
||||
expect(valueEl.innerText).toBe('Accounts');
|
||||
});
|
||||
|
||||
it('should display placeholder if no process category', () => {
|
||||
component.processInstance.processDefinitionCategory = null;
|
||||
component.ngOnChanges({});
|
||||
fixture.detectChanges();
|
||||
let valueEl = fixture.nativeElement.querySelector('[data-automation-id="card-textitem-value-category"]');
|
||||
expect(valueEl.innerText).toBe('ADF_PROCESS_LIST.PROPERTIES.CATEGORY_DEFAULT');
|
||||
});
|
||||
|
||||
it('should display created date', () => {
|
||||
component.processInstance.started = new Date('2016-11-03');
|
||||
component.ngOnChanges({});
|
||||
fixture.detectChanges();
|
||||
let valueEl = fixture.nativeElement.querySelector('[data-automation-id="card-dateitem-created"]');
|
||||
expect(valueEl.innerText).toBe('Nov 03 2016');
|
||||
});
|
||||
|
||||
it('should display started by', () => {
|
||||
component.processInstance.startedBy = {firstName: 'Admin', lastName: 'User'};
|
||||
component.ngOnChanges({});
|
||||
fixture.detectChanges();
|
||||
let valueEl = fixture.nativeElement.querySelector('[data-automation-id="card-textitem-value-assignee"]');
|
||||
expect(valueEl.innerText).toBe('Admin User');
|
||||
});
|
||||
|
||||
it('should display process instance id', () => {
|
||||
component.processInstance.id = '123';
|
||||
component.ngOnChanges({});
|
||||
fixture.detectChanges();
|
||||
let valueEl = fixture.nativeElement.querySelector('[data-automation-id="card-textitem-value-id"]');
|
||||
expect(valueEl.innerText).toBe('123');
|
||||
});
|
||||
|
||||
it('should display description', () => {
|
||||
component.processInstance.processDefinitionDescription = 'Test process';
|
||||
component.ngOnChanges({});
|
||||
fixture.detectChanges();
|
||||
let valueEl = fixture.nativeElement.querySelector('[data-automation-id="card-textitem-value-description"]');
|
||||
expect(valueEl.innerText).toBe('Test process');
|
||||
});
|
||||
|
||||
it('should display placeholder if no description', () => {
|
||||
component.processInstance.processDefinitionDescription = null;
|
||||
component.ngOnChanges({});
|
||||
fixture.detectChanges();
|
||||
let valueEl = fixture.nativeElement.querySelector('[data-automation-id="card-textitem-value-description"]');
|
||||
expect(valueEl.innerText).toBe('ADF_PROCESS_LIST.PROPERTIES.DESCRIPTION_DEFAULT');
|
||||
});
|
||||
|
||||
it('should display businessKey value', () => {
|
||||
component.processInstance.businessKey = 'fakeBusinessKey';
|
||||
component.ngOnChanges({});
|
||||
fixture.detectChanges();
|
||||
let valueEl = fixture.nativeElement.querySelector('[data-automation-id="card-textitem-value-businessKey"]');
|
||||
expect(valueEl.innerText).toBe('fakeBusinessKey');
|
||||
});
|
||||
|
||||
it('should display default key if no businessKey', () => {
|
||||
component.processInstance.businessKey = null;
|
||||
component.ngOnChanges({});
|
||||
fixture.detectChanges();
|
||||
let valueEl = fixture.nativeElement.querySelector('[data-automation-id="card-textitem-value-businessKey"]');
|
||||
expect(valueEl.innerText).toBe('ADF_PROCESS_LIST.PROPERTIES.BUSINESS_KEY_DEFAULT');
|
||||
});
|
||||
});
|
@@ -0,0 +1,117 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2016 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 { CardViewDateItemModel, CardViewItem, CardViewTextItemModel } from '@alfresco/core';
|
||||
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { ProcessInstance } from '../models/process-instance.model';
|
||||
|
||||
@Component({
|
||||
selector: 'adf-process-instance-header',
|
||||
templateUrl: './process-instance-header.component.html',
|
||||
styleUrls: ['./process-instance-header.component.css']
|
||||
})
|
||||
export class ProcessInstanceHeaderComponent implements OnChanges {
|
||||
|
||||
@Input()
|
||||
processInstance: ProcessInstance;
|
||||
|
||||
properties: CardViewItem [];
|
||||
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
this.refreshData();
|
||||
}
|
||||
|
||||
refreshData() {
|
||||
if (this.processInstance) {
|
||||
this.properties = [
|
||||
new CardViewTextItemModel(
|
||||
{
|
||||
label: 'ADF_PROCESS_LIST.PROPERTIES.STATUS',
|
||||
value: this.getProcessStatus(),
|
||||
key: 'status'
|
||||
}),
|
||||
new CardViewDateItemModel(
|
||||
{
|
||||
label: 'ADF_PROCESS_LIST.PROPERTIES.DUE_DATE',
|
||||
value: this.processInstance.ended,
|
||||
format: 'MMM DD YYYY',
|
||||
key: 'dueDate',
|
||||
default: 'ADF_PROCESS_LIST.PROPERTIES.DUE_DATE_DEFAULT'
|
||||
}),
|
||||
new CardViewTextItemModel(
|
||||
{
|
||||
label: 'ADF_PROCESS_LIST.PROPERTIES.CATEGORY',
|
||||
value: this.processInstance.processDefinitionCategory,
|
||||
key: 'category',
|
||||
default: 'ADF_PROCESS_LIST.PROPERTIES.CATEGORY_DEFAULT'
|
||||
}),
|
||||
new CardViewTextItemModel(
|
||||
{
|
||||
label: 'ADF_PROCESS_LIST.PROPERTIES.BUSINESS_KEY',
|
||||
value: this.processInstance.businessKey,
|
||||
key: 'businessKey',
|
||||
default: 'ADF_PROCESS_LIST.PROPERTIES.BUSINESS_KEY_DEFAULT'
|
||||
}),
|
||||
new CardViewTextItemModel(
|
||||
{
|
||||
label: 'ADF_PROCESS_LIST.PROPERTIES.CREATED_BY',
|
||||
value: this.getStartedByFullName(),
|
||||
key: 'assignee',
|
||||
default: 'ADF_PROCESS_LIST.PROPERTIES.CREATED_BY_DEFAULT'
|
||||
}),
|
||||
new CardViewDateItemModel(
|
||||
{
|
||||
label: 'ADF_PROCESS_LIST.PROPERTIES.CREATED',
|
||||
value: this.processInstance.started,
|
||||
format: 'MMM DD YYYY',
|
||||
key: 'created'
|
||||
}),
|
||||
new CardViewTextItemModel(
|
||||
{label: 'ADF_PROCESS_LIST.PROPERTIES.ID',
|
||||
value: this.processInstance.id,
|
||||
key: 'id'
|
||||
}),
|
||||
new CardViewTextItemModel(
|
||||
{label: 'ADF_PROCESS_LIST.PROPERTIES.DESCRIPTION',
|
||||
value: this.processInstance.processDefinitionDescription,
|
||||
key: 'description',
|
||||
default: 'ADF_PROCESS_LIST.PROPERTIES.DESCRIPTION_DEFAULT'
|
||||
})
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
getProcessStatus(): string {
|
||||
if (this.processInstance) {
|
||||
return this.isRunning() ? 'Running' : 'Completed';
|
||||
}
|
||||
}
|
||||
|
||||
getStartedByFullName(): string {
|
||||
let fullName = '';
|
||||
if (this.processInstance && this.processInstance.startedBy) {
|
||||
fullName += this.processInstance.startedBy.firstName || '';
|
||||
fullName += fullName ? ' ' : '';
|
||||
fullName += this.processInstance.startedBy.lastName || '';
|
||||
}
|
||||
return fullName;
|
||||
}
|
||||
|
||||
isRunning(): boolean {
|
||||
return this.processInstance && !this.processInstance.ended;
|
||||
}
|
||||
}
|
@@ -0,0 +1,71 @@
|
||||
:host {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.activiti-label {
|
||||
font-weight: bolder;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.adf-process-badge {
|
||||
pointer-events: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.adf-chip-label {
|
||||
position: relative;
|
||||
top: 5px;
|
||||
margin-right: 8px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.menu-container {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.activiti-label + .icon {
|
||||
position: relative;
|
||||
top: -2px;
|
||||
}
|
||||
|
||||
.task-details-dialog {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
transform: translate(0, -50%);
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.process-tasks-refresh {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.adf-start-process-dialog {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.adf-start-process-dialog-content {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.adf-start-process-dialog-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.no-results {
|
||||
margin-left: 9px;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0;
|
||||
line-height: 18px;
|
||||
color: rgba(0, 0, 0, .54);
|
||||
display: block;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.process-tasks__task-item {
|
||||
cursor: pointer;
|
||||
}
|
@@ -0,0 +1,85 @@
|
||||
<div *ngIf="showRefreshButton" class="process-tasks-refresh" >
|
||||
<button mat-icon-button (click)="onRefreshClicked()">
|
||||
<mat-icon class="md-24" aria-label="Refresh">refresh</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- ACTIVE FORM -->
|
||||
|
||||
<mat-chip-list>
|
||||
<span class="adf-chip-label">{{ 'ADF_PROCESS_LIST.DETAILS.LABELS.TASKS_ACTIVE'|translate }}</span>
|
||||
<mat-chip class="adf-process-badge" color="accent" selected="true">{{activeTasks?.length}}</mat-chip>
|
||||
</mat-chip-list>
|
||||
|
||||
<div class="menu-container" *ngIf="activeTasks?.length > 0" data-automation-id="active-tasks">
|
||||
<mat-list>
|
||||
<mat-list-item class="process-tasks__task-item" *ngFor="let task of activeTasks" (click)="clickTask($event, task)">
|
||||
<mat-icon mat-list-icon>assignment</mat-icon>
|
||||
<h3 matLine>{{task.name || 'Nameless task'}}</h3>
|
||||
<span matLine>
|
||||
{{ 'ADF_PROCESS_LIST.DETAILS.LABELS.TASK_SUBTITLE' | translate:{user: getUserFullName(task.assignee), created: getFormatDate(task.created, 'mediumDate') } }}
|
||||
</span>
|
||||
</mat-list-item>
|
||||
</mat-list>
|
||||
</div>
|
||||
|
||||
<!-- START FORM -->
|
||||
|
||||
<div *ngIf="activeTasks?.length === 0" data-automation-id="active-tasks-none" class="no-results">
|
||||
{{ 'ADF_PROCESS_LIST.DETAILS.TASKS.NO_ACTIVE' | translate }}
|
||||
</div>
|
||||
|
||||
<div *ngIf="hasStartFormDefined()">
|
||||
<span class="activiti-label">{{ 'ADF_PROCESS_LIST.DETAILS.LABELS.START_FORM'|translate }}</span>
|
||||
|
||||
<!--IF START TASK COMPLETED -->
|
||||
<div class="menu-container">
|
||||
<mat-list>
|
||||
<mat-list-item class="process-tasks__task-item" (click)="clickStartTask($event)">
|
||||
<mat-icon mat-list-icon>assignment</mat-icon>
|
||||
<h3 matLine>{{ 'ADF_PROCESS_LIST.DETAILS.LABELS.START_FORM'|translate }}</h3>
|
||||
<span matLine>
|
||||
{{ 'ADF_PROCESS_LIST.DETAILS.LABELS.TASK_SUBTITLE' | translate:{user:getUserFullName(processInstanceDetails.startedBy), created: getFormatDate(processInstanceDetails.started, 'mediumDate') } }}
|
||||
</span>
|
||||
</mat-list-item>
|
||||
</mat-list>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- COMPLETED FORM -->
|
||||
<mat-chip-list>
|
||||
<span class="adf-chip-label">{{ 'ADF_PROCESS_LIST.DETAILS.LABELS.TASKS_COMPLETED'|translate }}</span>
|
||||
<mat-chip class="adf-process-badge" color="accent" selected="true">{{completedTasks?.length}}</mat-chip>
|
||||
</mat-chip-list>
|
||||
|
||||
<div class="menu-container" *ngIf="completedTasks?.length > 0" data-automation-id="completed-tasks">
|
||||
<mat-list>
|
||||
<mat-list-item class="process-tasks__task-item" *ngFor="let task of completedTasks" (click)="clickTask($event, task)">
|
||||
<mat-icon mat-list-icon>assignment</mat-icon>
|
||||
<h3 matLine>{{task.name || 'Nameless task'}}</h3>
|
||||
<span matLine>
|
||||
{{ 'ADF_PROCESS_LIST.DETAILS.LABELS.TASK_SUBTITLE' | translate:{user:getUserFullName(task.assignee), created: getFormatDate(task.created, 'mediumDate') } }}
|
||||
</span>
|
||||
</mat-list-item>
|
||||
</mat-list>
|
||||
</div>
|
||||
|
||||
<div *ngIf="completedTasks?.length === 0" data-automation-id="completed-tasks-none" class="no-results">
|
||||
{{ 'ADF_PROCESS_LIST.DETAILS.TASKS.NO_COMPLETED' | translate }}
|
||||
</div>
|
||||
|
||||
<ng-template *ngIf="hasStartFormDefined()" #startDialog>
|
||||
<div id="adf-start-process-dialog" class="adf-start-process-dialog">
|
||||
<h4 matDialogTitle>{{ 'ADF_PROCESS_LIST.DETAILS.LABELS.START_FORM'|translate }}</h4>
|
||||
<div mat-dialog-content class="adf-start-process-dialog-content">
|
||||
<adf-start-form [processId]="processId"
|
||||
[showRefreshButton]="false" [readOnlyForm]="true"
|
||||
(formContentClicked)='onFormContentClick($event)'>
|
||||
</adf-start-form>
|
||||
</div>
|
||||
<div mat-dialog-actions class="adf-start-process-dialog-actions">
|
||||
<button mat-button type="button" (click)="closeSartDialog()">{{ 'ADF_PROCESS_LIST.DETAILS.TASKS.TASK_CLOSE' | translate }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
@@ -0,0 +1,150 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2016 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 { DebugElement, NO_ERRORS_SCHEMA, SimpleChange } from '@angular/core';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { Observable } from 'rxjs/Rx';
|
||||
|
||||
import { TaskDetailsModel } from '../../task-list';
|
||||
|
||||
import { taskDetailsMock } from '../../mock';
|
||||
import { ProcessInstance } from './../models/process-instance.model';
|
||||
import { ProcessService } from './../services/process.service';
|
||||
import { ProcessInstanceTasksComponent } from './process-instance-tasks.component';
|
||||
|
||||
describe('ProcessInstanceTasksComponent', () => {
|
||||
|
||||
let component: ProcessInstanceTasksComponent;
|
||||
let fixture: ComponentFixture<ProcessInstanceTasksComponent>;
|
||||
let debugElement: DebugElement;
|
||||
let service: ProcessService;
|
||||
let getProcessTasksSpy: jasmine.Spy;
|
||||
|
||||
let exampleProcessInstance = new ProcessInstance({ id: '123' });
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [],
|
||||
declarations: [
|
||||
ProcessInstanceTasksComponent
|
||||
],
|
||||
providers: [
|
||||
ProcessService
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
fixture = TestBed.createComponent(ProcessInstanceTasksComponent);
|
||||
component = fixture.componentInstance;
|
||||
debugElement = fixture.debugElement;
|
||||
service = fixture.debugElement.injector.get(ProcessService);
|
||||
getProcessTasksSpy = spyOn(service, 'getProcessTasks').and.returnValue(Observable.of([new TaskDetailsModel(taskDetailsMock)]));
|
||||
|
||||
});
|
||||
|
||||
it('should initially render message about no active tasks if no process instance ID provided', async(() => {
|
||||
component.processInstanceDetails = undefined;
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
let msgEl = fixture.debugElement.query(By.css('[data-automation-id="active-tasks-none"]'));
|
||||
expect(msgEl).not.toBeNull();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should initially render message about no completed tasks if no process instance ID provided', async(() => {
|
||||
component.processInstanceDetails = undefined;
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
let msgEl = fixture.debugElement.query(By.css('[data-automation-id="completed-tasks-none"]'));
|
||||
expect(msgEl).not.toBeNull();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should not render active tasks list if no process instance ID provided', () => {
|
||||
component.processInstanceDetails = undefined;
|
||||
fixture.detectChanges();
|
||||
let listEl = fixture.debugElement.query(By.css('[data-automation-id="active-tasks"]'));
|
||||
expect(listEl).toBeNull();
|
||||
});
|
||||
|
||||
it('should not render completed tasks list if no process instance ID provided', () => {
|
||||
component.processInstanceDetails = undefined;
|
||||
fixture.detectChanges();
|
||||
let listEl = fixture.debugElement.query(By.css('[data-automation-id="completed-tasks"]'));
|
||||
expect(listEl).toBeNull();
|
||||
});
|
||||
|
||||
it('should display active tasks', () => {
|
||||
let change = new SimpleChange(null, exampleProcessInstance, true);
|
||||
fixture.detectChanges();
|
||||
component.ngOnChanges({ 'processInstanceDetails': change });
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
component.ngOnChanges({ 'processInstanceDetails': change });
|
||||
let listEl = fixture.debugElement.query(By.css('[data-automation-id="active-tasks"]'));
|
||||
expect(listEl).not.toBeNull();
|
||||
expect(listEl.queryAll(By.css('mat-list-item')).length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('should display completed tasks', () => {
|
||||
let change = new SimpleChange(null, exampleProcessInstance, true);
|
||||
fixture.detectChanges();
|
||||
component.ngOnChanges({ 'processInstanceDetails': change });
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
let listEl = fixture.debugElement.query(By.css('[data-automation-id="completed-tasks"]'));
|
||||
expect(listEl).not.toBeNull();
|
||||
expect(listEl.queryAll(By.css('mat-list-item')).length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('task reloading', () => {
|
||||
|
||||
beforeEach(async(() => {
|
||||
component.processInstanceDetails = exampleProcessInstance;
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable();
|
||||
}));
|
||||
|
||||
it('should render a refresh button by default', () => {
|
||||
expect(fixture.debugElement.query(By.css('.process-tasks-refresh'))).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should render a refresh button if configured to', () => {
|
||||
component.showRefreshButton = true;
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.query(By.css('.process-tasks-refresh'))).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should NOT render a refresh button if configured not to', () => {
|
||||
component.showRefreshButton = false;
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.query(By.css('.process-tasks-refresh'))).toBeNull();
|
||||
});
|
||||
|
||||
it('should call service to get tasks when reload button clicked', () => {
|
||||
getProcessTasksSpy.calls.reset();
|
||||
component.onRefreshClicked();
|
||||
expect(getProcessTasksSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,178 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2016 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 } from '@alfresco/core';
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material';
|
||||
import { Observable, Observer } from 'rxjs/Rx';
|
||||
import { TaskDetailsEvent, TaskDetailsModel } from '../../task-list';
|
||||
import { ProcessInstance } from '../models/process-instance.model';
|
||||
import { ProcessService } from './../services/process.service';
|
||||
|
||||
@Component({
|
||||
selector: 'adf-process-instance-tasks',
|
||||
templateUrl: './process-instance-tasks.component.html',
|
||||
styleUrls: ['./process-instance-tasks.component.css']
|
||||
})
|
||||
export class ProcessInstanceTasksComponent implements OnInit, OnChanges {
|
||||
|
||||
@Input()
|
||||
processInstanceDetails: ProcessInstance;
|
||||
|
||||
@Input()
|
||||
showRefreshButton: boolean = true;
|
||||
|
||||
@Output()
|
||||
error: EventEmitter<any> = new EventEmitter<any>();
|
||||
|
||||
activeTasks: TaskDetailsModel[] = [];
|
||||
completedTasks: TaskDetailsModel[] = [];
|
||||
|
||||
private taskObserver: Observer<TaskDetailsModel>;
|
||||
private completedTaskObserver: Observer<TaskDetailsModel>;
|
||||
|
||||
task$: Observable<TaskDetailsModel>;
|
||||
completedTask$: Observable<TaskDetailsModel>;
|
||||
|
||||
message: string;
|
||||
processId: string;
|
||||
|
||||
// @ViewChild('dialog')
|
||||
// dialog: any;
|
||||
|
||||
@ViewChild('startDialog')
|
||||
startDialog: any;
|
||||
|
||||
@ViewChild('taskdetails')
|
||||
taskdetails: any;
|
||||
|
||||
@Output()
|
||||
taskClick: EventEmitter<TaskDetailsEvent> = new EventEmitter<TaskDetailsEvent>();
|
||||
|
||||
constructor(private activitiProcess: ProcessService,
|
||||
private logService: LogService,
|
||||
private dialog: MatDialog) {
|
||||
this.task$ = new Observable<TaskDetailsModel>(observer => this.taskObserver = observer).share();
|
||||
this.completedTask$ = new Observable<TaskDetailsModel>(observer => this.completedTaskObserver = observer).share();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.task$.subscribe((task: TaskDetailsModel) => {
|
||||
this.activeTasks.push(task);
|
||||
});
|
||||
this.completedTask$.subscribe((task: TaskDetailsModel) => {
|
||||
this.completedTasks.push(task);
|
||||
});
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
let processInstanceDetails = changes['processInstanceDetails'];
|
||||
if (processInstanceDetails && processInstanceDetails.currentValue) {
|
||||
this.load(processInstanceDetails.currentValue.id);
|
||||
}
|
||||
}
|
||||
|
||||
load(processInstanceId: string) {
|
||||
this.loadActive(processInstanceId);
|
||||
this.loadCompleted(processInstanceId);
|
||||
}
|
||||
|
||||
loadActive(processInstanceId: string) {
|
||||
this.activeTasks = [];
|
||||
if (processInstanceId) {
|
||||
this.activitiProcess.getProcessTasks(processInstanceId, null).subscribe(
|
||||
(res: TaskDetailsModel[]) => {
|
||||
res.forEach((task) => {
|
||||
this.taskObserver.next(task);
|
||||
});
|
||||
},
|
||||
(err) => {
|
||||
this.error.emit(err);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
this.activeTasks = [];
|
||||
}
|
||||
}
|
||||
|
||||
loadCompleted(processInstanceId: string) {
|
||||
this.completedTasks = [];
|
||||
if (processInstanceId) {
|
||||
this.activitiProcess.getProcessTasks(processInstanceId, 'completed').subscribe(
|
||||
(res: TaskDetailsModel[]) => {
|
||||
res.forEach((task) => {
|
||||
this.completedTaskObserver.next(task);
|
||||
});
|
||||
},
|
||||
(err) => {
|
||||
this.error.emit(err);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
this.completedTasks = [];
|
||||
}
|
||||
}
|
||||
|
||||
hasStartFormDefined(): boolean {
|
||||
return this.processInstanceDetails && this.processInstanceDetails.startFormDefined === true;
|
||||
}
|
||||
|
||||
getUserFullName(user: any) {
|
||||
if (user) {
|
||||
return (user.firstName && user.firstName !== 'null'
|
||||
? user.firstName + ' ' : '') +
|
||||
user.lastName;
|
||||
}
|
||||
return 'Nobody';
|
||||
}
|
||||
|
||||
getFormatDate(value, format: string) {
|
||||
let datePipe = new DatePipe('en-US');
|
||||
try {
|
||||
return datePipe.transform(value, format);
|
||||
} catch (err) {
|
||||
this.logService.error(`ProcessListInstanceTask: error parsing date ${value} to format ${format}`);
|
||||
}
|
||||
}
|
||||
|
||||
clickTask($event: any, task: TaskDetailsModel) {
|
||||
let args = new TaskDetailsEvent(task);
|
||||
this.taskClick.emit(args);
|
||||
}
|
||||
|
||||
clickStartTask() {
|
||||
this.processId = this.processInstanceDetails.id;
|
||||
this.showStartDialog();
|
||||
}
|
||||
|
||||
showStartDialog() {
|
||||
this.dialog.open(this.startDialog, { height: '500px', width: '700px' });
|
||||
}
|
||||
|
||||
closeSartDialog() {
|
||||
this.dialog.closeAll();
|
||||
}
|
||||
|
||||
onRefreshClicked() {
|
||||
this.load(this.processInstanceDetails.id);
|
||||
}
|
||||
|
||||
onFormContentClick() {
|
||||
this.closeSartDialog();
|
||||
}
|
||||
}
|
@@ -0,0 +1,28 @@
|
||||
adf-datatable >>> .column-header {
|
||||
color: #232323;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
adf-datatable >>> .data-cell {
|
||||
cursor: pointer !important;
|
||||
}
|
||||
|
||||
adf-datatable >>> .cell-value{
|
||||
width: 250px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis
|
||||
}
|
||||
|
||||
.adf-process-list-loading-margin {
|
||||
margin-left: calc((100% - 100px) / 2);
|
||||
margin-right: calc((100% - 100px) / 2);
|
||||
}
|
||||
|
||||
.no-content-message {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
opacity: 0.54;
|
||||
color: #000;
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
<div *ngIf="!requestNode">{{ 'ADF_PROCESS_LIST.FILTERS.MESSAGES.NONE' | translate }}</div>
|
||||
<div *ngIf="requestNode">
|
||||
<adf-datatable
|
||||
[data]="data"
|
||||
[loading]="isLoading"
|
||||
(rowClick)="onRowClick($event)"
|
||||
(row-keyup)="onRowKeyUp($event)">
|
||||
<loading-content-template>
|
||||
<ng-template>
|
||||
<!--Add your custom loading template here-->
|
||||
<mat-progress-spinner
|
||||
class="adf-process-list-loading-margin"
|
||||
[color]="'primary'"
|
||||
[mode]="'indeterminate'">
|
||||
</mat-progress-spinner>
|
||||
</ng-template>
|
||||
</loading-content-template>
|
||||
<no-content-template>
|
||||
<!--Add your custom empty template here-->
|
||||
<ng-template>
|
||||
<div class="no-content-message">
|
||||
{{ 'ADF_PROCESS_LIST.LIST.NONE' | translate }}
|
||||
</div>
|
||||
</ng-template>
|
||||
</no-content-template>
|
||||
</adf-datatable>
|
||||
</div>
|
@@ -0,0 +1,485 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2016 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, fakeAsync, TestBed, tick } from '@angular/core/testing';
|
||||
import { MatProgressSpinnerModule } from '@angular/material';
|
||||
import { Observable } from 'rxjs/Rx';
|
||||
import { ProcessInstanceListComponent } from './process-list.component';
|
||||
|
||||
import { AppConfigService } from '@alfresco/core';
|
||||
import { DataRowEvent, DataSorting, ObjectDataRow, ObjectDataTableAdapter } from '@alfresco/core';
|
||||
|
||||
import { fakeProcessInstance, fakeProcessInstancesWithNoName } from '../../mock';
|
||||
import { ProcessService } from '../services/process.service';
|
||||
|
||||
describe('ProcessInstanceListComponent', () => {
|
||||
|
||||
let fixture: ComponentFixture<ProcessInstanceListComponent>;
|
||||
let component: ProcessInstanceListComponent;
|
||||
let service: ProcessService;
|
||||
let getProcessInstancesSpy: jasmine.Spy;
|
||||
let appConfig: AppConfigService;
|
||||
|
||||
let fakeCutomSchema = [
|
||||
{
|
||||
'key': 'fakeName',
|
||||
'type': 'text',
|
||||
'title': 'ADF_PROCESS_LIST.PROPERTIES.FAKE',
|
||||
'sortable': true
|
||||
},
|
||||
{
|
||||
'key': 'fakeProcessName',
|
||||
'type': 'text',
|
||||
'title': 'ADF_PROCESS_LIST.PROPERTIES.PROCESS_FAKE',
|
||||
'sortable': true
|
||||
}
|
||||
];
|
||||
|
||||
let fakeColumnSchema = {
|
||||
'default': [
|
||||
{
|
||||
'key': 'name',
|
||||
'type': 'text',
|
||||
'title': 'ADF_PROCESS_LIST.PROPERTIES.NAME',
|
||||
'sortable': true
|
||||
},
|
||||
{
|
||||
'key': 'created',
|
||||
'type': 'text',
|
||||
'title': 'ADF_PROCESS_LIST.PROPERTIES.CREATED',
|
||||
'cssClass': 'hidden',
|
||||
'sortable': true
|
||||
}
|
||||
]
|
||||
, fakeCutomSchema };
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
MatProgressSpinnerModule
|
||||
],
|
||||
declarations: [ ProcessInstanceListComponent ],
|
||||
providers: [
|
||||
ProcessService
|
||||
]
|
||||
}).compileComponents().then(() => {
|
||||
fixture = TestBed.createComponent(ProcessInstanceListComponent);
|
||||
component = fixture.componentInstance;
|
||||
appConfig = TestBed.get(AppConfigService);
|
||||
service = fixture.debugElement.injector.get(ProcessService);
|
||||
|
||||
getProcessInstancesSpy = spyOn(service, 'getProcessInstances').and.returnValue(Observable.of(fakeProcessInstance));
|
||||
appConfig.config = Object.assign(appConfig.config, {
|
||||
'adf-process-list': {
|
||||
'presets': {
|
||||
'fakeCutomSchema': [
|
||||
{
|
||||
'key': 'fakeName',
|
||||
'type': 'text',
|
||||
'title': 'ADF_PROCESS_LIST.PROPERTIES.FAKE',
|
||||
'sortable': true
|
||||
},
|
||||
{
|
||||
'key': 'fakeProcessName',
|
||||
'type': 'text',
|
||||
'title': 'ADF_PROCESS_LIST.PROPERTIES.PROCESS_FAKE',
|
||||
'sortable': true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
});
|
||||
}));
|
||||
|
||||
it('should use the default schemaColumn as default', () => {
|
||||
component.ngAfterContentInit();
|
||||
expect(component.data.getColumns()).toBeDefined();
|
||||
expect(component.data.getColumns().length).toEqual(2);
|
||||
});
|
||||
|
||||
it('should use the schemaColumn passed in input', () => {
|
||||
component.data = new ObjectDataTableAdapter(
|
||||
[],
|
||||
[
|
||||
{type: 'text', key: 'fake-id', title: 'Name'}
|
||||
]
|
||||
);
|
||||
|
||||
component.ngAfterContentInit();
|
||||
expect(component.data.getColumns()).toBeDefined();
|
||||
expect(component.data.getColumns().length).toEqual(1);
|
||||
});
|
||||
|
||||
it('should fetch the custom schemaColumn from app.config.json', () => {
|
||||
component.ngAfterContentInit();
|
||||
fixture.detectChanges();
|
||||
expect(component.layoutPresets).toEqual(fakeColumnSchema);
|
||||
});
|
||||
|
||||
it('should fetch custom schemaColumn when the input presetColumn is defined', () => {
|
||||
component.presetColumn = 'fakeCutomColumns';
|
||||
component.ngAfterContentInit();
|
||||
fixture.detectChanges();
|
||||
expect(component.data.getColumns()).toBeDefined();
|
||||
expect(component.data.getColumns().length).toEqual(2);
|
||||
});
|
||||
|
||||
it('should return an empty process list when no input parameters are passed', () => {
|
||||
component.ngAfterContentInit();
|
||||
expect(component.data).toBeDefined();
|
||||
expect(component.isListEmpty()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should emit onSuccess event when process instances loaded', fakeAsync(() => {
|
||||
let emitSpy = spyOn(component.success, 'emit');
|
||||
component.appId = 1;
|
||||
component.state = 'open';
|
||||
component.processDefinitionKey = null;
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
expect(emitSpy).toHaveBeenCalledWith(fakeProcessInstance);
|
||||
}));
|
||||
|
||||
it('should return the process instances list in original order when datalist passed non-existent columns', async(() => {
|
||||
component.data = new ObjectDataTableAdapter(
|
||||
[],
|
||||
[
|
||||
{type: 'text', key: 'fake-id', title: 'Name'}
|
||||
]
|
||||
);
|
||||
component.appId = 1;
|
||||
component.state = 'open';
|
||||
component.processDefinitionKey = null;
|
||||
component.success.subscribe((res) => {
|
||||
expect(res).toBeDefined();
|
||||
expect(component.data).toBeDefined();
|
||||
expect(component.isListEmpty()).not.toBeTruthy();
|
||||
expect(component.data.getRows().length).toEqual(2);
|
||||
expect(component.data.getRows()[0].getValue('name')).toEqual('Process 773443333');
|
||||
expect(component.data.getRows()[1].getValue('name')).toEqual('Process 382927392');
|
||||
});
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should order the process instances by name column when no sort passed', async(() => {
|
||||
component.appId = 1;
|
||||
component.state = 'open';
|
||||
component.processDefinitionKey = null;
|
||||
component.success.subscribe((res) => {
|
||||
expect(res).toBeDefined();
|
||||
expect(component.data).toBeDefined();
|
||||
expect(component.isListEmpty()).not.toBeTruthy();
|
||||
expect(component.data.getRows().length).toEqual(2);
|
||||
expect(component.data.getRows()[0].getValue('name')).toEqual('Process 382927392');
|
||||
expect(component.data.getRows()[1].getValue('name')).toEqual('Process 773443333');
|
||||
});
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should order the process instances by descending column when specified', async(() => {
|
||||
component.appId = 1;
|
||||
component.state = 'open';
|
||||
component.processDefinitionKey = null;
|
||||
component.sort = 'name-desc';
|
||||
component.success.subscribe((res) => {
|
||||
expect(res).toBeDefined();
|
||||
expect(component.data).toBeDefined();
|
||||
expect(component.isListEmpty()).not.toBeTruthy();
|
||||
expect(component.data.getRows().length).toEqual(2);
|
||||
expect(component.data.getRows()[0].getValue('name')).toEqual('Process 773443333');
|
||||
expect(component.data.getRows()[1].getValue('name')).toEqual('Process 382927392');
|
||||
});
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should order the process instances by ascending column when specified', async(() => {
|
||||
component.appId = 1;
|
||||
component.state = 'open';
|
||||
component.processDefinitionKey = null;
|
||||
component.sort = 'started-asc';
|
||||
component.success.subscribe((res) => {
|
||||
expect(res).toBeDefined();
|
||||
expect(component.data).toBeDefined();
|
||||
expect(component.isListEmpty()).not.toBeTruthy();
|
||||
expect(component.data.getRows().length).toEqual(2);
|
||||
expect(component.data.getRows()[0].getValue('name')).toEqual('Process 773443333');
|
||||
expect(component.data.getRows()[1].getValue('name')).toEqual('Process 382927392');
|
||||
});
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should order the process instances by descending start date when specified', async(() => {
|
||||
component.appId = 1;
|
||||
component.state = 'open';
|
||||
component.processDefinitionKey = null;
|
||||
component.sort = 'started-desc';
|
||||
component.success.subscribe((res) => {
|
||||
expect(res).toBeDefined();
|
||||
expect(component.data).toBeDefined();
|
||||
expect(component.isListEmpty()).not.toBeTruthy();
|
||||
expect(component.data.getRows().length).toEqual(2);
|
||||
expect(component.data.getRows()[0].getValue('name')).toEqual('Process 382927392');
|
||||
expect(component.data.getRows()[1].getValue('name')).toEqual('Process 773443333');
|
||||
});
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should return a default name if no name is specified on the process', async(() => {
|
||||
getProcessInstancesSpy = getProcessInstancesSpy.and.returnValue(Observable.of(fakeProcessInstancesWithNoName));
|
||||
component.appId = 1;
|
||||
component.state = 'open';
|
||||
component.processDefinitionKey = 'fakeprocess';
|
||||
component.success.subscribe( (res) => {
|
||||
expect(component.data.getRows()[0].getValue('name')).toEqual('Fake Process Name - Nov 9, 2017, 12:36:14 PM');
|
||||
expect(component.data.getRows()[1].getValue('name')).toEqual('Fake Process Name - Nov 9, 2017, 12:37:25 PM');
|
||||
});
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should return a currentId null when the processList is empty', () => {
|
||||
component.selectFirst();
|
||||
expect(component.getCurrentId()).toBeNull();
|
||||
});
|
||||
|
||||
it('should return selected true for the selected process', () => {
|
||||
component.data = new ObjectDataTableAdapter(
|
||||
[
|
||||
{ id: '999', name: 'Fake-name' },
|
||||
{ id: '888', name: 'Fake-name-888' }
|
||||
],
|
||||
[
|
||||
{ type: 'text', key: 'id', title: 'Id' },
|
||||
{ type: 'text', key: 'name', title: 'Name' }
|
||||
]
|
||||
);
|
||||
component.selectFirst();
|
||||
const dataRow = component.data.getRows();
|
||||
expect(dataRow).toBeDefined();
|
||||
expect(dataRow[0].getValue('id')).toEqual('999');
|
||||
expect(dataRow[0].isSelected).toEqual(true);
|
||||
expect(dataRow[1].getValue('id')).toEqual('888');
|
||||
expect(dataRow[1].isSelected).toEqual(false);
|
||||
});
|
||||
|
||||
it('should throw an exception when the response is wrong', fakeAsync(() => {
|
||||
let emitSpy: jasmine.Spy = spyOn(component.error, 'emit');
|
||||
let fakeError = 'Fake server error';
|
||||
getProcessInstancesSpy.and.returnValue(Observable.throw(fakeError));
|
||||
component.appId = 1;
|
||||
component.state = 'open';
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
expect(emitSpy).toHaveBeenCalledWith(fakeError);
|
||||
}));
|
||||
|
||||
it('should emit onSuccess event when reload() called', fakeAsync(() => {
|
||||
component.appId = 1;
|
||||
component.state = 'open';
|
||||
component.processDefinitionKey = null;
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
let emitSpy = spyOn(component.success, 'emit');
|
||||
component.reload();
|
||||
tick();
|
||||
expect(emitSpy).toHaveBeenCalledWith(fakeProcessInstance);
|
||||
}));
|
||||
|
||||
it('should reload processes when reload() is called', (done) => {
|
||||
component.data = new ObjectDataTableAdapter(
|
||||
[],
|
||||
[
|
||||
{type: 'text', key: 'fake-id', title: 'Name'}
|
||||
]
|
||||
);
|
||||
component.state = 'open';
|
||||
component.success.subscribe( (res) => {
|
||||
expect(res).toBeDefined();
|
||||
expect(component.data).toBeDefined();
|
||||
expect(component.isListEmpty()).not.toBeTruthy();
|
||||
expect(component.data.getRows().length).toEqual(2);
|
||||
expect(component.data.getRows()[0].getValue('name')).toEqual('Process 773443333');
|
||||
done();
|
||||
});
|
||||
component.reload();
|
||||
});
|
||||
|
||||
it('should emit row click event', (done) => {
|
||||
let row = new ObjectDataRow({
|
||||
id: '999'
|
||||
});
|
||||
let rowEvent = new DataRowEvent(row, null);
|
||||
|
||||
component.rowClick.subscribe(taskId => {
|
||||
expect(taskId).toEqual('999');
|
||||
expect(component.getCurrentId()).toEqual('999');
|
||||
done();
|
||||
});
|
||||
|
||||
component.onRowClick(rowEvent);
|
||||
});
|
||||
|
||||
it('should emit row click event on Enter', (done) => {
|
||||
let prevented = false;
|
||||
let keyEvent = new CustomEvent('Keyboard event', { detail: {
|
||||
keyboardEvent: { key: 'Enter' },
|
||||
row: new ObjectDataRow({ id: '999' })
|
||||
}});
|
||||
|
||||
spyOn(keyEvent, 'preventDefault').and.callFake(() => prevented = true);
|
||||
|
||||
component.rowClick.subscribe((taskId: string) => {
|
||||
expect(taskId).toEqual('999');
|
||||
expect(component.getCurrentId()).toEqual('999');
|
||||
expect(prevented).toBeTruthy();
|
||||
done();
|
||||
});
|
||||
|
||||
component.onRowKeyUp(keyEvent);
|
||||
});
|
||||
|
||||
it('should NOT emit row click event on every other key', async(() => {
|
||||
let triggered = false;
|
||||
let keyEvent = new CustomEvent('Keyboard event', { detail: {
|
||||
keyboardEvent: { key: 'Space' },
|
||||
row: new ObjectDataRow({ id: 999 })
|
||||
}});
|
||||
|
||||
component.rowClick.subscribe(() => triggered = true);
|
||||
component.onRowKeyUp(keyEvent);
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
expect(triggered).toBeFalsy();
|
||||
});
|
||||
}));
|
||||
|
||||
describe('component changes', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
component.data = new ObjectDataTableAdapter(
|
||||
[],
|
||||
[
|
||||
{type: 'text', key: 'fake-id', title: 'Name'}
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
it('should NOT reload the process list when no parameters changed', () => {
|
||||
expect(component.isListEmpty()).toBeTruthy();
|
||||
component.ngOnChanges({});
|
||||
expect(component.isListEmpty()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should reload the list when the appId parameter changes', (done) => {
|
||||
const appId = '1';
|
||||
let change = new SimpleChange(null, appId, true);
|
||||
|
||||
component.success.subscribe((res) => {
|
||||
expect(res).toBeDefined();
|
||||
expect(component.data).toBeDefined();
|
||||
expect(component.isListEmpty()).not.toBeTruthy();
|
||||
expect(component.data.getRows().length).toEqual(2);
|
||||
expect(component.data.getRows()[0].getValue('name')).toEqual('Process 773443333');
|
||||
done();
|
||||
});
|
||||
|
||||
component.ngOnChanges({'appId': change});
|
||||
});
|
||||
|
||||
it('should reload the list when the processDefinitionKey parameter changes', (done) => {
|
||||
const processDefinitionKey = 'fakeprocess';
|
||||
let change = new SimpleChange(null, processDefinitionKey, true);
|
||||
|
||||
component.success.subscribe((res) => {
|
||||
expect(res).toBeDefined();
|
||||
expect(component.data).toBeDefined();
|
||||
expect(component.isListEmpty()).not.toBeTruthy();
|
||||
expect(component.data.getRows().length).toEqual(2);
|
||||
expect(component.data.getRows()[0].getValue('name')).toEqual('Process 773443333');
|
||||
done();
|
||||
});
|
||||
|
||||
component.ngOnChanges({'processDefinitionKey': change});
|
||||
});
|
||||
|
||||
it('should reload the list when the state parameter changes', (done) => {
|
||||
const state = 'open';
|
||||
let change = new SimpleChange(null, state, true);
|
||||
|
||||
component.success.subscribe((res) => {
|
||||
expect(res).toBeDefined();
|
||||
expect(component.data).toBeDefined();
|
||||
expect(component.isListEmpty()).not.toBeTruthy();
|
||||
expect(component.data.getRows().length).toEqual(2);
|
||||
expect(component.data.getRows()[0].getValue('name')).toEqual('Process 773443333');
|
||||
done();
|
||||
});
|
||||
|
||||
component.ngOnChanges({'state': change});
|
||||
});
|
||||
|
||||
it('should reload the list when the sort parameter changes', (done) => {
|
||||
const sort = 'created-desc';
|
||||
let change = new SimpleChange(null, sort, true);
|
||||
|
||||
component.success.subscribe((res) => {
|
||||
expect(res).toBeDefined();
|
||||
expect(component.data).toBeDefined();
|
||||
expect(component.isListEmpty()).not.toBeTruthy();
|
||||
expect(component.data.getRows().length).toEqual(2);
|
||||
expect(component.data.getRows()[0].getValue('name')).toEqual('Process 773443333');
|
||||
done();
|
||||
});
|
||||
|
||||
component.ngOnChanges({'sort': change});
|
||||
});
|
||||
|
||||
it('should sort the list when the sort parameter changes', (done) => {
|
||||
const sort = 'created-asc';
|
||||
let change = new SimpleChange(null, sort, true);
|
||||
let sortSpy = spyOn(component.data, 'setSorting');
|
||||
|
||||
component.success.subscribe((res) => {
|
||||
expect(res).toBeDefined();
|
||||
expect(sortSpy).toHaveBeenCalledWith(new DataSorting('started', 'asc'));
|
||||
done();
|
||||
});
|
||||
|
||||
component.sort = sort;
|
||||
component.ngOnChanges({'sort': change});
|
||||
});
|
||||
|
||||
it('should reload the process list when the name parameter changes', (done) => {
|
||||
const name = 'FakeTaskName';
|
||||
let change = new SimpleChange(null, name, true);
|
||||
|
||||
component.success.subscribe((res) => {
|
||||
expect(res).toBeDefined();
|
||||
expect(component.data).toBeDefined();
|
||||
expect(component.isListEmpty()).not.toBeTruthy();
|
||||
expect(component.data.getRows().length).toEqual(2);
|
||||
expect(component.data.getRows()[0].getValue('name')).toEqual('Process 773443333');
|
||||
done();
|
||||
});
|
||||
|
||||
component.ngOnChanges({'name': change});
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,314 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2016 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 { DataColumn, DataRowEvent, DataSorting, DataTableAdapter, ObjectDataColumn, ObjectDataRow, ObjectDataTableAdapter } from '@alfresco/core';
|
||||
import { AppConfigService, DataColumnListComponent } from '@alfresco/core';
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { AfterContentInit, Component, ContentChild, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
|
||||
import { ProcessFilterParamRepresentationModel } from '../models/filter-process.model';
|
||||
import { ProcessInstance } from '../models/process-instance.model';
|
||||
import { processPresetsDefaultModel } from '../models/process-preset.model';
|
||||
import { ProcessService } from '../services/process.service';
|
||||
|
||||
@Component({
|
||||
selector: 'adf-process-instance-list',
|
||||
styleUrls: ['./process-list.component.css'],
|
||||
templateUrl: './process-list.component.html'
|
||||
})
|
||||
export class ProcessInstanceListComponent implements OnChanges, AfterContentInit {
|
||||
|
||||
@ContentChild(DataColumnListComponent) columnList: DataColumnListComponent;
|
||||
|
||||
@Input()
|
||||
appId: number;
|
||||
|
||||
@Input()
|
||||
processDefinitionKey: string;
|
||||
|
||||
@Input()
|
||||
state: string;
|
||||
|
||||
@Input()
|
||||
sort: string;
|
||||
|
||||
@Input()
|
||||
name: string;
|
||||
|
||||
@Input()
|
||||
presetColumn: string;
|
||||
|
||||
requestNode: ProcessFilterParamRepresentationModel;
|
||||
|
||||
@Input()
|
||||
data: DataTableAdapter;
|
||||
|
||||
@Output()
|
||||
rowClick: EventEmitter<string> = new EventEmitter<string>();
|
||||
|
||||
@Output()
|
||||
success: EventEmitter<ProcessInstance[]> = new EventEmitter<ProcessInstance[]>();
|
||||
|
||||
@Output()
|
||||
error: EventEmitter<any> = new EventEmitter<any>();
|
||||
|
||||
currentInstanceId: string;
|
||||
isLoading: boolean = true;
|
||||
layoutPresets = {};
|
||||
|
||||
constructor(private processService: ProcessService,
|
||||
private appConfig: AppConfigService) {
|
||||
}
|
||||
|
||||
ngAfterContentInit() {
|
||||
this.loadLayoutPresets();
|
||||
this.setupSchema();
|
||||
|
||||
if (this.appId) {
|
||||
this.reload();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup html-based (html definitions) or code behind (data adapter) schema.
|
||||
* If component is assigned with an empty data adater the default schema settings applied.
|
||||
*/
|
||||
setupSchema() {
|
||||
let schema: DataColumn[] = [];
|
||||
|
||||
if (this.columnList && this.columnList.columns && this.columnList.columns.length > 0) {
|
||||
schema = this.columnList.columns.map(c => <DataColumn> c);
|
||||
}
|
||||
|
||||
if (!this.data) {
|
||||
this.data = new ObjectDataTableAdapter([], schema.length > 0 ? schema : this.getLayoutPreset(this.presetColumn));
|
||||
} else {
|
||||
if (schema && schema.length > 0) {
|
||||
this.data.setColumns(schema);
|
||||
} else if (this.data.getColumns().length === 0) {
|
||||
this.presetColumn ? this.setupDefaultColumns(this.presetColumn) : this.setupDefaultColumns();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
if (this.isPropertyChanged(changes)) {
|
||||
this.reload();
|
||||
}
|
||||
}
|
||||
|
||||
private isPropertyChanged(changes: SimpleChanges): boolean {
|
||||
let changed: boolean = false;
|
||||
|
||||
let appId = changes['appId'];
|
||||
let processDefinitionKey = changes['processDefinitionKey'];
|
||||
let state = changes['state'];
|
||||
let sort = changes['sort'];
|
||||
let name = changes['name'];
|
||||
|
||||
if (appId && appId.currentValue) {
|
||||
changed = true;
|
||||
} else if (processDefinitionKey && processDefinitionKey.currentValue) {
|
||||
changed = true;
|
||||
} else if (state && state.currentValue) {
|
||||
changed = true;
|
||||
} else if (sort && sort.currentValue) {
|
||||
changed = true;
|
||||
} else if (name && name.currentValue) {
|
||||
changed = true;
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
public reload() {
|
||||
this.requestNode = this.createRequestNode();
|
||||
this.load(this.requestNode);
|
||||
}
|
||||
|
||||
private load(requestNode: ProcessFilterParamRepresentationModel) {
|
||||
this.isLoading = true;
|
||||
this.processService.getProcessInstances(requestNode, this.processDefinitionKey)
|
||||
.subscribe(
|
||||
(response) => {
|
||||
let instancesRow = this.createDataRow(response);
|
||||
this.renderInstances(instancesRow);
|
||||
this.selectFirst();
|
||||
this.success.emit(response);
|
||||
this.isLoading = false;
|
||||
},
|
||||
error => {
|
||||
this.error.emit(error);
|
||||
this.isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an array of ObjectDataRow
|
||||
* @param instances
|
||||
* @returns {ObjectDataRow[]}
|
||||
*/
|
||||
private createDataRow(instances: any[]): ObjectDataRow[] {
|
||||
let instancesRows: ObjectDataRow[] = [];
|
||||
instances.forEach((row) => {
|
||||
instancesRows.push(new ObjectDataRow(row));
|
||||
});
|
||||
return instancesRows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the instances list
|
||||
*
|
||||
* @param instances
|
||||
*/
|
||||
private renderInstances(instances: any[]) {
|
||||
instances = this.optimizeNames(instances);
|
||||
this.setDatatableSorting();
|
||||
this.data.setRows(instances);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort the datatable rows based on current value of 'sort' property
|
||||
*/
|
||||
private setDatatableSorting() {
|
||||
if (!this.sort) {
|
||||
return;
|
||||
}
|
||||
let sortingParams: string[] = this.sort.split('-');
|
||||
if (sortingParams.length === 2) {
|
||||
let sortColumn = sortingParams[0] === 'created' ? 'started' : sortingParams[0];
|
||||
let sortOrder = sortingParams[1];
|
||||
this.data.setSorting(new DataSorting(sortColumn, sortOrder));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Select the first instance of a list if present
|
||||
*/
|
||||
selectFirst() {
|
||||
if (!this.isListEmpty()) {
|
||||
let row = this.data.getRows()[0];
|
||||
row.isSelected = true;
|
||||
this.data.selectedRow = row;
|
||||
this.currentInstanceId = row.getValue('id');
|
||||
} else {
|
||||
if (this.data) {
|
||||
this.data.selectedRow = null;
|
||||
}
|
||||
this.currentInstanceId = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current id
|
||||
* @returns {string}
|
||||
*/
|
||||
getCurrentId(): string {
|
||||
return this.currentInstanceId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the list is empty
|
||||
* @returns {ObjectDataTableAdapter|boolean}
|
||||
*/
|
||||
isListEmpty(): boolean {
|
||||
return this.data === undefined ||
|
||||
(this.data && this.data.getRows() && this.data.getRows().length === 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit the event rowClick passing the current task id when the row is clicked
|
||||
* @param event
|
||||
*/
|
||||
onRowClick(event: DataRowEvent) {
|
||||
let item = event;
|
||||
this.currentInstanceId = item.value.getValue('id');
|
||||
this.rowClick.emit(this.currentInstanceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit the event rowClick passing the current task id when pressed the Enter key on the selected row
|
||||
* @param event
|
||||
*/
|
||||
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
|
||||
* @returns {any[]}
|
||||
*/
|
||||
private optimizeNames(instances: any[]) {
|
||||
instances = instances.map(t => {
|
||||
t.obj.name = this.getProcessNameOrDescription(t.obj, 'medium');
|
||||
return t;
|
||||
});
|
||||
return instances;
|
||||
}
|
||||
|
||||
getProcessNameOrDescription(processInstance, dateFormat): string {
|
||||
let name = '';
|
||||
if (processInstance) {
|
||||
name = processInstance.name ||
|
||||
processInstance.processDefinitionName + ' - ' + this.getFormatDate(processInstance.started, dateFormat);
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
getFormatDate(value, format: string) {
|
||||
let datePipe = new DatePipe('en-US');
|
||||
try {
|
||||
return datePipe.transform(value, format);
|
||||
} catch (err) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
private createRequestNode() {
|
||||
let requestNode = {
|
||||
appDefinitionId: this.appId,
|
||||
state: this.state,
|
||||
sort: this.sort
|
||||
};
|
||||
return new ProcessFilterParamRepresentationModel(requestNode);
|
||||
}
|
||||
|
||||
setupDefaultColumns(preset: string = 'default'): void {
|
||||
if (this.data) {
|
||||
const columns = this.getLayoutPreset(preset);
|
||||
this.data.setColumns(columns);
|
||||
}
|
||||
}
|
||||
|
||||
private loadLayoutPresets(): void {
|
||||
const externalSettings = this.appConfig.get('adf-process-list.presets', null);
|
||||
|
||||
if (externalSettings) {
|
||||
this.layoutPresets = Object.assign({}, processPresetsDefaultModel, externalSettings);
|
||||
} else {
|
||||
this.layoutPresets = processPresetsDefaultModel;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private getLayoutPreset(name: string = 'default'): DataColumn[] {
|
||||
return (this.layoutPresets[name] || this.layoutPresets['default']).map(col => new ObjectDataColumn(col));
|
||||
}
|
||||
}
|
@@ -0,0 +1,36 @@
|
||||
<mat-card class="adf-start-process">
|
||||
<mat-card-title>{{'ADF_PROCESS_LIST.START_PROCESS.FORM.TITLE' | translate}}
|
||||
</mat-card-title>
|
||||
<mat-card-content *ngIf="isProcessDefinitionEmpty()">
|
||||
<mat-card-subtitle id="error-message" *ngIf="errorMessageId">
|
||||
{{errorMessageId|translate}}
|
||||
</mat-card-subtitle>
|
||||
<mat-form-field class="adf-process-input-container">
|
||||
<input matInput placeholder="{{'ADF_PROCESS_LIST.START_PROCESS.FORM.LABEL.NAME'|translate}}" [(ngModel)]="name" id="processName" required />
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<mat-select placeholder="{{'ADF_PROCESS_LIST.START_PROCESS.FORM.LABEL.TYPE'|translate}}" [(ngModel)]="currentProcessDef.id" (ngModelChange)="onProcessDefChange($event)" required>
|
||||
<mat-option>{{'ADF_PROCESS_LIST.START_PROCESS.FORM.TYPE_PLACEHOLDER' | translate}}</mat-option>
|
||||
<mat-option *ngFor="let processDef of processDefinitions" [value]="processDef.id">
|
||||
{{ processDef.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<adf-start-form *ngIf="hasStartForm()"
|
||||
[disableStartProcessButton]="!hasProcessName()"
|
||||
[processDefinitionId]="currentProcessDef.id"
|
||||
(outcomeClick)="onOutcomeClick($event)"
|
||||
[showRefreshButton]="false">
|
||||
<button form-custom-button mat-button (click)="cancelStartProcess()" id="cancle_process" class=""> {{'ADF_PROCESS_LIST.START_PROCESS.FORM.ACTION.CANCEL'| translate}} </button>
|
||||
</adf-start-form>
|
||||
</mat-card-content>
|
||||
<mat-card-content *ngIf="hasErrorMessage()">
|
||||
<mat-card-subtitle class="error-message" id="no-process-message">
|
||||
{{'ADF_PROCESS_LIST.START_PROCESS.NO_PROCESS_DEFINITIONS' | translate}}
|
||||
</mat-card-subtitle>
|
||||
</mat-card-content>
|
||||
<mat-card-actions *ngIf="!hasStartForm()">
|
||||
<button mat-button *ngIf="!hasStartForm()" (click)="cancelStartProcess()" id="cancle_process" class=""> {{'ADF_PROCESS_LIST.START_PROCESS.FORM.ACTION.CANCEL'| translate}} </button>
|
||||
<button mat-button *ngIf="!hasStartForm()" [disabled]="!validateForm()" (click)="startProcess()" data-automation-id="btn-start" id="button-start" class="btn-start"> {{'ADF_PROCESS_LIST.START_PROCESS.FORM.ACTION.START' | translate}} </button>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
@@ -0,0 +1,35 @@
|
||||
.adf {
|
||||
&-start-process {
|
||||
width: calc(66.6666% - 48px);
|
||||
margin-left: calc(33.3333333333% / 2);
|
||||
margin-right: calc(33.3333333333% / 2);
|
||||
margin-top: 10px;
|
||||
.mat-select-trigger {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
mat-form-field {
|
||||
width: 100%;
|
||||
}
|
||||
mat-select {
|
||||
width: 100%;
|
||||
padding: 16px 0px 0px 0px;
|
||||
}
|
||||
mat-card-actions {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
&-process-input-container {
|
||||
mat-form-field {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
&-start-form-container {
|
||||
.mat-card {
|
||||
box-shadow: none !important;
|
||||
padding: 0px !important;
|
||||
}
|
||||
}
|
||||
&-start-form-actions {
|
||||
text-align: right !important;
|
||||
}
|
||||
}
|
@@ -0,0 +1,396 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2016 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 { DebugElement, SimpleChange } from '@angular/core';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import {
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
MatInputModule,
|
||||
MatSelectModule
|
||||
} from '@angular/material';
|
||||
import { FormModule, FormService } from '@alfresco/core';
|
||||
import { Observable } from 'rxjs/Rx';
|
||||
|
||||
import { ProcessInstanceVariable } from '../models/process-instance-variable.model';
|
||||
import { ProcessService } from '../services/process.service';
|
||||
import { newProcess, taskFormMock, testProcessDefRepr, testProcessDefs, testProcessDefWithForm } from '../../mock';
|
||||
import { StartProcessInstanceComponent } from './start-process.component';
|
||||
|
||||
describe('StartProcessInstanceComponent', () => {
|
||||
|
||||
let component: StartProcessInstanceComponent;
|
||||
let fixture: ComponentFixture<StartProcessInstanceComponent>;
|
||||
let processService: ProcessService;
|
||||
let formService: FormService;
|
||||
let getDefinitionsSpy: jasmine.Spy;
|
||||
let getStartFormDefinitionSpy: jasmine.Spy;
|
||||
let startProcessSpy: jasmine.Spy;
|
||||
let debugElement: DebugElement;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
FormModule,
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
MatInputModule,
|
||||
MatSelectModule
|
||||
],
|
||||
declarations: [
|
||||
StartProcessInstanceComponent
|
||||
],
|
||||
providers: [
|
||||
ProcessService,
|
||||
FormService
|
||||
]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
fixture = TestBed.createComponent(StartProcessInstanceComponent);
|
||||
component = fixture.componentInstance;
|
||||
debugElement = fixture.debugElement;
|
||||
processService = fixture.debugElement.injector.get(ProcessService);
|
||||
formService = fixture.debugElement.injector.get(FormService);
|
||||
|
||||
getDefinitionsSpy = spyOn(processService, 'getProcessDefinitions').and.returnValue(Observable.of(testProcessDefs));
|
||||
startProcessSpy = spyOn(processService, 'startProcess').and.returnValue(Observable.of(newProcess));
|
||||
getStartFormDefinitionSpy = spyOn(formService, 'getStartFormDefinition').and.returnValue(Observable.of(taskFormMock));
|
||||
|
||||
});
|
||||
|
||||
it('should create instance of StartProcessInstanceComponent', () => {
|
||||
expect(fixture.componentInstance instanceof StartProcessInstanceComponent).toBe(true, 'should create StartProcessInstanceComponent');
|
||||
});
|
||||
|
||||
describe('process definitions list', () => {
|
||||
|
||||
it('should call service to fetch process definitions with appId', () => {
|
||||
let change = new SimpleChange(null, '123', true);
|
||||
component.ngOnChanges({'appId': change});
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(getDefinitionsSpy).toHaveBeenCalledWith('123');
|
||||
});
|
||||
|
||||
it('should call service to fetch process definitions without appId', () => {
|
||||
let change = new SimpleChange(null, null, true);
|
||||
component.ngOnChanges({'appId': change});
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(getDefinitionsSpy).toHaveBeenCalledWith(null);
|
||||
});
|
||||
|
||||
it('should call service to fetch process definitions with appId when provided', () => {
|
||||
let change = new SimpleChange(null, '123', true);
|
||||
component.ngOnChanges({'appId': change});
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(getDefinitionsSpy).toHaveBeenCalledWith('123');
|
||||
});
|
||||
|
||||
it('should display the correct number of processes in the select list', () => {
|
||||
let change = new SimpleChange(null, '123', true);
|
||||
component.ngOnChanges({'appId': change});
|
||||
fixture.detectChanges();
|
||||
|
||||
let selectElement = fixture.nativeElement.querySelector('mat-select');
|
||||
expect(selectElement.children.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should display the option def details', () => {
|
||||
let change = new SimpleChange(null, '123', true);
|
||||
component.ngOnChanges({'appId': change});
|
||||
component.processDefinitions = testProcessDefs;
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
let selectElement = fixture.nativeElement.querySelector('mat-select > .mat-select-trigger');
|
||||
let optionElement = fixture.nativeElement.querySelectorAll('mat-option');
|
||||
selectElement.click();
|
||||
expect(selectElement).not.toBeNull();
|
||||
expect(selectElement).toBeDefined();
|
||||
expect(optionElement).not.toBeNull();
|
||||
expect(optionElement).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('should indicate an error to the user if process defs cannot be loaded', async(() => {
|
||||
getDefinitionsSpy = getDefinitionsSpy.and.returnValue(Observable.throw({}));
|
||||
let change = new SimpleChange(null, '123', true);
|
||||
component.ngOnChanges({'appId': change});
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
let errorEl = fixture.nativeElement.querySelector('#error-message');
|
||||
expect(errorEl).not.toBeNull('Expected error message to be present');
|
||||
expect(errorEl.innerText.trim()).toBe('ADF_PROCESS_LIST.START_PROCESS.ERROR.LOAD_PROCESS_DEFS');
|
||||
});
|
||||
}));
|
||||
|
||||
it('should show no process available message when no process definition is loaded', async(() => {
|
||||
getDefinitionsSpy = getDefinitionsSpy.and.returnValue(Observable.of([]));
|
||||
let change = new SimpleChange(null, '123', true);
|
||||
component.ngOnChanges({'appId': change});
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
let noprocessElement = fixture.nativeElement.querySelector('#no-process-message');
|
||||
expect(noprocessElement).not.toBeNull('Expected no available process message to be present');
|
||||
expect(noprocessElement.innerText.trim()).toBe('ADF_PROCESS_LIST.START_PROCESS.NO_PROCESS_DEFINITIONS');
|
||||
});
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
describe('input changes', () => {
|
||||
|
||||
let change = new SimpleChange(123, 456, true);
|
||||
let nullChange = new SimpleChange(123, null, true);
|
||||
|
||||
beforeEach(async(() => {
|
||||
component.appId = 123;
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
getDefinitionsSpy.calls.reset();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should reload processes when appId input changed', () => {
|
||||
component.ngOnChanges({appId: change});
|
||||
expect(getDefinitionsSpy).toHaveBeenCalledWith(456);
|
||||
});
|
||||
|
||||
it('should reload processes when appId input changed to null', () => {
|
||||
component.ngOnChanges({appId: nullChange});
|
||||
expect(getDefinitionsSpy).toHaveBeenCalledWith(null);
|
||||
});
|
||||
|
||||
it('should get current processDeff', () => {
|
||||
component.ngOnChanges({appId: change});
|
||||
component.onProcessDefChange('my:Process');
|
||||
fixture.detectChanges();
|
||||
expect(getDefinitionsSpy).toHaveBeenCalled();
|
||||
expect(component.processDefinitions).toBe(testProcessDefs);
|
||||
});
|
||||
});
|
||||
|
||||
describe('start process', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
component.name = 'My new process';
|
||||
let change = new SimpleChange(null, 123, true);
|
||||
component.ngOnChanges({'appId': change});
|
||||
});
|
||||
|
||||
it('should call service to start process if required fields provided', async(() => {
|
||||
component.onProcessDefChange('my:process1');
|
||||
component.startProcess();
|
||||
fixture.whenStable().then(() => {
|
||||
expect(startProcessSpy).toHaveBeenCalled();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should avoid calling service to start process if required fields NOT provided', async(() => {
|
||||
component.name = '';
|
||||
component.startProcess();
|
||||
fixture.whenStable().then(() => {
|
||||
expect(startProcessSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should call service to start process with the correct parameters', async(() => {
|
||||
component.onProcessDefChange('my:process1');
|
||||
component.startProcess();
|
||||
fixture.whenStable().then(() => {
|
||||
expect(startProcessSpy).toHaveBeenCalledWith('my:process1', 'My new process', undefined, undefined, undefined);
|
||||
});
|
||||
}));
|
||||
|
||||
it('should call service to start process with the variables setted', async(() => {
|
||||
let inputProcessVariable: ProcessInstanceVariable[] = [];
|
||||
|
||||
let variable: ProcessInstanceVariable = {};
|
||||
variable.name = 'nodeId';
|
||||
variable.value = 'id';
|
||||
|
||||
inputProcessVariable.push(variable);
|
||||
|
||||
component.variables = inputProcessVariable;
|
||||
component.onProcessDefChange('my:process1');
|
||||
component.startProcess();
|
||||
fixture.whenStable().then(() => {
|
||||
expect(startProcessSpy).toHaveBeenCalledWith('my:process1', 'My new process', undefined, undefined, inputProcessVariable);
|
||||
});
|
||||
}));
|
||||
|
||||
it('should output start event when process started successfully', async(() => {
|
||||
let emitSpy = spyOn(component.start, 'emit');
|
||||
component.onProcessDefChange('my:process1');
|
||||
component.startProcess();
|
||||
fixture.whenStable().then(() => {
|
||||
expect(emitSpy).toHaveBeenCalledWith(newProcess);
|
||||
});
|
||||
}));
|
||||
|
||||
it('should throw error event when process cannot be started', async(() => {
|
||||
let errorSpy = spyOn(component.error, 'error');
|
||||
let error = {message: 'My error'};
|
||||
startProcessSpy = startProcessSpy.and.returnValue(Observable.throw(error));
|
||||
component.onProcessDefChange('my:process1');
|
||||
component.startProcess();
|
||||
fixture.whenStable().then(() => {
|
||||
expect(errorSpy).toHaveBeenCalledWith(error);
|
||||
});
|
||||
}));
|
||||
|
||||
it('should indicate an error to the user if process cannot be started', async(() => {
|
||||
startProcessSpy = startProcessSpy.and.returnValue(Observable.throw({}));
|
||||
component.onProcessDefChange('my:process1');
|
||||
component.startProcess();
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
let errorEl = fixture.nativeElement.querySelector('#error-message');
|
||||
expect(errorEl).not.toBeNull();
|
||||
expect(errorEl.innerText.trim()).toBe('ADF_PROCESS_LIST.START_PROCESS.ERROR.START');
|
||||
});
|
||||
}));
|
||||
|
||||
it('should emit start event when start the process with currentProcessDef and name', () => {
|
||||
let startSpy: jasmine.Spy = spyOn(component.start, 'emit');
|
||||
component.currentProcessDef.id = '1001';
|
||||
component.name = 'my:Process';
|
||||
component.startProcess();
|
||||
fixture.detectChanges();
|
||||
expect(startSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not emit start event when start the process without currentProcessDef and name', () => {
|
||||
let startSpy: jasmine.Spy = spyOn(component.start, 'emit');
|
||||
component.startProcess();
|
||||
fixture.detectChanges();
|
||||
expect(startSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should able to start the process when the required fields are filled up', async(() => {
|
||||
let startSpy: jasmine.Spy = spyOn(component.start, 'emit');
|
||||
component.name = 'my:process1';
|
||||
component.onProcessDefChange('my:process1');
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
let startButton = fixture.nativeElement.querySelector('#button-start');
|
||||
startButton.click();
|
||||
expect(startSpy).toHaveBeenCalled();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should return true if startFrom defined', async(() => {
|
||||
component.currentProcessDef = testProcessDefRepr;
|
||||
component.name = 'my:process1';
|
||||
component.currentProcessDef.hasStartForm = true;
|
||||
component.hasStartForm();
|
||||
fixture.whenStable().then(() => {
|
||||
expect(component.hasStartForm()).toBe(true);
|
||||
});
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
describe('start forms', () => {
|
||||
|
||||
let startBtn;
|
||||
|
||||
describe('without start form', () => {
|
||||
|
||||
beforeEach(async(() => {
|
||||
component.name = 'My new process';
|
||||
let change = new SimpleChange(null, '123', true);
|
||||
component.ngOnChanges({'appId': change});
|
||||
fixture.detectChanges();
|
||||
component.onProcessDefChange('my:process1');
|
||||
fixture.whenStable();
|
||||
startBtn = fixture.nativeElement.querySelector('#button-start');
|
||||
}));
|
||||
|
||||
it('should have start button disabled when name not filled out', async(() => {
|
||||
component.name = '';
|
||||
fixture.detectChanges();
|
||||
expect(startBtn.disabled).toBe(true);
|
||||
}));
|
||||
|
||||
it('should have start button disabled when no process is selected', async(() => {
|
||||
component.onProcessDefChange('');
|
||||
fixture.detectChanges();
|
||||
expect(startBtn.disabled).toBe(true);
|
||||
}));
|
||||
|
||||
it('should enable start button when name and process filled out', async(() => {
|
||||
fixture.detectChanges();
|
||||
let startButton = fixture.nativeElement.querySelector('#button-start');
|
||||
expect(startButton.disabled).toBeFalsy();
|
||||
}));
|
||||
|
||||
it('should disable the start process button when process name is empty', async(() => {
|
||||
component.name = '';
|
||||
fixture.detectChanges();
|
||||
let startButton = fixture.nativeElement.querySelector('#button-start');
|
||||
expect(startButton.disabled).toBeTruthy();
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
describe('with start form', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
getDefinitionsSpy.and.returnValue(Observable.of(testProcessDefWithForm));
|
||||
let change = new SimpleChange(null, '123', true);
|
||||
component.ngOnChanges({'appId': change});
|
||||
component.onProcessDefChange('my:process1');
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable();
|
||||
startBtn = fixture.nativeElement.querySelector('#button-start');
|
||||
});
|
||||
|
||||
it('should initialize start form', () => {
|
||||
expect(component.startForm).toBeDefined();
|
||||
expect(component.startForm).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should load start form from service', () => {
|
||||
expect(getStartFormDefinitionSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not show the start process button', async(() => {
|
||||
component.name = 'My new process';
|
||||
fixture.detectChanges();
|
||||
expect(startBtn).toBeNull();
|
||||
}));
|
||||
|
||||
it('should emit cancel event on cancel Button', () => {
|
||||
let cancelButton = fixture.nativeElement.querySelector('#cancle_process');
|
||||
let cancelSpy: jasmine.Spy = spyOn(component.cancel, 'emit');
|
||||
cancelButton.click();
|
||||
fixture.detectChanges();
|
||||
expect(cancelSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
@@ -0,0 +1,161 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright 2016 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, ViewEncapsulation } from '@angular/core';
|
||||
import { StartFormComponent } from '@alfresco/core';
|
||||
import { ProcessInstanceVariable } from '../models/process-instance-variable.model';
|
||||
import { ProcessDefinitionRepresentation } from './../models/process-definition.model';
|
||||
import { ProcessInstance } from './../models/process-instance.model';
|
||||
import { ProcessService } from './../services/process.service';
|
||||
|
||||
@Component({
|
||||
selector: 'adf-start-process',
|
||||
templateUrl: './start-process.component.html',
|
||||
styleUrls: ['./start-process.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class StartProcessInstanceComponent implements OnChanges {
|
||||
|
||||
@Input()
|
||||
appId: number;
|
||||
|
||||
@Input()
|
||||
variables: ProcessInstanceVariable[];
|
||||
|
||||
@Output()
|
||||
start: EventEmitter<ProcessInstance> = new EventEmitter<ProcessInstance>();
|
||||
|
||||
@Output()
|
||||
cancel: EventEmitter<ProcessInstance> = new EventEmitter<ProcessInstance>();
|
||||
|
||||
@Output()
|
||||
error: EventEmitter<ProcessInstance> = new EventEmitter<ProcessInstance>();
|
||||
|
||||
@ViewChild(StartFormComponent)
|
||||
startForm: StartFormComponent;
|
||||
|
||||
processDefinitions: ProcessDefinitionRepresentation[] = [];
|
||||
|
||||
name: string;
|
||||
|
||||
currentProcessDef: ProcessDefinitionRepresentation = new ProcessDefinitionRepresentation();
|
||||
|
||||
errorMessageId: string = '';
|
||||
|
||||
constructor(private activitiProcess: ProcessService) {
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
let appIdChange = changes['appId'];
|
||||
let appId = appIdChange ? appIdChange.currentValue : null;
|
||||
this.load(appId);
|
||||
}
|
||||
|
||||
public load(appId?: number) {
|
||||
this.resetSelectedProcessDefinition();
|
||||
this.resetErrorMessage();
|
||||
this.activitiProcess.getProcessDefinitions(appId).subscribe(
|
||||
(res) => {
|
||||
this.processDefinitions = res;
|
||||
},
|
||||
() => {
|
||||
this.errorMessageId = 'ADF_PROCESS_LIST.START_PROCESS.ERROR.LOAD_PROCESS_DEFS';
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public startProcess(outcome?: string) {
|
||||
if (this.currentProcessDef.id && this.name) {
|
||||
this.resetErrorMessage();
|
||||
let formValues = this.startForm ? this.startForm.form.values : undefined;
|
||||
this.activitiProcess.startProcess(this.currentProcessDef.id, this.name, outcome, formValues, this.variables).subscribe(
|
||||
(res) => {
|
||||
this.name = '';
|
||||
this.start.emit(res);
|
||||
},
|
||||
(err) => {
|
||||
this.errorMessageId = 'ADF_PROCESS_LIST.START_PROCESS.ERROR.START';
|
||||
this.error.error(err);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
onProcessDefChange(processDefinitionId) {
|
||||
let processDef = this.processDefinitions.find((processDefinition) => {
|
||||
return processDefinition.id === processDefinitionId;
|
||||
});
|
||||
if (processDef) {
|
||||
this.currentProcessDef = JSON.parse(JSON.stringify(processDef));
|
||||
} else {
|
||||
this.resetSelectedProcessDefinition();
|
||||
}
|
||||
}
|
||||
|
||||
public cancelStartProcess() {
|
||||
this.cancel.emit();
|
||||
}
|
||||
|
||||
hasStartForm() {
|
||||
return this.currentProcessDef && this.currentProcessDef.hasStartForm;
|
||||
}
|
||||
|
||||
isProcessDefinitionEmpty() {
|
||||
return this.processDefinitions ? (this.processDefinitions.length > 0 || this.errorMessageId) : this.errorMessageId;
|
||||
}
|
||||
|
||||
isStartFormMissingOrValid() {
|
||||
if (this.startForm) {
|
||||
return this.startForm.form && this.startForm.form.isValid;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
validateForm() {
|
||||
return this.currentProcessDef.id && this.name && this.isStartFormMissingOrValid();
|
||||
}
|
||||
|
||||
private resetSelectedProcessDefinition() {
|
||||
this.currentProcessDef = new ProcessDefinitionRepresentation();
|
||||
}
|
||||
|
||||
private resetErrorMessage(): void {
|
||||
this.errorMessageId = '';
|
||||
}
|
||||
|
||||
hasErrorMessage() {
|
||||
return this.processDefinitions.length === 0 && !this.errorMessageId;
|
||||
}
|
||||
|
||||
public onOutcomeClick(outcome: string) {
|
||||
this.startProcess(outcome);
|
||||
}
|
||||
|
||||
public reset() {
|
||||
this.resetSelectedProcessDefinition();
|
||||
this.name = '';
|
||||
if (this.startForm) {
|
||||
this.startForm.data = {};
|
||||
}
|
||||
this.resetErrorMessage();
|
||||
}
|
||||
|
||||
hasProcessName(): boolean {
|
||||
return this.name ? true : false;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user