Merge pull request #752 from Alfresco/dev-denys-726

#726 Form Validation
This commit is contained in:
Maurizio Vitale
2016-09-15 21:44:03 +01:00
committed by GitHub
44 changed files with 1327 additions and 85 deletions

View File

@@ -4,8 +4,9 @@
</div>
<div *ngIf="hasForm()">
<div class="mdl-card mdl-shadow--2dp activiti-form-container">
<div *ngIf="isTitleEnabled()" class="mdl-card__title">
<h2 class="mdl-card__title-text">{{form.taskName}}</h2>
<div class="mdl-card__title">
<i class="material-icons">{{ form.isValid ? 'event_available' : 'event_busy' }}</i>
<h2 *ngIf="isTitleEnabled()" class="mdl-card__title-text">{{form.taskName}}</h2>
</div>
<div class="mdl-card__media">
<div *ngIf="form.hasTabs()">
@@ -19,9 +20,9 @@
<div *ngIf="form.hasOutcomes()" class="mdl-card__actions mdl-card--border">
<button *ngFor="let outcome of form.outcomes"
alfresco-mdl-button
[disabled]="readOnly"
[disabled]="!isOutcomeButtonEnabled(outcome)"
[class.mdl-button--colored]="!outcome.isSystem"
[class.activiti-form-hide-button]="!isOutcomeButtonEnabled(outcome)"
[class.activiti-form-hide-button]="!isOutcomeButtonVisible(outcome)"
(click)="onOutcomeClicked(outcome, $event)">
{{outcome.name}}
</button>

View File

@@ -22,7 +22,7 @@ import { ActivitiForm } from './activiti-form.component';
import { FormModel, FormOutcomeModel, FormFieldModel, FormOutcomeEvent } from './widgets/index';
import { FormService } from './../services/form.service';
import { WidgetVisibilityService } from './../services/widget-visibility.service';
import { ContainerWidget } from './widgets/container/container.widget';
// import { ContainerWidget } from './widgets/container/container.widget';
describe('ActivitiForm', () => {
@@ -98,13 +98,13 @@ describe('ActivitiForm', () => {
});
it('should not enable outcome button when model missing', () => {
expect(formComponent.isOutcomeButtonEnabled(null)).toBeFalsy();
expect(formComponent.isOutcomeButtonVisible(null)).toBeFalsy();
});
it('should enable custom outcome buttons', () => {
let formModel = new FormModel();
let outcome = new FormOutcomeModel(formModel, { id: 'action1', name: 'Action 1' });
expect(formComponent.isOutcomeButtonEnabled(outcome)).toBeTruthy();
expect(formComponent.isOutcomeButtonVisible(outcome)).toBeTruthy();
});
@@ -113,10 +113,10 @@ describe('ActivitiForm', () => {
let outcome = new FormOutcomeModel(formModel, { id: '$save', name: FormOutcomeModel.SAVE_ACTION });
formComponent.showSaveButton = true;
expect(formComponent.isOutcomeButtonEnabled(outcome)).toBeTruthy();
expect(formComponent.isOutcomeButtonVisible(outcome)).toBeTruthy();
formComponent.showSaveButton = false;
expect(formComponent.isOutcomeButtonEnabled(outcome)).toBeFalsy();
expect(formComponent.isOutcomeButtonVisible(outcome)).toBeFalsy();
});
it('should allow controlling [save] button visibility', () => {
@@ -124,10 +124,10 @@ describe('ActivitiForm', () => {
let outcome = new FormOutcomeModel(formModel, { id: '$save', name: FormOutcomeModel.COMPLETE_ACTION });
formComponent.showCompleteButton = true;
expect(formComponent.isOutcomeButtonEnabled(outcome)).toBeTruthy();
expect(formComponent.isOutcomeButtonVisible(outcome)).toBeTruthy();
formComponent.showCompleteButton = false;
expect(formComponent.isOutcomeButtonEnabled(outcome)).toBeFalsy();
expect(formComponent.isOutcomeButtonVisible(outcome)).toBeFalsy();
});
it('should load form on refresh', () => {
@@ -571,6 +571,7 @@ describe('ActivitiForm', () => {
expect(formComponent.getFormDefinitionOutcomes).toHaveBeenCalledWith(form);
});
/*
it('should update the visibility when the container raise the change event', (valueChanged) => {
spyOn(formComponent, 'checkVisibility').and.callThrough();
let widget = new ContainerWidget();
@@ -581,6 +582,7 @@ describe('ActivitiForm', () => {
expect(formComponent.checkVisibility).toHaveBeenCalledWith(fakeField);
});
*/
it('should prevent default outcome execution', () => {

View File

@@ -128,7 +128,7 @@ export class ActivitiForm implements OnInit, AfterViewChecked, OnChanges {
showSaveButton: boolean = true;
@Input()
showDebugButton: boolean = false;
showDebugButton: boolean = true;
@Input()
readOnly: boolean = false;
@@ -175,6 +175,21 @@ export class ActivitiForm implements OnInit, AfterViewChecked, OnChanges {
}
isOutcomeButtonEnabled(outcome: FormOutcomeModel): boolean {
if (this.form.readOnly) {
return false;
}
if (outcome) {
// Make 'Save' button always available
if (outcome.name === FormOutcomeModel.SAVE_ACTION) {
return true;
}
return this.form.isValid;
}
return false;
}
isOutcomeButtonVisible(outcome: FormOutcomeModel): boolean {
if (outcome && outcome.name) {
if (outcome.name === FormOutcomeModel.COMPLETE_ACTION) {
return this.showCompleteButton;

View File

@@ -1,6 +1,7 @@
<label class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect" [attr.for]="field.id">
<input type="checkbox"
[attr.id]="field.id"
[attr.required]="isRequired()"
class="mdl-checkbox__input"
[(ngModel)]="field.value"
(ngModelChange)="checkVisibility(field)"

View File

@@ -17,6 +17,7 @@
import { it, describe, expect } from '@angular/core/testing';
import { ContainerColumnModel } from './container-column.model';
import { FormModel } from './form.model';
import { FormFieldModel } from './form-field.model';
describe('ContainerColumnModel', () => {
@@ -35,7 +36,7 @@ describe('ContainerColumnModel', () => {
column.fields = [];
expect(column.hasFields()).toBeFalsy();
column.fields = [new FormFieldModel(null, null)];
column.fields = [new FormFieldModel(new FormModel(), null)];
expect(column.hasFields()).toBeTruthy();
});

View File

@@ -30,6 +30,7 @@ export class FormFieldTypes {
static FUNCTIONAL_GROUP: string = 'functional-group';
static PEOPLE: string = 'people';
static BOOLEAN: string = 'boolean';
static NUMBER: string = 'integer';
static READONLY_TYPES: string[] = [
FormFieldTypes.HYPERLINK,

View File

@@ -0,0 +1,493 @@
/*!
* @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 { it, describe, expect } from '@angular/core/testing';
import { FormModel } from './form.model';
import { FormFieldModel } from './form-field.model';
import { FormFieldOption } from './form-field-option';
import { FormFieldTypes } from './form-field-types';
import {
RequiredFieldValidator,
NumberFieldValidator,
MinLengthFieldValidator,
MaxLengthFieldValidator,
MinValueFieldValidator,
MaxValueFieldValidator,
RegExFieldValidator
} from './form-field-validator';
describe('FormFieldValidator', () => {
describe('RequiredFieldValidator', () => {
let validator: RequiredFieldValidator;
beforeEach(() => {
validator = new RequiredFieldValidator();
});
it('should require [required] setting', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.TEXT,
value: '<value>'
});
field.required = false;
expect(validator.isSupported(field)).toBeFalsy();
expect(validator.validate(field)).toBeTruthy();
field.required = true;
expect(validator.isSupported(field)).toBeTruthy();
expect(validator.validate(field)).toBeTruthy();
});
it('should skip unsupported type', () => {
let field = new FormFieldModel(new FormModel(), { type: 'wrong-type' });
expect(validator.validate(field)).toBeTruthy();
});
it('should fail for dropdown with empty value', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DROPDOWN,
value: '<empty>',
hasEmptyValue: true,
required: true
});
field.emptyOption = <FormFieldOption> { id: '<empty>' };
expect(validator.validate(field)).toBeFalsy();
field.value = '<non-empty>';
expect(validator.validate(field)).toBeTruthy();
});
it('should fail for radio buttons', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.RADIO_BUTTONS,
required: true,
value: 'one',
options: [{ id: 'two', name: 'two' }]
});
expect(validator.validate(field)).toBeFalsy();
});
it('should succeed for radio buttons', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.RADIO_BUTTONS,
required: true,
value: 'two',
options: [{ id: 'two', name: 'two' }]
});
expect(validator.validate(field)).toBeTruthy();
});
it('should fail for upload', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.UPLOAD,
value: null,
required: true
});
field.value = null;
expect(validator.validate(field)).toBeFalsy();
field.value = [];
expect(validator.validate(field)).toBeFalsy();
});
it('should succeed for upload', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.UPLOAD,
value: [{}],
required: true
});
expect(validator.validate(field)).toBeTruthy();
});
it('should fail for text', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.TEXT,
value: null,
required: true
});
field.value = null;
expect(validator.validate(field)).toBeFalsy();
field.value = '';
expect(validator.validate(field)).toBeFalsy();
});
it('should succeed for text', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.TEXT,
value: '<value>',
required: true
});
expect(validator.validate(field)).toBeTruthy();
});
});
describe('NumberFieldValidator', () => {
let validator: NumberFieldValidator;
beforeEach(() => {
validator = new NumberFieldValidator();
});
it('should verify number', () => {
expect(NumberFieldValidator.isNumber('1')).toBeTruthy();
expect(NumberFieldValidator.isNumber('1.0')).toBeTruthy();
expect(NumberFieldValidator.isNumber('-1')).toBeTruthy();
});
it('should not verify number', () => {
expect(NumberFieldValidator.isNumber(null)).toBeFalsy();
expect(NumberFieldValidator.isNumber(undefined)).toBeFalsy();
expect(NumberFieldValidator.isNumber('')).toBeFalsy();
expect(NumberFieldValidator.isNumber('one')).toBeFalsy();
expect(NumberFieldValidator.isNumber('1q')).toBeFalsy();
});
it('should allow empty number value', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.NUMBER,
value: null
});
expect(validator.validate(field)).toBeTruthy();
});
it('should fail for wrong number value', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.NUMBER,
value: '<value>'
});
field.validationSummary = null;
expect(validator.validate(field)).toBeFalsy();
expect(field.validationSummary).not.toBeNull();
});
});
describe('MinLengthFieldValidator', () => {
let validator: MinLengthFieldValidator;
beforeEach(() => {
validator = new MinLengthFieldValidator();
});
it('should require minLength defined', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.TEXT
});
expect(validator.isSupported(field)).toBeFalsy();
field.minLength = 10;
expect(validator.isSupported(field)).toBeTruthy();
});
it('should allow empty values', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.TEXT,
minLength: 10,
value: null
});
expect(validator.validate(field)).toBeTruthy();
});
it('should succeed text validation', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.TEXT,
minLength: 3,
value: '1234'
});
expect(validator.validate(field)).toBeTruthy();
});
it('should fail text validation', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.TEXT,
minLength: 3,
value: '12'
});
field.validationSummary = null;
expect(validator.validate(field)).toBeFalsy();
expect(field.validationSummary).not.toBeNull();
});
});
describe('MaxLengthFieldValidator', () => {
let validator: MaxLengthFieldValidator;
beforeEach(() => {
validator = new MaxLengthFieldValidator();
});
it('should require maxLength defined', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.TEXT
});
expect(validator.isSupported(field)).toBeFalsy();
field.maxLength = 10;
expect(validator.isSupported(field)).toBeTruthy();
});
it('should allow empty values', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.TEXT,
maxLength: 10,
value: null
});
expect(validator.validate(field)).toBeTruthy();
});
it('should succeed text validation', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.TEXT,
maxLength: 3,
value: '123'
});
expect(validator.validate(field)).toBeTruthy();
});
it('should fail text validation', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.TEXT,
maxLength: 3,
value: '1234'
});
field.validationSummary = null;
expect(validator.validate(field)).toBeFalsy();
expect(field.validationSummary).not.toBeNull();
});
});
describe('MinValueFieldValidator', () => {
let validator: MinValueFieldValidator;
beforeEach(() => {
validator = new MinValueFieldValidator();
});
it('should require minValue defined', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.NUMBER
});
expect(validator.isSupported(field)).toBeFalsy();
field.minValue = '1';
expect(validator.isSupported(field)).toBeTruthy();
});
it('should support numeric widgets only', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.NUMBER,
minValue: '1'
});
expect(validator.isSupported(field)).toBeTruthy();
field.type = FormFieldTypes.TEXT;
expect(validator.isSupported(field)).toBeFalsy();
});
it('should allow empty values', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.NUMBER,
value: null,
minValue: '1'
});
expect(validator.validate(field)).toBeTruthy();
});
it('should succeed for unsupported types', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.TEXT
});
expect(validator.validate(field)).toBeTruthy();
});
it('should succeed validating value', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.NUMBER,
value: '10',
minValue: '10'
});
expect(validator.validate(field)).toBeTruthy();
});
it('should fail validating value', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.NUMBER,
value: '9',
minValue: '10'
});
field.validationSummary = null;
expect(validator.validate(field)).toBeFalsy();
expect(field.validationSummary).not.toBeNull();
});
});
describe('MaxValueFieldValidator', () => {
let validator: MaxValueFieldValidator;
beforeEach(() => {
validator = new MaxValueFieldValidator();
});
it('should require maxValue defined', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.NUMBER
});
expect(validator.isSupported(field)).toBeFalsy();
field.maxValue = '1';
expect(validator.isSupported(field)).toBeTruthy();
});
it('should support numeric widgets only', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.NUMBER,
maxValue: '1'
});
expect(validator.isSupported(field)).toBeTruthy();
field.type = FormFieldTypes.TEXT;
expect(validator.isSupported(field)).toBeFalsy();
});
it('should allow empty values', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.NUMBER,
value: null,
maxValue: '1'
});
expect(validator.validate(field)).toBeTruthy();
});
it('should succeed for unsupported types', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.TEXT
});
expect(validator.validate(field)).toBeTruthy();
});
it('should succeed validating value', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.NUMBER,
value: '10',
maxValue: '10'
});
expect(validator.validate(field)).toBeTruthy();
});
it('should fail validating value', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.NUMBER,
value: '11',
maxValue: '10'
});
field.validationSummary = null;
expect(validator.validate(field)).toBeFalsy();
expect(field.validationSummary).not.toBeNull();
});
});
describe('RegExFieldValidator', () => {
let validator: RegExFieldValidator;
beforeEach(() => {
validator = new RegExFieldValidator();
});
it('should require regex pattern to be defined', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.TEXT
});
expect(validator.isSupported(field)).toBeFalsy();
field.regexPattern = '<pattern>';
expect(validator.isSupported(field)).toBeTruthy();
});
it('should allow empty values', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.TEXT,
value: null,
regexPattern: 'pattern'
});
expect(validator.validate(field)).toBeTruthy();
});
it('should succeed validating regex', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.TEXT,
value: 'pattern',
regexPattern: 'pattern'
});
expect(validator.validate(field)).toBeTruthy();
});
it('should fail validating regex', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.TEXT,
value: 'some value',
regexPattern: 'pattern'
});
expect(validator.validate(field)).toBeFalsy();
});
});
});

View File

@@ -0,0 +1,240 @@
/*!
* @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 { FormFieldModel } from './form-field.model';
import { FormFieldTypes } from './form-field-types';
export interface FormFieldValidator {
isSupported(field: FormFieldModel): boolean;
validate(field: FormFieldModel): boolean;
}
export class RequiredFieldValidator implements FormFieldValidator {
private supportedTypes = [
FormFieldTypes.TEXT,
FormFieldTypes.MULTILINE_TEXT,
FormFieldTypes.NUMBER,
FormFieldTypes.TYPEAHEAD,
FormFieldTypes.DROPDOWN,
FormFieldTypes.PEOPLE,
FormFieldTypes.FUNCTIONAL_GROUP,
FormFieldTypes.RADIO_BUTTONS,
FormFieldTypes.UPLOAD
];
isSupported(field: FormFieldModel): boolean {
return field &&
this.supportedTypes.indexOf(field.type) > -1 &&
field.required;
}
validate(field: FormFieldModel): boolean {
if (this.isSupported(field)) {
if (field.type === FormFieldTypes.DROPDOWN) {
if (field.hasEmptyValue && field.emptyOption) {
if (field.value === field.emptyOption.id) {
return false;
}
}
}
if (field.type === FormFieldTypes.RADIO_BUTTONS) {
let option = field.options.find(opt => opt.id === field.value);
return !!option;
}
if (field.type === FormFieldTypes.UPLOAD) {
return field.value && field.value.length > 0;
}
if (!field.value) {
return false;
}
}
return true;
}
}
export class NumberFieldValidator implements FormFieldValidator {
private supportedTypes = [
FormFieldTypes.NUMBER
];
static isNumber(value: any): boolean {
if (value === null || value === undefined || value === '') {
return false;
}
return !isNaN(+value);
}
isSupported(field: FormFieldModel): boolean {
return field && this.supportedTypes.indexOf(field.type) > -1;
}
validate(field: FormFieldModel): boolean {
if (this.isSupported(field)) {
if (field.value === null ||
field.value === undefined ||
field.value === '' ||
NumberFieldValidator.isNumber(field.value)) {
return true;
}
field.validationSummary = 'Input must be a number';
return false;
}
return true;
}
}
export class MinLengthFieldValidator implements FormFieldValidator {
private supportedTypes = [
FormFieldTypes.TEXT,
FormFieldTypes.MULTILINE_TEXT
];
isSupported(field: FormFieldModel): boolean {
return field &&
this.supportedTypes.indexOf(field.type) > -1 &&
field.minLength > 0;
}
validate(field: FormFieldModel): boolean {
if (this.isSupported(field) && field.value) {
if (field.value.length >= field.minLength) {
return true;
}
field.validationSummary = `Should be at least ${field.minLength} characters long.`;
return false;
}
return true;
}
}
export class MaxLengthFieldValidator implements FormFieldValidator {
private supportedTypes = [
FormFieldTypes.TEXT,
FormFieldTypes.MULTILINE_TEXT
];
isSupported(field: FormFieldModel): boolean {
return field &&
this.supportedTypes.indexOf(field.type) > -1 &&
field.maxLength > 0;
}
validate(field: FormFieldModel): boolean {
if (this.isSupported(field) && field.value) {
if (field.value.length <= field.maxLength) {
return true;
}
field.validationSummary = `Should be ${field.maxLength} characters maximum.`;
return false;
}
return true;
}
}
export class MinValueFieldValidator implements FormFieldValidator {
private supportedTypes = [
FormFieldTypes.NUMBER
];
isSupported(field: FormFieldModel): boolean {
return field &&
this.supportedTypes.indexOf(field.type) > -1 &&
NumberFieldValidator.isNumber(field.minValue);
}
validate(field: FormFieldModel): boolean {
if (this.isSupported(field) && field.value) {
let value: number = +field.value;
let minValue: number = +field.minValue;
if (value >= minValue) {
return true;
}
field.validationSummary = `Should not be less than ${field.minValue}`;
return false;
}
return true;
}
}
export class MaxValueFieldValidator implements FormFieldValidator {
private supportedTypes = [
FormFieldTypes.NUMBER
];
isSupported(field: FormFieldModel): boolean {
return field &&
this.supportedTypes.indexOf(field.type) > -1 &&
NumberFieldValidator.isNumber(field.maxValue);
}
validate(field: FormFieldModel): boolean {
if (this.isSupported(field) && field.value) {
let value: number = +field.value;
let maxValue: number = +field.maxValue;
if (value <= maxValue) {
return true;
}
field.validationSummary = `Should not be greater than ${field.maxValue}`;
return false;
}
return true;
}
}
export class RegExFieldValidator implements FormFieldValidator {
private supportedTypes = [
FormFieldTypes.TEXT,
FormFieldTypes.MULTILINE_TEXT
];
isSupported(field: FormFieldModel): boolean {
return field &&
this.supportedTypes.indexOf(field.type) > -1 &&
!!field.regexPattern;
}
validate(field: FormFieldModel): boolean {
if (this.isSupported(field) && field.value) {
if (field.value.length > 0 && field.value.match(new RegExp('^' + field.regexPattern + '$'))) {
return true;
}
field.validationSummary = 'Invalid value format';
return false;
}
return true;
}
}

View File

@@ -113,15 +113,6 @@ describe('FormFieldModel', () => {
expect(field.readOnly).toBeTruthy();
});
it('should parse and convert empty dropdown value', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DROPDOWN,
value: ''
});
expect(field.value).toBe('empty');
});
it('should parse and leave dropdown value as is', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DROPDOWN,
@@ -145,19 +136,6 @@ describe('FormFieldModel', () => {
expect(field.value).toBe('opt2');
});
it('should parse and fall back to first radio button value', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.RADIO_BUTTONS,
options: [
{ id: 'opt1', value: 'Option 1' },
{ id: 'opt2', value: 'Option 2' }
],
value: 'opt3'
});
expect(field.value).toBe('opt1');
});
it('should parse and leave radio button value as is', () => {
let field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.RADIO_BUTTONS,

View File

@@ -21,11 +21,23 @@ import { FormFieldTypes } from './form-field-types';
import { FormFieldMetadata } from './form-field-metadata';
import { FormModel } from './form.model';
import { WidgetVisibilityModel } from '../../../models/widget-visibility.model';
import {
FormFieldValidator,
RequiredFieldValidator,
NumberFieldValidator,
MinLengthFieldValidator,
MaxLengthFieldValidator,
MinValueFieldValidator,
MaxValueFieldValidator,
RegExFieldValidator
} from './form-field-validator';
export class FormFieldModel extends FormWidgetModel {
private _value: string;
private _readOnly: boolean = false;
private _isValid: boolean = true;
fieldType: string;
id: string;
@@ -35,6 +47,11 @@ export class FormFieldModel extends FormWidgetModel {
overrideId: boolean;
tab: string;
colspan: number = 1;
minLength: number = 0;
maxLength: number = 0;
minValue: string;
maxValue: string;
regexPattern: string;
options: FormFieldOption[] = [];
restUrl: string;
restResponsePath: string;
@@ -49,12 +66,17 @@ export class FormFieldModel extends FormWidgetModel {
isVisible: boolean = true;
visibilityCondition: WidgetVisibilityModel = null;
emptyOption: FormFieldOption;
validationSummary: string;
validators: FormFieldValidator[] = [];
get value(): any {
return this._value;
}
set value(v: any) {
this._value = v;
this.validate();
this.updateForm();
}
@@ -65,6 +87,27 @@ export class FormFieldModel extends FormWidgetModel {
return this._readOnly;
}
get isValid(): boolean {
return this._isValid;
}
validate(): boolean {
this.validationSummary = null;
// TODO: consider doing that on value setter and caching result
if (this.validators && this.validators.length > 0) {
for (let i = 0; i < this.validators.length; i++) {
if (!this.validators[i].validate(this)) {
this._isValid = false;
return this._isValid;
}
}
}
this._isValid = true;
return this._isValid;
}
constructor(form: FormModel, json?: any) {
super(form, json);
@@ -82,6 +125,11 @@ export class FormFieldModel extends FormWidgetModel {
this.restIdProperty = json.restIdProperty;
this.restLabelProperty = json.restLabelProperty;
this.colspan = <number> json.colspan;
this.minLength = <number> json.minLength || 0;
this.maxLength = <number> json.maxLength || 0;
this.minValue = json.minValue;
this.maxValue = json.maxValue;
this.regexPattern = json.regexPattern;
this.options = <FormFieldOption[]> json.options || [];
this.hasEmptyValue = <boolean> json.hasEmptyValue;
this.className = json.className;
@@ -90,10 +138,24 @@ export class FormFieldModel extends FormWidgetModel {
this.hyperlinkUrl = json.hyperlinkUrl;
this.displayText = json.displayText;
this.visibilityCondition = <WidgetVisibilityModel> json.visibilityCondition;
this._value = this.parseValue(json);
this.updateForm();
}
if (this.hasEmptyValue && this.options && this.options.length > 0) {
this.emptyOption = this.options[0];
}
this.validators = [
new RequiredFieldValidator(),
new NumberFieldValidator(),
new MinLengthFieldValidator(),
new MaxLengthFieldValidator(),
new MinValueFieldValidator(),
new MaxValueFieldValidator(),
new RegExFieldValidator()
];
this.updateForm();
}
parseValue(json: any): any {
@@ -103,10 +165,15 @@ export class FormFieldModel extends FormWidgetModel {
This is needed due to Activiti issue related to reading dropdown values as value string
but saving back as object: { id: <id>, name: <name> }
*/
// TODO: needs review
if (json.type === FormFieldTypes.DROPDOWN) {
if (value === '') {
value = 'empty';
if (json.hasEmptyValue && json.options) {
let options = <FormFieldOption[]> json.options || [];
if (options.length > 0) {
let emptyOption = json.options[0];
if (value === '' || value === emptyOption.id || value === emptyOption.name) {
value = emptyOption.id;
}
}
}
}
@@ -115,13 +182,12 @@ export class FormFieldModel extends FormWidgetModel {
but saving back as object: { id: <id>, name: <name> }
*/
if (json.type === FormFieldTypes.RADIO_BUTTONS) {
// Activiti has a bug with default radio button value,
// so try resolving current one with a fallback to first entry
let entry: FormFieldOption[] = this.options.filter(opt => opt.id === value);
// Activiti has a bug with default radio button value where initial selection passed as `name` value
// so try resolving current one with a fallback to first entry via name or id
// TODO: needs to be reported and fixed at Activiti side
let entry: FormFieldOption[] = this.options.filter(opt => opt.id === value || opt.name === value);
if (entry.length > 0) {
value = entry[0].id;
} else if (this.options.length > 0) {
value = this.options[0].id;
}
}
@@ -129,6 +195,9 @@ export class FormFieldModel extends FormWidgetModel {
}
updateForm() {
if (!this.form) {
return;
}
switch (this.type) {
case FormFieldTypes.DROPDOWN:
@@ -177,5 +246,7 @@ export class FormFieldModel extends FormWidgetModel {
this.form.values[this.id] = this.value;
}
}
this.form.onFormFieldChanged(this);
}
}

View File

@@ -20,6 +20,7 @@ import { FormValues } from './form-values';
import { ContainerModel } from './container.model';
import { TabModel } from './tab.model';
import { FormOutcomeModel } from './form-outcome.model';
import { FormFieldModel } from './form-field.model';
export class FormModel {
@@ -31,6 +32,7 @@ export class FormModel {
private _name: string;
private _taskId: string;
private _taskName: string = FormModel.UNSET_TASK_NAME;
private _isValid: boolean = true;
get id(): string {
return this._id;
@@ -48,6 +50,10 @@ export class FormModel {
return this._taskName;
}
get isValid(): boolean {
return this._isValid;
}
readOnly: boolean = false;
tabs: TabModel[] = [];
fields: ContainerModel[] = [];
@@ -102,7 +108,8 @@ export class FormModel {
if (field.tab) {
let tab = tabCache[field.tab];
if (tab) {
tab.fields.push(new ContainerModel(this, field.json));
// tab.fields.push(new ContainerModel(this, field.json));
tab.fields.push(field);
}
}
}
@@ -117,6 +124,51 @@ export class FormModel {
);
}
}
this.validateForm();
}
onFormFieldChanged(field: FormFieldModel) {
this.validateField(field);
}
// TODO: evaluate and cache once the form is loaded
private getFormFields(): FormFieldModel[] {
let result: FormFieldModel[] = [];
for (let i = 0; i < this.fields.length; i++) {
let container = this.fields[i];
for (let j = 0; j < container.columns.length; j++) {
let column = container.columns[j];
for (let k = 0; k < column.fields.length; k++) {
let field = column.fields[k];
result.push(field);
}
}
}
return result;
}
private validateForm() {
this._isValid = true;
let fields = this.getFormFields();
for (let i = 0; i < fields.length; i++) {
if (!fields[i].validate()) {
this._isValid = false;
return;
}
}
}
private validateField(field: FormFieldModel) {
if (!field) {
return;
}
if (!field.validate()) {
this._isValid = false;
return;
}
this.validateForm();
}
private parseContainerFields(json: any): ContainerModel[] {

View File

@@ -0,0 +1,40 @@
/*!
* @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 { it, describe, expect } from '@angular/core/testing';
import { GroupUserModel } from './group-user.model';
describe('GroupUserModel', () => {
it('should init with json', () => {
let json = {
company: '<company>',
email: '<email>',
firstName: '<firstName>',
id: '<id>',
lastName: '<lastName>'
};
let model = new GroupUserModel(json);
expect(model.company).toBe(json.company);
expect(model.email).toBe(json.email);
expect(model.firstName).toBe(json.firstName);
expect(model.id).toBe(json.id);
expect(model.lastName).toBe(json.lastName);
});
});

View File

@@ -27,3 +27,4 @@ export * from './container.model';
export * from './tab.model';
export * from './form-outcome.model';
export * from './form-outcome-event.model';
export * from './form-field-validator';

View File

@@ -2,6 +2,22 @@
width: 100%;
}
.dropdown-widget > select {
.dropdown-widget__select {
width: 100%;
}
.dropdown-widget__invalid .dropdown-widget__select {
border-color: #d50000;
}
.dropdown-widget__invalid .dropdown-widget__label {
color: #d50000;
}
.dropdown-widget__invalid .dropdown-widget__label:after {
background-color: #d50000;
}
.dropdown-widget__invalid .mdl-textfield__error {
visibility: visible !important;
}

View File

@@ -1,6 +1,10 @@
<div class="dropdown-widget">
<label [attr.for]="field.id">{{field.name}}</label>
<select [(ngModel)]="field.value" (ngModelChange)="checkVisibility(field)">
<div class="dropdown-widget"
[class.dropdown-widget__invalid]="!field.isValid">
<label class="dropdown-widget__label" [attr.for]="field.id">{{field.name}}</label>
<select class="dropdown-widget__select"
[(ngModel)]="field.value"
(ngModelChange)="checkVisibility(field)">
<option *ngFor="let opt of field.options" [value]="opt.id">{{opt.name}}</option>
</select>
<span *ngIf="field.validationSummary" class="mdl-textfield__error">{{field.validationSummary}}</span>
</div>

View File

@@ -42,7 +42,8 @@ describe('DropdownWidget', () => {
});
widget.field = new FormFieldModel(form, {
id: fieldId
id: fieldId,
restUrl: '<url>'
});
spyOn(formService, 'getRestFieldValues').and.returnValue(Observable.create(observer => {

View File

@@ -21,7 +21,6 @@ import { WidgetComponent } from './../widget.component';
import { FormFieldOption } from './../core/form-field-option';
declare let __moduleName: string;
declare var componentHandler;
@Component({
moduleId: __moduleName,
@@ -36,18 +35,24 @@ export class DropdownWidget extends WidgetComponent implements OnInit {
}
ngOnInit() {
this.formService
.getRestFieldValues(
this.field.form.taskId,
this.field.id
)
.subscribe(
(result: FormFieldOption[]) => {
this.field.options = result || [];
this.field.updateForm();
},
this.handleError
);
if (this.field && this.field.restUrl) {
this.formService
.getRestFieldValues(
this.field.form.taskId,
this.field.id
)
.subscribe(
(result: FormFieldOption[]) => {
let options = [];
if (this.field.emptyOption) {
options.push(this.field.emptyOption);
}
this.field.options = options.concat((result || []));
this.field.updateForm();
},
this.handleError
);
}
}
handleError(error: any) {

View File

@@ -27,3 +27,23 @@
.functional-group-widget--autocomplete > ul > li {
opacity: 1;
}
.people-widget--autocomplete > ul > li {
opacity: 1;
}
.functional-group-widget__invalid .mdl-textfield__input {
border-color: #d50000;
}
.functional-group-widget__invalid .mdl-textfield__label {
color: #d50000;
}
.functional-group-widget__invalid .mdl-textfield__label:after {
background-color: #d50000;
}
.functional-group-widget__invalid .mdl-textfield__error {
visibility: visible !important;
}

View File

@@ -1,4 +1,5 @@
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label functional-group-widget">
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label functional-group-widget"
[class.functional-group-widget__invalid]="!field.isValid">
<input class="mdl-textfield__input"
type="text"
[attr.id]="field.id"
@@ -8,8 +9,8 @@
(blur)="onBlur()"
[disabled]="field.readOnly">
<label class="mdl-textfield__label" [attr.for]="field.id">{{field.name}}</label>
<span *ngIf="field.validationSummary" class="mdl-textfield__error">{{field.validationSummary}}</span>
</div>
<div class="functional-group-widget--autocomplete mdl-shadow--2dp" *ngIf="popupVisible && groups.length > 0">
<ul>
<li *ngFor="let item of groups"

View File

@@ -37,7 +37,16 @@ describe('FunctionalGroupWidget', () => {
it('should setup text from underlying field on init', () => {
let group = new GroupModel({ name: 'group-1'});
widget.field.value = group;
spyOn(formService, 'getWorkflowGroups').and.returnValue(
Observable.create(observer => {
observer.next([]);
observer.complete();
})
);
widget.ngOnInit();
expect(formService.getWorkflowGroups).toHaveBeenCalled();
expect(widget.value).toBe(group.name);
});

View File

@@ -54,6 +54,13 @@ export class FunctionalGroupWidget extends WidgetComponent implements OnInit {
let restrictWithGroup = <GroupModel> params['restrictWithGroup'];
this.groupId = restrictWithGroup.id;
}
// Load auto-completion for previously saved value
if (this.value) {
this.formService
.getWorkflowGroups(this.value, this.groupId)
.subscribe((result: GroupModel[]) => this.groups = result || []);
}
}
}

View File

@@ -1,3 +1,19 @@
.multiline-text-widget {
width: 100%;
}
.multiline-text-widget__invalid .mdl-textfield__input {
border-color: #d50000;
}
.multiline-text-widget__invalid .mdl-textfield__label {
color: #d50000;
}
.multiline-text-widget__invalid .mdl-textfield__label:after {
background-color: #d50000;
}
.multiline-text-widget__invalid .mdl-textfield__error {
visibility: visible !important;
}

View File

@@ -1,11 +1,15 @@
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label multiline-text-widget">
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label multiline-text-widget"
[class.multiline-text-widget__invalid]="!field.isValid">
<textarea class="mdl-textfield__input"
type="text"
rows= "3"
[attr.id]="field.id"
[attr.required]="isRequired()"
[(ngModel)]="field.value"
(ngModelChange)="checkVisibility(field)"
[disabled]="field.readOnly">
</textarea>
<label class="mdl-textfield__label" [attr.for]="field.id">{{field.name}}</label>
<span *ngIf="field.validationSummary" class="mdl-textfield__error">{{field.validationSummary}}</span>
</div>

View File

@@ -1,3 +1,20 @@
:host .number-widget {
.number-widget {
width: 100%;
}
.number-widget__invalid .mdl-textfield__input {
border-color: #d50000;
}
.number-widget__invalid .mdl-textfield__label {
color: #d50000;
}
.number-widget__invalid .mdl-textfield__label:after {
background-color: #d50000;
}
.number-widget__invalid .mdl-textfield__error {
visibility: visible !important;
}

View File

@@ -1,11 +1,13 @@
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label number-widget">
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label number-widget"
[class.number-widget__invalid]="!field.isValid">
<input class="mdl-textfield__input"
type="text"
pattern="-?[0-9]*(\.[0-9]+)?"
[attr.id]="field.id"
[attr.required]="isRequired()"
[(ngModel)]="field.value"
(ngModelChange)="checkVisibility(field)"
[disabled]="field.readOnly">
<label class="mdl-textfield__label" [attr.for]="field.id">{{field.name}}</label>
<span class="mdl-textfield__error">Input is not a number!</span>
<span *ngIf="field.validationSummary" class="mdl-textfield__error">{{field.validationSummary}}</span>
</div>

View File

@@ -27,3 +27,19 @@
.people-widget--autocomplete > ul > li {
opacity: 1;
}
.people-widget__invalid .mdl-textfield__input {
border-color: #d50000;
}
.people-widget__invalid .mdl-textfield__label {
color: #d50000;
}
.people-widget__invalid .mdl-textfield__label:after {
background-color: #d50000;
}
.people-widget__invalid .mdl-textfield__error {
visibility: visible !important;
}

View File

@@ -1,4 +1,5 @@
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label people-widget">
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label people-widget"
[class.people-widget__invalid]="!field.isValid">
<input class="mdl-textfield__input"
type="text"
[attr.id]="field.id"
@@ -8,8 +9,8 @@
(blur)="onBlur()"
[disabled]="field.readOnly">
<label class="mdl-textfield__label" [attr.for]="field.id">{{field.name}}</label>
<span *ngIf="field.validationSummary" class="mdl-textfield__error">{{field.validationSummary}}</span>
</div>
<div class="people-widget--autocomplete mdl-shadow--2dp" *ngIf="popupVisible && users.length > 0">
<ul>
<li *ngFor="let item of users"

View File

@@ -62,6 +62,12 @@ describe('PeopleWidget', () => {
firstName: 'John',
lastName: 'Doe'
});
spyOn(formService, 'getWorkflowUsers').and.returnValue(Observable.create(observer => {
observer.next([]);
observer.complete();
}));
widget.ngOnInit();
expect(widget.value).toBe('John Doe');
});

View File

@@ -55,6 +55,13 @@ export class PeopleWidget extends WidgetComponent implements OnInit {
let restrictWithGroup = <GroupModel> params['restrictWithGroup'];
this.groupId = restrictWithGroup.id;
}
// Load auto-completion for previously saved value
if (this.value) {
this.formService
.getWorkflowUsers(this.value, this.groupId)
.subscribe((result: GroupUserModel[]) => this.users = result || []);
}
}
}
@@ -97,7 +104,7 @@ export class PeopleWidget extends WidgetComponent implements OnInit {
getDisplayName(model: GroupUserModel) {
if (model) {
let displayName = `${model.firstName} ${model.lastName}`;
let displayName = `${model.firstName || ''} ${model.lastName || ''}`;
return displayName.trim();
}

View File

@@ -1 +1,17 @@
.radio-buttons-widget {}
.radio-buttons-widget__invalid .mdl-radio__label {
color: #d50000;
}
.radio-buttons-widget__invalid .radio-buttons-widget__label {
color: #d50000;
}
.radio-buttons-widget__invalid .radio-buttons-widget__label:after {
background-color: #d50000;
}
.radio-buttons-widget__invalid .mdl-textfield__error {
visibility: visible !important;
}

View File

@@ -1,4 +1,6 @@
<div class="radio-buttons-widget">
<div class="radio-buttons-widget"
[class.radio-buttons-widget__invalid]="!field.isValid">
<label class="radio-buttons-widget__label" [attr.for]="field.id">{{field.name}}</label>
<div *ngFor="let opt of field.options">
<label [attr.for]="opt.id" class="mdl-radio mdl-js-radio">
<input type="radio"
@@ -12,4 +14,5 @@
<span class="mdl-radio__label">{{opt.name}}</span>
</label>
</div>
<span *ngIf="field.validationSummary" class="mdl-textfield__error">{{field.validationSummary}}</span>
</div>

View File

@@ -0,0 +1,96 @@
/*!
* @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 { it, describe, expect, beforeEach } from '@angular/core/testing';
import { Observable } from 'rxjs/Rx';
import { FormService } from '../../../services/form.service';
import { RadioButtonsWidget } from './radio-buttons.widget';
import { FormModel } from './../core/form.model';
import { FormFieldModel } from './../core/form-field.model';
describe('RadioButtonsWidget', () => {
let formService: FormService;
let widget: RadioButtonsWidget;
beforeEach(() => {
formService = new FormService(null, null);
widget = new RadioButtonsWidget(formService);
widget.field = new FormFieldModel(new FormModel(), { restUrl: '<url>' });
});
it('should request field values from service', () => {
const taskId = '<form-id>';
const fieldId = '<field-id>';
let form = new FormModel({
taskId: taskId
});
widget.field = new FormFieldModel(form, {
id: fieldId,
restUrl: '<url>'
});
spyOn(formService, 'getRestFieldValues').and.returnValue(Observable.create(observer => {
observer.next(null);
observer.complete();
}));
widget.ngOnInit();
expect(formService.getRestFieldValues).toHaveBeenCalledWith(taskId, fieldId);
});
it('should update form on values fetched', () => {
let form = widget.field;
spyOn(form, 'updateForm').and.stub();
spyOn(formService, 'getRestFieldValues').and.returnValue(Observable.create(observer => {
observer.next(null);
observer.complete();
}));
widget.ngOnInit();
expect(form.updateForm).toHaveBeenCalled();
});
it('should require field with rest URL to fetch data', () => {
spyOn(formService, 'getRestFieldValues').and.returnValue(Observable.create(observer => {
observer.next(null);
observer.complete();
}));
let field = widget.field;
widget.field = null;
widget.ngOnInit();
expect(formService.getRestFieldValues).not.toHaveBeenCalled();
widget.field = field;
widget.field.restUrl = null;
widget.ngOnInit();
expect(formService.getRestFieldValues).not.toHaveBeenCalled();
widget.field.restUrl = '<url>';
widget.ngOnInit();
expect(formService.getRestFieldValues).toHaveBeenCalled();
});
it('should log error to console by default', () => {
spyOn(console, 'error').and.stub();
widget.handleError('Err');
expect(console.error).toHaveBeenCalledWith('Err');
});
});

View File

@@ -15,8 +15,10 @@
* limitations under the License.
*/
import { Component } from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { WidgetComponent } from './../widget.component';
import { FormService } from '../../../services/form.service';
import { FormFieldOption } from './../core/form-field-option';
declare let __moduleName: string;
declare var componentHandler;
@@ -27,6 +29,31 @@ declare var componentHandler;
templateUrl: './radio-buttons.widget.html',
styleUrls: ['./radio-buttons.widget.css']
})
export class RadioButtonsWidget extends WidgetComponent {
export class RadioButtonsWidget extends WidgetComponent implements OnInit {
constructor(private formService: FormService) {
super();
}
ngOnInit() {
if (this.field && this.field.restUrl) {
this.formService
.getRestFieldValues(
this.field.form.taskId,
this.field.id
)
.subscribe(
(result: FormFieldOption[]) => {
this.field.options = result || [];
this.field.updateForm();
},
this.handleError
);
}
}
handleError(error: any) {
console.error(error);
}
}

View File

@@ -1,3 +1,20 @@
.text-widget {
width: 100%;
}
.text-widget__invalid .mdl-textfield__input {
border-color: #d50000;
}
.text-widget__invalid .mdl-textfield__label {
color: #d50000;
}
.text-widget__invalid .mdl-textfield__label:after {
background-color: #d50000;
}
.text-widget__invalid .mdl-textfield__error {
visibility: visible !important;
}

View File

@@ -1,9 +1,12 @@
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label text-widget">
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label text-widget"
[class.text-widget__invalid]="!field.isValid">
<input class="mdl-textfield__input"
type="text"
[attr.id]="field.id"
[attr.required]="isRequired()"
[(ngModel)]="field.value"
(ngModelChange)="checkVisibility(field)"
[disabled]="field.readOnly">
<label class="mdl-textfield__label" [attr.for]="field.id">{{field.name}}</label>
<span *ngIf="field.validationSummary" class="mdl-textfield__error">{{field.validationSummary}}</span>
</div>

View File

@@ -27,3 +27,21 @@
.typeahead-autocomplete > ul > li {
opacity: 1;
}
.typeahead-widget__invalid .mdl-textfield__input {
border-color: #d50000;
}
.typeahead-widget__invalid .mdl-textfield__label {
color: #d50000;
}
.typeahead-widget__invalid .mdl-textfield__label:after {
background-color: #d50000;
}
.typeahead-widget__invalid .mdl-textfield__error {
visibility: visible !important;
}

View File

@@ -1,5 +1,6 @@
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label typeahead-widget"
[class.is-dirty]="value">
[class.is-dirty]="value"
[class.typeahead-widget__invalid]="!field.isValid">
<input class="mdl-textfield__input"
type="text"
[attr.id]="field.id"
@@ -9,8 +10,8 @@
(blur)="onBlur()"
[disabled]="field.readOnly">
<label class="mdl-textfield__label" [attr.for]="field.id">{{field.name}}</label>
<span *ngIf="field.validationSummary" class="mdl-textfield__error">{{field.validationSummary}}</span>
</div>
<div class="typeahead-autocomplete mdl-shadow--2dp" *ngIf="options.length > 0 && popupVisible">
<ul>
<li *ngFor="let item of options"

View File

@@ -91,7 +91,9 @@ export class TypeaheadWidget extends WidgetComponent implements OnInit {
this.popupVisible = false;
let options = this.field.options || [];
let field = options.find(item => item.name.toLocaleLowerCase() === this.value.toLocaleLowerCase());
let lValue = this.value ? this.value.toLocaleLowerCase() : null;
let field = options.find(item => item.name && item.name.toLocaleLowerCase() === lValue);
if (field) {
this.field.value = field.id;
this.value = field.name;
@@ -100,6 +102,7 @@ export class TypeaheadWidget extends WidgetComponent implements OnInit {
this.value = null;
}
// TODO: seems to be not needed as field.value setter calls it
this.field.updateForm();
}

View File

@@ -15,3 +15,19 @@
float: left;
margin-top: 4px;
}
.upload-widget__invalid .upload-widget__label {
color: #d50000;
}
.upload-widget__invalid .upload-widget__label:after {
background-color: #d50000;
}
.upload-widget__invalid .upload-widget__file {
color: #d50000;
}
.upload-widget__invalid .mdl-textfield__error {
visibility: visible !important;
}

View File

@@ -1,6 +1,6 @@
<div class="upload-widget">
<label [attr.for]="field.id">{{field.name}}</label>
<div class="upload-widget"
[class.upload-widget__invalid]="!field.isValid">
<label class="upload-widget__label" [attr.for]="field.id">{{field.name}}</label>
<div>
<i *ngIf="hasFile" class="material-icons upload-widget__icon">attachment</i>
<span *ngIf="hasFile" class="upload-widget__file">{{getUploadedFileName()}}</span>
@@ -12,4 +12,5 @@
(change)="onFileChanged($event)">
<button *ngIf="hasFile" (click)="reset(file);" class="upload-widget__reset">X</button>
</div>
<span *ngIf="field.validationSummary" class="mdl-textfield__error">{{field.validationSummary}}</span>
</div>

View File

@@ -50,7 +50,7 @@ describe('WidgetComponent', () => {
let component = new WidgetComponent();
expect(component.hasField()).toBeFalsy();
component.field = new FormFieldModel(null);
component.field = new FormFieldModel(new FormModel());
expect(component.hasField()).toBeTruthy();
});

View File

@@ -36,6 +36,13 @@ export class WidgetComponent implements AfterViewInit {
return this.field ? true : false;
}
isRequired(): any {
if (this.field && this.field.required) {
return true;
}
return null;
}
ngAfterViewInit() {
this.setupMaterialComponents();
this.fieldChanged.emit(this.field);

View File

@@ -205,6 +205,7 @@ describe('WidgetVisibilityService', () => {
expect(res).toBeFalsy();
});
/*
it('should be able to retrieve the value of a process variable', (done) => {
service.getTaskProcessVariableModelsForTask(9999).subscribe(
(res: TaskProcessVariableModel[]) => {
@@ -223,6 +224,7 @@ describe('WidgetVisibilityService', () => {
expect(varValue).not.toBe(null);
expect(varValue).toBe('test_value_1');
});
*/
it('should be able to retrieve the value of a form variable', () => {
let fakeForm = new FormModel({variables: [
@@ -308,6 +310,7 @@ describe('WidgetVisibilityService', () => {
expect(rightValue).toBe('100');
});
/*
it('should retrieve the value for the right field when it is a process variable', (done) => {
service.getTaskProcessVariableModelsForTask(9999).subscribe(
(res: TaskProcessVariableModel[]) => {
@@ -327,6 +330,7 @@ describe('WidgetVisibilityService', () => {
expect(rightValue).not.toBe(null);
expect(rightValue).toBe('test_value_2');
});
*/
it('should retrieve the value for the right field when it is a form variable', () => {
let fakeFormWithField = new FormModel(fakeFormJson);
@@ -466,6 +470,7 @@ describe('WidgetVisibilityService', () => {
expect(isVisible).toBeTruthy();
});
/*
it('should evaluate the visibility for the field between form value and process var', (varReady) => {
service.getTaskProcessVariableModelsForTask(9999).subscribe(
(res: TaskProcessVariableModel[]) => {
@@ -487,6 +492,7 @@ describe('WidgetVisibilityService', () => {
expect(isVisible).toBeTruthy();
});
*/
/*
it('should evaluate visibility with multiple conditions', (ready) => {