[ADF-584] [ADF-3561] Validation error summary for error component (#3795)

* emit Forms error

* add demo

* add documentation
remove old form tag deprecated in 1.x
fix translation outcome

* remove prevent default validation

* fix lint
This commit is contained in:
Eugenio Romano
2018-09-18 17:40:00 +01:00
committed by GitHub
parent ec633f27d6
commit 219e093d66
20 changed files with 237 additions and 238 deletions

View File

@@ -1,3 +0,0 @@
.form-container {
padding: 10px;
}

View File

@@ -1,6 +1,19 @@
<div class="form-container">
<div class="main-content">
<h1>Form Component</h1>
<div class="form-container">
<adf-form
[showRefreshButton]="false"
[form]="form">
[form]="form"
(formError)="logErrors($event)">
</adf-form>
</div>
<div class="console" #console>
<h3>Error log:</h3>
<p *ngFor="let error of errorFields">Error {{ error.name }} {{error.validationSummary.message | translate}}</p>
</div>
</div>

View File

@@ -0,0 +1,32 @@
.form-container {
padding: 10px;
}
.main-content {
padding: 0 15px;
}
.card-view {
width: 30%;
display: inline-block;
}
.console {
width: 60%;
display: inline-block;
vertical-align: top;
margin-left: 10px;
height: 500px;
overflow: scroll;
padding-bottom: 30px;
h3 {
margin-top: 0;
}
p {
display: block;
font-family: monospace, monospace;
margin: 0;
}
}

View File

@@ -15,45 +15,38 @@
* limitations under the License.
*/
/*!
* @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, Inject, OnInit, ViewEncapsulation } from '@angular/core';
import { FormModel, FormService, FormOutcomeEvent } from '@alfresco/adf-core';
import { Component, Inject, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
import { FormModel, FormFieldModel, FormService, FormOutcomeEvent } from '@alfresco/adf-core';
import { InMemoryFormService } from '../../services/in-memory-form.service';
import { DemoForm } from './demo-form';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-form',
templateUrl: 'form.component.html',
styleUrls: ['form.component.css'],
styleUrls: ['form.component.scss'],
providers: [
{ provide: FormService, useClass: InMemoryFormService }
],
encapsulation: ViewEncapsulation.None
})
export class FormComponent implements OnInit {
export class FormComponent implements OnInit, OnDestroy {
form: FormModel;
errorFields: FormFieldModel[] = [];
private subscriptions: Subscription[] = [];
constructor(@Inject(FormService) private formService: InMemoryFormService) {
this.subscriptions.push(
formService.executeOutcome.subscribe((formOutcomeEvent: FormOutcomeEvent) => {
formOutcomeEvent.preventDefault();
});
})
);
}
logErrors(errorFields: FormFieldModel[]) {
this.errorFields = errorFields;
}
ngOnInit() {
@@ -61,4 +54,9 @@ export class FormComponent implements OnInit {
this.form = this.formService.parseForm(formDefinitionJSON);
}
ngOnDestroy() {
this.subscriptions.forEach(subscription => subscription.unsubscribe());
this.subscriptions = [];
}
}

View File

@@ -29,7 +29,11 @@ import {
Output
} from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { ProcessInstanceFilterRepresentation, Pagination, UserProcessInstanceFilterRepresentation } from 'alfresco-js-api';
import {
ProcessInstanceFilterRepresentation,
Pagination,
UserProcessInstanceFilterRepresentation
} from 'alfresco-js-api';
import {
FORM_FIELD_VALIDATORS, FormEvent, FormFieldEvent, FormRenderingService, FormService,
DynamicTableRow, ValidateDynamicTableRowEvent, AppConfigService, PaginationComponent, UserPreferenceValues
@@ -53,7 +57,7 @@ import {
TaskListComponent
} from '@alfresco/adf-process-services';
import { LogService } from '@alfresco/adf-core';
import { AlfrescoApiService, UserPreferencesService } from '@alfresco/adf-core';
import { AlfrescoApiService, UserPreferencesService, ValidateFormEvent } from '@alfresco/adf-core';
import { Subscription } from 'rxjs';
import { /*CustomEditorComponent*/ CustomStencil01 } from './custom-editor/custom-editor.component';
import { DemoFieldValidator } from './demo-field-validator';
@@ -198,6 +202,10 @@ export class ProcessServiceComponent implements AfterViewInit, OnDestroy, OnInit
formService.formContentClicked.subscribe(content => {
this.showContentPreview(content);
}),
formService.validateForm.subscribe((validateFormEvent: ValidateFormEvent) => {
this.logService.log('Error form:' + validateFormEvent.errorsField);
})
);

View File

@@ -80,6 +80,7 @@ Any content in the body of `<adf-form>` will be shown when no form definition is
| formCompleted | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`FormModel`](../../lib/core/form/components/widgets/core/form.model.ts)`>` | Emitted when the form is submitted with the `Complete` outcome. |
| formContentClicked | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`ContentLinkModel`](../../lib/core/form/components/widgets/core/content-link.model.ts)`>` | Emitted when form content is clicked. |
| formDataRefreshed | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`FormModel`](../../lib/core/form/components/widgets/core/form.model.ts)`>` | Emitted when form values are refreshed due to a data property change. |
| formError | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`FormFieldModel`](../core/form-field.model.md)`[]>` | Emitted when form validations has validation error. |
| formLoaded | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`FormModel`](../../lib/core/form/components/widgets/core/form.model.ts)`>` | Emitted when the form is loaded or reloaded. |
| formSaved | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`FormModel`](../../lib/core/form/components/widgets/core/form.model.ts)`>` | Emitted when the form is submitted with the `Save` or custom outcomes. |
| onError | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<any>` | Emitted when any error occurs. |

View File

@@ -57,6 +57,7 @@ Displays the Start [`Form`](../../lib/process-services/task-list/models/form.mod
| formCompleted | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`FormModel`](../../lib/core/form/components/widgets/core/form.model.ts)`>` | Emitted when the form is submitted with the `Complete` outcome. |
| formContentClicked | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`ContentLinkModel`](../../lib/core/form/components/widgets/core/content-link.model.ts)`>` | Emitted when a field of the form is clicked. |
| formDataRefreshed | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`FormModel`](../../lib/core/form/components/widgets/core/form.model.ts)`>` | Emitted when form values are refreshed due to a data property change. |
| formError | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`FormFieldModel`](../core/form-field.model.md)`[]>` | Emitted when form validations has validation error. |
| formLoaded | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`FormModel`](../../lib/core/form/components/widgets/core/form.model.ts)`>` | Emitted when the form is loaded or reloaded. |
| formSaved | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`FormModel`](../../lib/core/form/components/widgets/core/form.model.ts)`>` | Emitted when the form is submitted with the `Save` or custom outcomes. |
| onError | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<any>` | Emitted when any error occurs. |

View File

@@ -1,79 +0,0 @@
/*!
* @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 AlfrescoApi = require('alfresco-js-api-node');
import TestConfig = require('../test.config');
import fs = require('fs');
import path = require('path');
let buildNumber = process.env.TRAVIS_BUILD_NUMBER;
let saveScreenshot = process.env.SAVE_SCREENSHOT;
describe('Save screenshot at the end', () => {
beforeAll(async (done) => {
if (saveScreenshot === 'true') {
if (!buildNumber) {
buildNumber = Date.now();
}
let alfrescoJsApi = new AlfrescoApi({
provider: 'ECM',
hostEcm: TestConfig.adf.url
});
let files = fs.readdirSync(path.join(__dirname, '../../e2e-output/screenshots'));
if (files && files.length > 0) {
alfrescoJsApi.login(TestConfig.adf.adminEmail, TestConfig.adf.adminPassword);
let folder = await alfrescoJsApi.nodes.addNode('-my-', {
'name': 'insights',
'relativePath': 'Build-screenshot/Screenshot-e2e-' + buildNumber,
'nodeType': 'cm:folder'
}, {}, {
'overwrite': true
});
for (const fileName of files) {
let pathFile = path.join(__dirname, '../../e2e-output/screenshots', fileName);
let file: any = fs.createReadStream(pathFile);
await alfrescoJsApi.upload.uploadFile(
file,
'',
folder.entry.id,
null,
{
'name': file.name,
'nodeType': 'cm:content'
}
);
}
}
}
done();
});
it('screenshot need it', () => {
expect(true).toEqual(true);
});
});

View File

@@ -38,7 +38,7 @@ import { WidgetComponent } from './../widgets/widget.component';
declare var adf: any;
@Component({
selector: 'adf-form-field, form-field',
selector: 'adf-form-field',
template: `
<div [id]="'field-'+field?.id+'-container'"
[hidden]="!field?.isVisible"

View File

@@ -31,7 +31,7 @@
<div *ngIf="!form.hasTabs() && form.hasFields()">
<div *ngFor="let field of form.fields">
<form-field [field]="field.field"></form-field>
<adf-form-field [field]="field.field"></adf-form-field>
</div>
</div>
</mat-card-content>
@@ -43,7 +43,7 @@
[disabled]="!isOutcomeButtonEnabled(outcome)"
[class.adf-form-hide-button]="!isOutcomeButtonVisible(outcome, form.readOnly)"
(click)="onOutcomeClicked(outcome)">
{{outcome.name | uppercase}}
{{outcome.name | uppercase | translate}}
</button>
</mat-card-actions>
</mat-card>

View File

@@ -16,16 +16,24 @@
*/
/* tslint:disable */
import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewEncapsulation } from '@angular/core';
import {
Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit,
Output, SimpleChanges, ViewEncapsulation
} from '@angular/core';
import { FormErrorEvent, FormEvent } from './../events/index';
import { EcmModelService } from './../services/ecm-model.service';
import { FormService } from './../services/form.service';
import { NodeService } from './../services/node.service';
import { ContentLinkModel } from './widgets/core/content-link.model';
import { FormFieldModel, FormModel, FormOutcomeEvent, FormOutcomeModel, FormValues, FormFieldValidator } from './widgets/core/index';
import {
FormFieldModel, FormModel, FormOutcomeEvent, FormOutcomeModel,
FormValues, FormFieldValidator
} from './widgets/core/index';
import { Observable, of } from 'rxjs';
import { WidgetVisibilityService } from './../services/widget-visibility.service';
import { switchMap } from 'rxjs/operators';
import { ValidateFormEvent } from './../events/validate-form.event';
import { Subscription } from 'rxjs';
@Component({
selector: 'adf-form',
@@ -33,14 +41,14 @@ import { switchMap } from 'rxjs/operators';
styleUrls: ['./form.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class FormComponent implements OnInit, OnChanges {
export class FormComponent implements OnInit, OnChanges, OnDestroy {
static SAVE_OUTCOME_ID: string = '$save';
static COMPLETE_OUTCOME_ID: string = '$complete';
static START_PROCESS_OUTCOME_ID: string = '$startProcess';
static CUSTOM_OUTCOME_ID: string = '$custom';
static COMPLETE_BUTTON_COLOR: string = 'primary';
static COMPLETE_OUTCOME_NAME: string ='Complete'
static COMPLETE_OUTCOME_NAME: string = 'Complete'
/** Underlying form model instance. */
@Input()
@@ -138,18 +146,25 @@ export class FormComponent implements OnInit, OnChanges {
@Output()
formDataRefreshed: EventEmitter<FormModel> = new EventEmitter<FormModel>();
/** Emitted when form validations has validation error.*/
@Output()
formError: EventEmitter<FormFieldModel[]> = new EventEmitter<FormFieldModel[]>();
/** Emitted when any outcome is executed. Default behaviour can be prevented
* via `event.preventDefault()`.
*/
@Output()
executeOutcome: EventEmitter<FormOutcomeEvent> = new EventEmitter<FormOutcomeEvent>();
/** @deprecated in 2.4.0, will be renamed in error in 3.x.x */
/** Emitted when any error occurs. */
@Output()
onError: EventEmitter<any> = new EventEmitter<any>();
debugMode: boolean = false;
protected subscriptions: Subscription[] = [];
constructor(protected formService: FormService,
protected visibilityService: WidgetVisibilityService,
private ecmModelService: EcmModelService,
@@ -214,9 +229,21 @@ export class FormComponent implements OnInit, OnChanges {
}
ngOnInit() {
this.subscriptions.push(
this.formService.formContentClicked.subscribe((content: ContentLinkModel) => {
this.formContentClicked.emit(content);
});
}),
this.formService.validateForm.subscribe((validateFormEvent: ValidateFormEvent) => {
if (validateFormEvent.errorsField.length > 0) {
this.formError.next(validateFormEvent.errorsField);
}
})
);
}
ngOnDestroy() {
this.subscriptions.forEach(subscription => subscription.unsubscribe());
this.subscriptions = [];
}
ngOnChanges(changes: SimpleChanges) {
@@ -338,7 +365,7 @@ export class FormComponent implements OnInit, OnChanges {
getFormByTaskId(taskId: string): Promise<FormModel> {
return new Promise<FormModel>((resolve, reject) => {
this.findProcessVariablesByTaskId(taskId).subscribe( (processVariables) => {
this.findProcessVariablesByTaskId(taskId).subscribe((processVariables) => {
this.formService
.getTaskForm(taskId)
.subscribe(

View File

@@ -12,11 +12,12 @@
<div *ngIf="!form.hasTabs() && form.hasFields()">
<div *ngFor="let field of form.fields">
<form-field [field]="field.field"></form-field>
<adf-form-field [field]="field.field"></adf-form-field>
</div>
</div>
</mat-card-content>
<mat-card-content class="adf-start-form-actions" *ngIf="showOutcomeButtons && form.hasOutcomes()" #outcomesContainer>
<mat-card-content class="adf-start-form-actions" *ngIf="showOutcomeButtons && form.hasOutcomes()"
#outcomesContainer>
<ng-content select="[form-custom-button]"></ng-content>
<button *ngFor="let outcome of form.outcomes"
mat-button
@@ -25,7 +26,7 @@
[class.mdl-button--colored]="!outcome.isSystem"
[class.adf-form-hide-button]="!isOutcomeButtonVisible(outcome, form.readOnly)"
(click)="onOutcomeClicked(outcome)">
{{outcome.name}}
{{outcome.name| uppercase | translate}}
</button>
</mat-card-content>
<mat-card-actions *ngIf="showRefreshButton">

View File

@@ -15,30 +15,26 @@
* limitations under the License.
*/
import { Component, ElementRef, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild, ViewEncapsulation, OnDestroy } from '@angular/core';
import {
Component,
ElementRef,
EventEmitter,
Input,
OnChanges,
OnInit,
Output,
SimpleChanges,
ViewChild,
ViewEncapsulation,
OnDestroy
} from '@angular/core';
import { FormService } from './../services/form.service';
import { WidgetVisibilityService } from './../services/widget-visibility.service';
import { FormComponent } from './form.component';
import { ContentLinkModel } from './widgets/core/content-link.model';
import { FormOutcomeModel } from './widgets/core/index';
import { Subscription } from 'rxjs';
import { ValidateFormEvent } from './../events/validate-form.event';
/**
* 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
* {processDefinitionId} string: The process definition ID
* {showOutcomeButtons} boolean: Whether form outcome buttons should be shown, this is now always active to show form outcomes
* @Output
* {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.
* {formCompleted} EventEmitter - This event is fired when the form is completed, it pass all the value in the form.
*
*/
@Component({
selector: 'adf-start-form',
templateUrl: './start-form.component.html',
@@ -47,8 +43,6 @@ import { Subscription } from 'rxjs';
})
export class StartFormComponent extends FormComponent implements OnChanges, OnInit, OnDestroy {
private subscriptions: Subscription[] = [];
/** Definition ID of the process to start. */
@Input()
processDefinitionId: string;
@@ -90,6 +84,11 @@ export class StartFormComponent extends FormComponent implements OnChanges, OnIn
this.subscriptions.push(
this.formService.formContentClicked.subscribe(content => {
this.formContentClicked.emit(content);
}),
this.formService.validateForm.subscribe((validateFormEvent: ValidateFormEvent) => {
if (validateFormEvent.errorsField.length > 0) {
this.formError.next(validateFormEvent.errorsField);
}
})
);
}
@@ -155,8 +154,8 @@ export class StartFormComponent extends FormComponent implements OnChanges, OnIn
/** @override */
isOutcomeButtonVisible(outcome: FormOutcomeModel, isFormReadOnly: boolean): boolean {
if (outcome && outcome.isSystem && ( outcome.name === FormOutcomeModel.SAVE_ACTION ||
outcome.name === FormOutcomeModel.COMPLETE_ACTION )) {
if (outcome && outcome.isSystem && (outcome.name === FormOutcomeModel.SAVE_ACTION ||
outcome.name === FormOutcomeModel.COMPLETE_ACTION)) {
return false;
} else if (outcome && outcome.name === FormOutcomeModel.START_PROCESS_ACTION) {
return true;

View File

@@ -13,7 +13,7 @@
<section class="grid-list" *ngIf="content?.isExpanded">
<div class="grid-list-item" *ngFor="let field of fields" [style.width]="getColumnWith(field)">
<form-field *ngIf="field" [field]="field"></form-field>
<adf-form-field *ngIf="field" [field]="field"></adf-form-field>
</div>
</section>

View File

@@ -271,27 +271,6 @@ describe('FormModel', () => {
form.validateField(field);
});
it('should skip form validation when default behaviour prevented', () => {
const form = new FormModel({}, null, false, formService);
let prevented = false;
formService.validateForm.subscribe((event: ValidateFormEvent) => {
event.isValid = false;
event.preventDefault();
prevented = true;
});
const field = jasmine.createSpyObj('FormFieldModel', ['validate']);
spyOn(form, 'getFormFields').and.returnValue([field]);
form.validateForm();
expect(prevented).toBeTruthy();
expect(form.isValid).toBeFalsy();
expect(field.validate).not.toHaveBeenCalled();
});
it('should skip field validation when default behaviour prevented', () => {
const form = new FormModel({}, null, false, formService);

View File

@@ -15,9 +15,11 @@
* limitations under the License.
*/
/* tslint:disable:component-selector */
/* tslint:disable:component-selector */
import { FormFieldEvent, ValidateFormEvent, ValidateFormFieldEvent } from './../../../events/index';
import { FormFieldEvent } from './../../../events/form-field.event';
import { ValidateFormFieldEvent } from './../../../events/validate-form-field.event';
import { ValidateFormEvent } from './../../../events/validate-form.event';
import { FormService } from './../../../services/form.service';
import { ContainerModel } from './container.model';
import { FormFieldTemplates } from './form-field-templates';
@@ -120,9 +122,21 @@ export class FormModel {
}
if (json.fields) {
let saveOutcome = new FormOutcomeModel(this, { id: FormModel.SAVE_OUTCOME, name: 'Save', isSystem: true });
let completeOutcome = new FormOutcomeModel(this, { id: FormModel.COMPLETE_OUTCOME, name: 'Complete', isSystem: true });
let startProcessOutcome = new FormOutcomeModel(this, { id: FormModel.START_PROCESS_OUTCOME, name: 'Start Process', isSystem: true });
let saveOutcome = new FormOutcomeModel(this, {
id: FormModel.SAVE_OUTCOME,
name: 'Save',
isSystem: true
});
let completeOutcome = new FormOutcomeModel(this, {
id: FormModel.COMPLETE_OUTCOME,
name: 'Complete',
isSystem: true
});
let startProcessOutcome = new FormOutcomeModel(this, {
id: FormModel.START_PROCESS_OUTCOME,
name: 'Start Process',
isSystem: true
});
let customOutcomes = (json.outcomes || []).map(obj => new FormOutcomeModel(this, obj));
@@ -176,27 +190,27 @@ export class FormModel {
* @memberof FormModel
*/
validateForm(): void {
const validateFormEvent = new ValidateFormEvent(this);
const validateFormEvent: any = new ValidateFormEvent(this);
if (this.formService) {
this.formService.validateForm.next(validateFormEvent);
}
let errorsField: FormFieldModel[] = [];
this._isValid = validateFormEvent.isValid;
if (validateFormEvent.defaultPrevented) {
return;
}
if (validateFormEvent.isValid) {
let fields = this.getFormFields();
for (let i = 0; i < fields.length; i++) {
if (!fields[i].validate()) {
errorsField.push(fields[i]);
}
}
if (errorsField.length > 0) {
this._isValid = false;
return;
}
}
if (this.formService) {
validateFormEvent.isValid = this._isValid;
validateFormEvent.errorsField = errorsField;
this.formService.validateForm.next(validateFormEvent);
}
}
/**
@@ -227,8 +241,8 @@ export class FormModel {
if (!field.validate()) {
this._isValid = false;
return;
}
this.validateForm();
}

View File

@@ -2,7 +2,7 @@
<mat-tab-group>
<mat-tab *ngFor="let tab of visibleTabs" [label]="tab.title">
<div *ngFor="let field of tab.fields">
<form-field [field]="field.field"></form-field>
<adf-form-field [field]="field.field"></adf-form-field>
</div>
</mat-tab>
</mat-tab-group>

View File

@@ -17,10 +17,12 @@
import { FormModel } from './../components/widgets/core/index';
import { FormEvent } from './form.event';
import { FormFieldModel } from '../components/widgets/core/form-field.model';
export class ValidateFormEvent extends FormEvent {
isValid = true;
errorsField: FormFieldModel[] = [];
constructor(form: FormModel) {
super(form);

View File

@@ -1,4 +1,7 @@
{
"SAVE": "SAVE",
"START": "COMPLETE",
"START PROCESS": "START PROCESS",
"FORM": {
"START_FORM": {
"TITLE": "Start Form"

View File

@@ -1,4 +1,7 @@
{
"SAVE": "SALVA",
"COMPLETE": "COMPLETA",
"START PROCESS": "INIZIA PROCESSO",
"FORM": {
"START_FORM": {
"TITLE": "Modulo di inizio"