Process start form validates submit state of start process form

Refs #730
This commit is contained in:
Will Abson 2016-11-03 18:29:52 +00:00
parent 2637d4286a
commit fef11f2a59
11 changed files with 249 additions and 60 deletions

View File

@ -0,0 +1,28 @@
/*!
* @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 { Observable } from 'rxjs/Rx';
export class TranslationMock {
get(key: string|Array<string>, interpolateParams?: Object): Observable<string|any> {
return Observable.of(key);
}
addTranslationFolder() {
}
}

View File

@ -14,7 +14,7 @@
<container-widget *ngFor="let field of form.fields" [content]="field" (formValueChanged)="checkVisibility($event);"></container-widget> <container-widget *ngFor="let field of form.fields" [content]="field" (formValueChanged)="checkVisibility($event);"></container-widget>
</div> </div>
</div> </div>
<div *ngIf="form.hasOutcomes()" class="mdl-card__actions mdl-card--border"> <div *ngIf="showOutcomeButtons && form.hasOutcomes()" class="mdl-card__actions mdl-card--border" #outcomesContainer>
<button *ngFor="let outcome of form.outcomes" <button *ngFor="let outcome of form.outcomes"
alfresco-mdl-button alfresco-mdl-button
[disabled]="!isOutcomeButtonEnabled(outcome)" [disabled]="!isOutcomeButtonEnabled(outcome)"

View File

@ -0,0 +1,134 @@
/*!
* @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 { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { Observable } from 'rxjs/Rx';
import { ActivitiStartForm } from './activiti-start-form.component';
import { WIDGET_DIRECTIVES } from './widgets/index';
import { FormService } from './../services/form.service';
import { EcmModelService } from './../services/ecm-model.service';
import { WidgetVisibilityService } from './../services/widget-visibility.service';
import { AlfrescoTranslationService, CoreModule } from 'ng2-alfresco-core';
import { TranslationMock } from './../assets/translation.service.mock';
describe('ActivitiStartForm', () => {
let componentHandler: any;
let formService: FormService;
let component: ActivitiStartForm;
let fixture: ComponentFixture<ActivitiStartForm>;
let getStartFormSpy: jasmine.Spy;
const exampleId1 = 'my:process1';
const exampleId2 = 'my:process2';
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [ CoreModule ],
declarations: [
ActivitiStartForm,
...WIDGET_DIRECTIVES
],
providers: [
{ provide: AlfrescoTranslationService, useClass: TranslationMock },
EcmModelService,
FormService,
WidgetVisibilityService
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ActivitiStartForm);
component = fixture.componentInstance;
formService = fixture.debugElement.injector.get(FormService);
getStartFormSpy = spyOn(formService, 'getStartFormDefinition').and.returnValue(Observable.of({
processDefinitionName: 'my:process'
}));
componentHandler = jasmine.createSpyObj('componentHandler', [
'upgradeAllRegistered',
'upgradeElement'
]);
window['componentHandler'] = componentHandler;
});
it('should load start form on init if processDefinitionId defined', () => {
component.processDefinitionId = exampleId1;
component.ngOnInit();
expect(formService.getStartFormDefinition).toHaveBeenCalled();
});
it('should load not start form on init if no processDefinitionId defined', () => {
component.ngOnInit();
expect(formService.getStartFormDefinition).not.toHaveBeenCalled();
});
it('should load start form when processDefinitionId changed', () => {
component.processDefinitionId = exampleId1;
component.ngOnChanges({processDefinitionId: new SimpleChange(exampleId1, exampleId2)});
expect(formService.getStartFormDefinition).toHaveBeenCalled();
});
it('should not load start form when changes notified but no change to processDefinitionId', () => {
component.processDefinitionId = exampleId1;
component.ngOnChanges({otherProp: new SimpleChange(exampleId1, exampleId2)});
expect(formService.getStartFormDefinition).not.toHaveBeenCalled();
});
it('should consume errors encountered when loading start form', () => {
getStartFormSpy.and.returnValue(Observable.throw({}));
component.processDefinitionId = exampleId1;
component.ngOnInit();
});
it('should not show outcome buttons by default', () => {
getStartFormSpy.and.returnValue(Observable.of({
id: '1',
processDefinitionName: 'my:process',
outcomes: [{
id: 'approve',
name: 'Approve'
}]
}));
component.processDefinitionId = exampleId1;
component.ngOnInit();
fixture.detectChanges();
expect(component.outcomesContainer).not.toBeTruthy();
});
it('should show outcome buttons if showOutcomeButtons is true', () => {
getStartFormSpy.and.returnValue(Observable.of({
id: '1',
processDefinitionName: 'my:process',
outcomes: [{
id: 'approve',
name: 'Approve'
}]
}));
component.processDefinitionId = exampleId1;
component.showOutcomeButtons = true;
component.ngOnInit();
fixture.detectChanges();
expect(component.outcomesContainer).toBeTruthy();
});
});

View File

@ -19,23 +19,26 @@ import {
Component, Component,
OnInit, AfterViewChecked, OnChanges, OnInit, AfterViewChecked, OnChanges,
SimpleChanges, SimpleChanges,
Input Input,
ViewChild,
ElementRef
} from '@angular/core'; } from '@angular/core';
import { AlfrescoTranslationService } from 'ng2-alfresco-core';
import { ActivitiForm } from './activiti-form.component'; import { ActivitiForm } from './activiti-form.component';
import { FormService } from './../services/form.service'; import { FormService } from './../services/form.service';
import { WidgetVisibilityService } from './../services/widget-visibility.service'; import { WidgetVisibilityService } from './../services/widget-visibility.service';
/** /**
* Displays the start form for a named process definition, which can be used to retrieve values to start a new process.
*
* After the form has been completed the form values are available from the attribute component.form.values and
* component.form.isValid (boolean) can be used to check the if the form is valid or not. Both of these properties are
* updated as the user types into the form.
*
* @Input * @Input
* ActivitiForm can show 4 types of forms searching by 4 type of params: * {processDefinitionId} string: The process definition ID
* 1) Form attached to a task passing the {taskId}. * {showOutcomeButtons} boolean: Whether form outcome buttons should be shown, as yet these don't do anything so this
* * is false by default
* 2) Form that are only defined with the {formId} (in this case you receive only the form definition and the form will not be
* attached to any process, useful in case you want to use ActivitiForm as form designer), in this case you can pass also other 2
* parameters:
* - {saveOption} as parameter to tell what is the function to call on the save action.
* - {data} to fill the form field with some data, the id of the form must to match the name of the field of the provided data object.
*
* @Output * @Output
* {formLoaded} EventEmitter - This event is fired when the form is loaded, it pass all the value in the form. * {formLoaded} EventEmitter - This event is fired when the form is loaded, it pass all the value in the form.
* {formSaved} EventEmitter - This event is fired when the form is saved, it pass all the value in the form. * {formSaved} EventEmitter - This event is fired when the form is saved, it pass all the value in the form.
@ -52,20 +55,30 @@ import { WidgetVisibilityService } from './../services/widget-visibility.servic
export class ActivitiStartForm extends ActivitiForm implements OnInit, AfterViewChecked, OnChanges { export class ActivitiStartForm extends ActivitiForm implements OnInit, AfterViewChecked, OnChanges {
@Input() @Input()
processId: string; processDefinitionId: string;
constructor(formService: FormService, @Input()
showOutcomeButtons: boolean = false;
@ViewChild('outcomesContainer', {})
outcomesContainer: ElementRef = null;
constructor(private translate: AlfrescoTranslationService,
formService: FormService,
visibilityService: WidgetVisibilityService) { visibilityService: WidgetVisibilityService) {
super(formService, visibilityService, null, null); super(formService, visibilityService, null, null);
} }
ngOnInit() { ngOnInit() {
this.loadForm(); this.loadForm();
if (this.translate) {
this.translate.addTranslationFolder('node_modules/ng2-activiti-form/src');
}
} }
ngOnChanges(changes: SimpleChanges) { ngOnChanges(changes: SimpleChanges) {
console.log('changes', changes); let processId = changes['processDefinitionId'];
let processId = changes['processId'];
if (processId && processId.currentValue) { if (processId && processId.currentValue) {
this.getStartFormDefinition(processId.currentValue); this.getStartFormDefinition(processId.currentValue);
return; return;
@ -73,8 +86,8 @@ export class ActivitiStartForm extends ActivitiForm implements OnInit, AfterView
} }
loadForm() { loadForm() {
if (this.processId) { if (this.processDefinitionId) {
this.getStartFormDefinition(this.formId); this.getStartFormDefinition(this.processDefinitionId);
return; return;
} }
} }

View File

@ -0,0 +1,7 @@
{
"FORM": {
"START_FORM": {
"TITLE": "Start Form"
}
}
}

View File

@ -229,6 +229,22 @@ describe('FormService', () => {
}); });
}); });
it('should get start form definition by process definition id', (done) => {
responseBody = {id: 1};
service.getStartFormDefinition('myprocess:1').subscribe(result => {
expect(jasmine.Ajax.requests.mostRecent().url.endsWith('/process-definitions/myprocess%3A1/start-form')).toBe(true);
expect(result.id).toEqual(responseBody.id);
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
'status': 200,
contentType: 'application/json',
responseText: JSON.stringify(responseBody)
});
});
it('should not get form id from response', () => { it('should not get form id from response', () => {
let response = new Response(new ResponseOptions({body: null})); let response = new Response(new ResponseOptions({body: null}));
expect(service.getFormId(response)).toBeNull(); expect(service.getFormId(response)).toBeNull();

View File

@ -213,9 +213,7 @@ export class FormService {
*/ */
getStartFormDefinition(processId: string): Observable<any> { getStartFormDefinition(processId: string): Observable<any> {
return Observable.fromPromise( return Observable.fromPromise(
this.apiService.getInstance().activiti.processApi.getProcessDefinitionStartForm({ this.apiService.getInstance().activiti.processApi.getProcessDefinitionStartForm(processId))
processDefinitionId: processId
}))
.map(this.toJson) .map(this.toJson)
.catch(this.handleError); .catch(this.handleError);
} }

View File

@ -9,3 +9,7 @@
.material-icons:hover { .material-icons:hover {
color: rgb(255, 152, 0); color: rgb(255, 152, 0);
} }
.mdl-dialog {
width: 400px;
}

View File

@ -15,20 +15,16 @@
<input class="mdl-textfield__input" type="text" [(ngModel)]="name" id="processName" /> <input class="mdl-textfield__input" type="text" [(ngModel)]="name" id="processName" />
<label class="mdl-textfield__label" for="processName">{{'START_PROCESS.DIALOG.LABEL.NAME'|translate}}</label> <label class="mdl-textfield__label" for="processName">{{'START_PROCESS.DIALOG.LABEL.NAME'|translate}}</label>
</div> </div>
<activiti-start-form *ngIf="hasFormKey()" [processId]="processDefinitionId" <activiti-start-form *ngIf="hasStartForm()" [processDefinitionId]="processDefinitionId"
(formSaved)='onFormSaved($event)' (formSaved)='onFormSaved($event)'
(formCompleted)='onFormCompleted($event)' (formCompleted)='onFormCompleted($event)'
(formLoaded)='onFormLoaded($event)' (formLoaded)='onFormLoaded($event)'
(onError)='onFormError($event)' (onError)='onFormError($event)'
(executeOutcome)='onExecuteOutcome($event)'
#startForm> #startForm>
</activiti-start-form> </activiti-start-form>
<button type="button" class="mdl-button" *ngIf="!hasFormKey()" (click)="onFormCompleted()">
{{ 'TASK_DETAILS.BUTTON.COMPLETE' | translate }}
</button>
</div> </div>
<div class="mdl-dialog__actions"> <div class="mdl-dialog__actions">
<button type="button" (click)="startProcess()" class="mdl-button">{{'START_PROCESS.DIALOG.ACTION.START'|translate}}</button> <button type="button" [disabled]="!validateForm()" (click)="startProcess()" class="mdl-button">{{'START_PROCESS.DIALOG.ACTION.START'|translate}}</button>
<button type="button" (click)="cancel()" class="mdl-button close">{{'START_PROCESS.DIALOG.ACTION.CANCEL'|translate}}</button> <button type="button" (click)="cancel()" class="mdl-button close">{{'START_PROCESS.DIALOG.ACTION.CANCEL'|translate}}</button>
</div> </div>
</dialog> </dialog>

View File

@ -15,7 +15,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { Component, Input, OnInit, OnChanges, SimpleChanges, ViewChild } from '@angular/core'; import { Component, Input, OnInit, ViewChild } from '@angular/core';
import { AlfrescoTranslationService } from 'ng2-alfresco-core'; import { AlfrescoTranslationService } from 'ng2-alfresco-core';
import { ActivitiStartForm } from 'ng2-activiti-form'; import { ActivitiStartForm } from 'ng2-activiti-form';
import { ActivitiProcessService } from './../services/activiti-process.service'; import { ActivitiProcessService } from './../services/activiti-process.service';
@ -28,7 +28,7 @@ declare let componentHandler: any;
templateUrl: './activiti-start-process.component.html', templateUrl: './activiti-start-process.component.html',
styleUrls: ['./activiti-start-process.component.css'] styleUrls: ['./activiti-start-process.component.css']
}) })
export class ActivitiStartProcessButton implements OnInit, OnChanges { export class ActivitiStartProcessButton implements OnInit {
@Input() @Input()
appId: string; appId: string;
@ -56,10 +56,6 @@ export class ActivitiStartProcessButton implements OnInit, OnChanges {
this.load(this.appId); this.load(this.appId);
} }
ngOnChanges(changes: SimpleChanges) {
console.log('changes', changes);
}
public load(appId: string) { public load(appId: string) {
this.activitiProcess.getProcessDefinitions(this.appId).subscribe( this.activitiProcess.getProcessDefinitions(this.appId).subscribe(
(res: any[]) => { (res: any[]) => {
@ -79,7 +75,8 @@ export class ActivitiStartProcessButton implements OnInit, OnChanges {
public startProcess() { public startProcess() {
if (this.processDefinitionId && this.name) { if (this.processDefinitionId && this.name) {
this.activitiProcess.startProcess(this.processDefinitionId, this.name).subscribe( let formValues = this.startForm ? this.startForm.form.values : undefined;
this.activitiProcess.startProcess(this.processDefinitionId, this.name, formValues).subscribe(
(res: any) => { (res: any) => {
this.cancel(); this.cancel();
}, },
@ -96,30 +93,22 @@ export class ActivitiStartProcessButton implements OnInit, OnChanges {
} }
} }
hasFormKey() { private getSelectedProcess(): any {
return true; return this.processDefinitions.filter((processDefinition) => {
return processDefinition.id === this.processDefinitionId;
})[0];
} }
onFormSaved($event: Event) { hasStartForm() {
$event.preventDefault(); let selectedProcessDefinition = this.getSelectedProcess();
console.log('form saved'); return selectedProcessDefinition && selectedProcessDefinition.hasStartForm;
} }
onFormCompleted($event: Event) { isStartFormMissingOrValid() {
$event.preventDefault(); return !this.startForm || this.startForm.form.isValid;
console.log('form saved');
} }
onExecuteOutcome($event: Event) { validateForm() {
$event.preventDefault(); return this.processDefinitionId && this.name && this.isStartFormMissingOrValid();
console.log('form outcome executed');
}
onFormLoaded($event: Event) {
console.log('form loaded', $event);
}
onFormError($event: Event) {
console.log('form error', $event);
} }
} }

View File

@ -226,10 +226,14 @@ export class ActivitiProcessService {
.catch(this.handleError); .catch(this.handleError);
} }
startProcess(processDefinitionId: string, name: string) { startProcess(processDefinitionId: string, name: string, startFormValues?: any) {
let startRequest: any = {}; let startRequest: any = {
startRequest.name = name; name: name,
startRequest.processDefinitionId = processDefinitionId; processDefinitionId: processDefinitionId
};
if (startFormValues) {
startRequest.values = startFormValues;
}
return Observable.fromPromise( return Observable.fromPromise(
this.authService.getAlfrescoApi().activiti.processApi.startNewProcessInstance(startRequest) this.authService.getAlfrescoApi().activiti.processApi.startNewProcessInstance(startRequest)
) )