mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2025-07-24 17:32:15 +00:00
[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:
@@ -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"
|
||||
|
@@ -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>
|
||||
|
@@ -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.formService.formContentClicked.subscribe((content: ContentLinkModel) => {
|
||||
this.formContentClicked.emit(content);
|
||||
});
|
||||
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,12 +365,12 @@ 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(
|
||||
form => {
|
||||
const parsedForm = this.parseForm(form);
|
||||
const parsedForm = this.parseForm(form);
|
||||
this.visibilityService.refreshVisibility(parsedForm);
|
||||
parsedForm.validateForm();
|
||||
this.form = parsedForm;
|
||||
@@ -364,16 +391,16 @@ export class FormComponent implements OnInit, OnChanges {
|
||||
this.formService
|
||||
.getFormDefinitionById(formId)
|
||||
.subscribe(
|
||||
form => {
|
||||
this.formName = form.name;
|
||||
this.form = this.parseForm(form);
|
||||
this.visibilityService.refreshVisibility(this.form);
|
||||
this.form.validateForm();
|
||||
this.onFormLoaded(this.form);
|
||||
},
|
||||
(error) => {
|
||||
this.handleError(error);
|
||||
}
|
||||
form => {
|
||||
this.formName = form.name;
|
||||
this.form = this.parseForm(form);
|
||||
this.visibilityService.refreshVisibility(this.form);
|
||||
this.form.validateForm();
|
||||
this.onFormLoaded(this.form);
|
||||
},
|
||||
(error) => {
|
||||
this.handleError(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -381,22 +408,22 @@ export class FormComponent implements OnInit, OnChanges {
|
||||
this.formService
|
||||
.getFormDefinitionByName(formName)
|
||||
.subscribe(
|
||||
id => {
|
||||
this.formService.getFormDefinitionById(id).subscribe(
|
||||
form => {
|
||||
this.form = this.parseForm(form);
|
||||
this.visibilityService.refreshVisibility(this.form);
|
||||
this.form.validateForm();
|
||||
this.onFormLoaded(this.form);
|
||||
},
|
||||
(error) => {
|
||||
this.handleError(error);
|
||||
}
|
||||
);
|
||||
},
|
||||
(error) => {
|
||||
this.handleError(error);
|
||||
}
|
||||
id => {
|
||||
this.formService.getFormDefinitionById(id).subscribe(
|
||||
form => {
|
||||
this.form = this.parseForm(form);
|
||||
this.visibilityService.refreshVisibility(this.form);
|
||||
this.form.validateForm();
|
||||
this.onFormLoaded(this.form);
|
||||
},
|
||||
(error) => {
|
||||
this.handleError(error);
|
||||
}
|
||||
);
|
||||
},
|
||||
(error) => {
|
||||
this.handleError(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -405,11 +432,11 @@ export class FormComponent implements OnInit, OnChanges {
|
||||
this.formService
|
||||
.saveTaskForm(this.form.taskId, this.form.values)
|
||||
.subscribe(
|
||||
() => {
|
||||
this.onTaskSaved(this.form);
|
||||
this.storeFormAsMetadata();
|
||||
},
|
||||
error => this.onTaskSavedError(this.form, error)
|
||||
() => {
|
||||
this.onTaskSaved(this.form);
|
||||
this.storeFormAsMetadata();
|
||||
},
|
||||
error => this.onTaskSavedError(this.form, error)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -419,11 +446,11 @@ export class FormComponent implements OnInit, OnChanges {
|
||||
this.formService
|
||||
.completeTaskForm(this.form.taskId, this.form.values, outcome)
|
||||
.subscribe(
|
||||
() => {
|
||||
this.onTaskCompleted(this.form);
|
||||
this.storeFormAsMetadata();
|
||||
},
|
||||
error => this.onTaskCompletedError(this.form, error)
|
||||
() => {
|
||||
this.onTaskCompleted(this.form);
|
||||
this.storeFormAsMetadata();
|
||||
},
|
||||
error => this.onTaskCompletedError(this.form, error)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -470,9 +497,9 @@ export class FormComponent implements OnInit, OnChanges {
|
||||
|
||||
private loadFormForEcmNode(nodeId: string): void {
|
||||
this.nodeService.getNodeMetadata(nodeId).subscribe(data => {
|
||||
this.data = data.metadata;
|
||||
this.loadFormFromActiviti(data.nodeType);
|
||||
},
|
||||
this.data = data.metadata;
|
||||
this.loadFormFromActiviti(data.nodeType);
|
||||
},
|
||||
this.handleError);
|
||||
}
|
||||
|
||||
@@ -501,8 +528,8 @@ export class FormComponent implements OnInit, OnChanges {
|
||||
private storeFormAsMetadata() {
|
||||
if (this.saveMetadata) {
|
||||
this.ecmModelService.createEcmTypeForActivitiForm(this.formName, this.form).subscribe(type => {
|
||||
this.nodeService.createNodeMetadata(type.nodeType || type.entry.prefixedName, EcmModelService.MODEL_NAMESPACE, this.form.values, this.path, this.nameNode);
|
||||
},
|
||||
this.nodeService.createNodeMetadata(type.nodeType || type.entry.prefixedName, EcmModelService.MODEL_NAMESPACE, this.form.values, this.path, this.nameNode);
|
||||
},
|
||||
(error) => {
|
||||
this.handleError(error);
|
||||
}
|
||||
|
@@ -12,12 +12,13 @@
|
||||
|
||||
<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>
|
||||
<ng-content select="[form-custom-button]"></ng-content>
|
||||
<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
|
||||
[attr.data-automation-id]="'adf-form-' + outcome.name | lowercase"
|
||||
@@ -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">
|
||||
|
@@ -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;
|
||||
|
@@ -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>
|
||||
|
||||
|
@@ -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);
|
||||
|
||||
|
@@ -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);
|
||||
|
||||
let errorsField: FormFieldModel[] = [];
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (this.formService) {
|
||||
validateFormEvent.isValid = this._isValid;
|
||||
validateFormEvent.errorsField = errorsField;
|
||||
this.formService.validateForm.next(validateFormEvent);
|
||||
}
|
||||
|
||||
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()) {
|
||||
this._isValid = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -227,8 +241,8 @@ export class FormModel {
|
||||
|
||||
if (!field.validate()) {
|
||||
this._isValid = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.validateForm();
|
||||
}
|
||||
|
||||
|
@@ -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>
|
||||
|
@@ -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);
|
||||
|
Reference in New Issue
Block a user