New packages org (#2639)

New packages org
This commit is contained in:
Eugenio Romano
2017-11-16 14:12:52 +00:00
committed by GitHub
parent 6a24c6ef75
commit a52bb5600a
1984 changed files with 17179 additions and 40423 deletions

View File

@@ -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) {
}
}

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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;
}
}
}

View File

@@ -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);
});
});

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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();
});
});
});

View File

@@ -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});
}
}

View File

@@ -0,0 +1,7 @@
:host {
width: 100%;
}
.adf-card-container {
font-family: inherit;
}

View File

@@ -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>

View File

@@ -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');
});
});

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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();
});
});
});

View File

@@ -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();
}
}

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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});
});
});
});

View File

@@ -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));
}
}

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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();
});
});
});
});

View File

@@ -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;
}
}