diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/core/form-field-validator.spec.ts b/ng2-components/ng2-activiti-form/src/components/widgets/core/form-field-validator.spec.ts index ae527ea62d..5a480e7773 100644 --- a/ng2-components/ng2-activiti-form/src/components/widgets/core/form-field-validator.spec.ts +++ b/ng2-components/ng2-activiti-form/src/components/widgets/core/form-field-validator.spec.ts @@ -18,6 +18,7 @@ import { FormFieldOption } from './form-field-option'; import { FormFieldTypes } from './form-field-types'; import { + FixedValueFieldValidator, MaxLengthFieldValidator, MaxValueFieldValidator, MinLengthFieldValidator, @@ -533,4 +534,57 @@ describe('FormFieldValidator', () => { }); }); + + describe('FixedValueFieldValidator', () => { + + let validator: FixedValueFieldValidator; + + beforeEach(() => { + validator = new FixedValueFieldValidator(); + }); + + it('should support only typeahead field', () => { + let field = new FormFieldModel(new FormModel(), { + type: FormFieldTypes.TEXT + }); + expect(validator.isSupported(field)).toBeFalsy(); + + field = new FormFieldModel(new FormModel(), { + type: FormFieldTypes.TYPEAHEAD + }); + + expect(validator.isSupported(field)).toBeTruthy(); + }); + + it('should allow empty values', () => { + let field = new FormFieldModel(new FormModel(), { + type: FormFieldTypes.TYPEAHEAD, + value: null, + regexPattern: 'pattern' + }); + + expect(validator.validate(field)).toBeTruthy(); + }); + + it('should succeed for a valid input value in options', () => { + let field = new FormFieldModel(new FormModel(), { + type: FormFieldTypes.TYPEAHEAD, + value: '1', + options: [{id: '1', name: 'Leanne Graham'}, {id: '2', name: 'Ervin Howell'}] + }); + + expect(validator.validate(field)).toBeTruthy(); + }); + + it('should fail for an invalid input value in options', () => { + let field = new FormFieldModel(new FormModel(), { + type: FormFieldTypes.TYPEAHEAD, + value: 'Lean', + options: [{id: '1', name: 'Leanne Graham'}, {id: '2', name: 'Ervin Howell'}] + }); + + expect(validator.validate(field)).toBeFalsy(); + }); + + }); }); diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/core/form-field-validator.ts b/ng2-components/ng2-activiti-form/src/components/widgets/core/form-field-validator.ts index f1a7756303..51639b3ca6 100644 --- a/ng2-components/ng2-activiti-form/src/components/widgets/core/form-field-validator.ts +++ b/ng2-components/ng2-activiti-form/src/components/widgets/core/form-field-validator.ts @@ -366,6 +366,47 @@ export class RegExFieldValidator implements FormFieldValidator { } +export class FixedValueFieldValidator implements FormFieldValidator { + + private supportedTypes = [ + FormFieldTypes.TYPEAHEAD + ]; + + isSupported(field: FormFieldModel): boolean { + return field && this.supportedTypes.indexOf(field.type) > -1; + } + + hasValidNameOrValidId(field: FormFieldModel): boolean { + return this.hasValidName(field) || this.hasValidId(field); + } + + hasValidName(field: FormFieldModel) { + return field.options.find(item => item.name && item.name.toLocaleLowerCase() === field.value.toLocaleLowerCase()) ? true : false; + } + + hasValidId(field: FormFieldModel) { + return field.options[field.value - 1] ? true : false; + } + + hasStringValue(field: FormFieldModel) { + return field.value && typeof field.value === 'string'; + } + + hasOptions(field: FormFieldModel) { + return field.options && field.options.length > 0; + } + + validate(field: FormFieldModel): boolean { + if (this.isSupported(field)) { + if (this.hasStringValue(field) && this.hasOptions(field) && !this.hasValidNameOrValidId(field)) { + field.validationSummary = 'Invalid data inserted'; + return false; + } + } + return true; + } +} + export const FORM_FIELD_VALIDATORS = [ new RequiredFieldValidator(), new NumberFieldValidator(), @@ -376,5 +417,6 @@ export const FORM_FIELD_VALIDATORS = [ new RegExFieldValidator(), new DateFieldValidator(), new MinDateFieldValidator(), - new MaxDateFieldValidator() + new MaxDateFieldValidator(), + new FixedValueFieldValidator() ]; diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/typeahead/typeahead.widget.html b/ng2-components/ng2-activiti-form/src/components/widgets/typeahead/typeahead.widget.html index c5eea0646e..1289f51401 100644 --- a/ng2-components/ng2-activiti-form/src/components/widgets/typeahead/typeahead.widget.html +++ b/ng2-components/ng2-activiti-form/src/components/widgets/typeahead/typeahead.widget.html @@ -10,18 +10,16 @@ type="text" [id]="field.id" [(ngModel)]="value" + (ngModelChange)="validate()" (keyup)="onKeyUp($event)" [disabled]="field.readOnly" placeholder="{{field.placeholder}}" [mdAutocomplete]="auto"> - - {{item.name}} + + {{item.name}} - - {{field.value}} - diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/typeahead/typeahead.widget.spec.ts b/ng2-components/ng2-activiti-form/src/components/widgets/typeahead/typeahead.widget.spec.ts index f159a7a3cb..681902efdb 100644 --- a/ng2-components/ng2-activiti-form/src/components/widgets/typeahead/typeahead.widget.spec.ts +++ b/ng2-components/ng2-activiti-form/src/components/widgets/typeahead/typeahead.widget.spec.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import { OverlayContainer } from '@angular/cdk/overlay'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { CoreModule, LogServiceMock } from 'ng2-alfresco-core'; import { Observable } from 'rxjs/Rx'; @@ -37,6 +38,7 @@ describe('TypeaheadWidgetComponent', () => { let widget: TypeaheadWidgetComponent; let visibilityService: WidgetVisibilityService; let logService: LogServiceMock; + let overlayContainerElement: HTMLElement; beforeEach(() => { logService = new LogServiceMock(); @@ -108,23 +110,6 @@ describe('TypeaheadWidgetComponent', () => { expect(widget.handleError).toHaveBeenCalledWith(err); }); - it('should prevent default behaviour on option item click', () => { - let event = jasmine.createSpyObj('event', ['preventDefault']); - widget.onItemClick(null, event); - expect(event.preventDefault).toHaveBeenCalled(); - }); - - it('should update values on option item click', () => { - let option: FormFieldOption = { - id: '1', - name: 'name' - }; - spyOn(visibilityService, 'refreshVisibility').and.stub(); - widget.onItemClick(option, null); - expect(widget.field.value).toBe(option.id); - expect(widget.value).toBe(option.name); - }); - it('should setup initial value', () => { spyOn(formService, 'getRestFieldValues').and.returnValue(Observable.create(observer => { observer.next([ @@ -208,89 +193,6 @@ describe('TypeaheadWidgetComponent', () => { expect(filtered[0]).toEqual(options[1]); }); - it('should update form on value flush', () => { - spyOn(widget.field, 'updateForm').and.callThrough(); - widget.flushValue(); - expect(widget.field.updateForm).toHaveBeenCalled(); - }); - - it('should flush selected value', () => { - let options: FormFieldOption[] = [ - { id: '1', name: 'Item one' }, - { id: '2', name: 'Item Two' } - ]; - - widget.field.options = options; - widget.value = 'Item Two'; - widget.flushValue(); - - expect(widget.value).toBe(options[1].name); - expect(widget.field.value).toBe(options[1].id); - }); - - it('should be case insensitive when flushing value', () => { - let options: FormFieldOption[] = [ - { id: '1', name: 'Item one' }, - { id: '2', name: 'iTEM TWo' } - ]; - - widget.field.options = options; - widget.value = 'ITEM TWO'; - widget.flushValue(); - - expect(widget.value).toBe(options[1].name); - expect(widget.field.value).toBe(options[1].id); - }); - - it('should reset fields when flushing missing option value', () => { - widget.field.options = [ - { id: '1', name: 'Item one' }, - { id: '2', name: 'Item two' } - ]; - widget.value = 'Missing item'; - widget.flushValue(); - - expect(widget.value).toBeNull(); - expect(widget.field.value).toBeNull(); - }); - - it('should reset fields when flushing incorrect value', () => { - widget.field.options = [ - { id: '1', name: 'Item one' }, - { id: '2', name: 'Item two' } - ]; - widget.field.value = 'Item two'; - widget.value = 'Item two!'; - widget.flushValue(); - - expect(widget.value).toBeNull(); - expect(widget.field.value).toBeNull(); - }); - - it('should reset fields when flushing value having no options', () => { - widget.field.options = null; - widget.field.value = 'item 1'; - widget.value = 'new item'; - widget.flushValue(); - - expect(widget.value).toBeNull(); - expect(widget.field.value).toBeNull(); - }); - - it('should emit field change event on item click', () => { - let event = jasmine.createSpyObj('event', ['preventDefault']); - let fakeField = new FormFieldModel(new FormModel(), { id: 'fakeField', value: 'fakeValue' }); - widget.field = fakeField; - let item = { id: 'fake-id-opt', name: 'fake-name-opt' }; - widget.onItemClick(item, event); - - widget.fieldChanged.subscribe((field) => { - expect(field).toBeDefined(); - expect(field.id).toEqual('fakeField'); - expect(field.value).toEqual('fake-id-opt'); - }); - }); - describe('when template is ready', () => { let typeaheadWidgetComponent: TypeaheadWidgetComponent; let fixture: ComponentFixture; @@ -308,7 +210,19 @@ describe('TypeaheadWidgetComponent', () => { TestBed.configureTestingModule({ imports: [CoreModule, MaterialModule], declarations: [TypeaheadWidgetComponent, ErrorWidgetComponent], - providers: [FormService, EcmModelService, WidgetVisibilityService] + providers: [ + {provide: OverlayContainer, useFactory: () => { + overlayContainerElement = document.createElement('div'); + overlayContainerElement.classList.add('cdk-overlay-container'); + + document.body.appendChild(overlayContainerElement); + + // remove body padding to keep consistent cross-browser + document.body.style.padding = '0'; + document.body.style.margin = '0'; + + return {getContainerElement: () => overlayContainerElement}; + }}, FormService, EcmModelService, WidgetVisibilityService] }).compileComponents().then(() => { fixture = TestBed.createComponent(TypeaheadWidgetComponent); typeaheadWidgetComponent = fixture.componentInstance; @@ -321,6 +235,33 @@ describe('TypeaheadWidgetComponent', () => { TestBed.resetTestingModule(); }); + describe ('and typeahead is in readonly mode', () => { + + it('should show typeahead value with input disabled', async(() => { + typeaheadWidgetComponent.field = new FormFieldModel( + new FormModel({ processVariables: [{ name: 'typeahead-id_LABEL', value: 'FakeProcessValue' }] }), { + id: 'typeahead-id', + name: 'typeahead-name', + type: 'readonly', + params: { field: { id: 'typeahead-id', name: 'typeahead-name', type: 'typeahead' } } + }); + fixture.detectChanges(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + let readonlyInput: HTMLInputElement = element.querySelector('#typeahead-id'); + expect(readonlyInput.disabled).toBeTruthy(); + expect(readonlyInput).not.toBeNull(); + expect(readonlyInput.value).toBe('FakeProcessValue'); + }); + })); + + afterEach(() => { + fixture.destroy(); + TestBed.resetTestingModule(); + }); + + }); + describe('and typeahead is populated via taskId', () => { beforeEach(async(() => { @@ -344,14 +285,58 @@ describe('TypeaheadWidgetComponent', () => { })); it('should show typeahead options', async(() => { - let keyboardEvent = new KeyboardEvent('keypress'); + let typeahedElement = fixture.debugElement.query(By.css('#typeahead-id')); + let typeahedHTMLElement: HTMLInputElement = typeahedElement.nativeElement; + typeahedHTMLElement.focus(); typeaheadWidgetComponent.value = 'F'; + typeahedHTMLElement.value = 'F'; + typeahedHTMLElement.dispatchEvent(new Event('keyup')); + typeahedHTMLElement.dispatchEvent(new Event('input')); + fixture.detectChanges(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + expect(fixture.debugElement.query(By.css('[id="typeahead-name_option_1"]'))).not.toBeNull(); + expect(fixture.debugElement.query(By.css('[id="typeahead-name_option_2"]'))).not.toBeNull(); + expect(fixture.debugElement.query(By.css('[id="typeahead-name_option_3"]'))).not.toBeNull(); + }); + })); + + it('should hide the option when the value is empty', async(() => { + let typeahedElement = fixture.debugElement.query(By.css('#typeahead-id')); + let typeahedHTMLElement: HTMLInputElement = typeahedElement.nativeElement; + typeahedHTMLElement.focus(); + typeaheadWidgetComponent.value = 'F'; + typeahedHTMLElement.value = 'F'; + typeahedHTMLElement.dispatchEvent(new Event('keyup')); + typeahedHTMLElement.dispatchEvent(new Event('input')); + fixture.detectChanges(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + expect(fixture.debugElement.query(By.css('[id="typeahead-name_option_1"]'))).not.toBeNull(); + typeahedHTMLElement.focus(); + typeaheadWidgetComponent.value = ''; + typeahedHTMLElement.dispatchEvent(new Event('keyup')); + typeahedHTMLElement.dispatchEvent(new Event('input')); + fixture.detectChanges(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + expect(fixture.debugElement.query(By.css('[id="typeahead-name_option_1"]'))).toBeNull(); + }); + }); + })); + + it('should show error message when the value is not valid', async(() => { + typeaheadWidgetComponent.value = 'Fake Name'; + typeaheadWidgetComponent.field.value = 'Fake Name'; + typeaheadWidgetComponent.field.options = fakeOptionList; + expect(element.querySelector('.adf-error-text')).toBeNull(); + let keyboardEvent = new KeyboardEvent('keypress'); typeaheadWidgetComponent.onKeyUp(keyboardEvent); fixture.detectChanges(); fixture.whenStable().then(() => { - expect(fixture.debugElement.queryAll(By.css('[id="md-option-1"]'))).toBeDefined(); - expect(fixture.debugElement.queryAll(By.css('[id="md-option-2"]'))).toBeDefined(); - expect(fixture.debugElement.queryAll(By.css('[id="md-option-3"]'))).toBeDefined(); + fixture.detectChanges(); + expect(element.querySelector('.adf-error-text')).not.toBeNull(); + expect(element.querySelector('.adf-error-text').textContent).toContain('Invalid data inserted'); }); })); @@ -362,27 +347,6 @@ describe('TypeaheadWidgetComponent', () => { expect(element.querySelector('#typeahead-id')).toBeNull(); }); })); - - it('should show typeahead value when the type is readonly', async(() => { - typeaheadWidgetComponent.field = new FormFieldModel( - new FormModel({ taskId: 'fake-task-id', processVariables: [{ name: 'typeahead-id_LABEL', value: 'FakeProcessValue' }] }), { - id: 'typeahead-id', - name: 'typeahead-name', - type: 'readonly', - value: '9', - params: { field: { id: 'typeahead-id', name: 'typeahead-name', type: 'typeahead' } } - }); - fixture.detectChanges(); - const trigger = fixture.debugElement.query(By.css('.mat-select-trigger')).nativeElement; - trigger.click(); - fixture.detectChanges(); - fixture.whenStable().then(() => { - expect(element.querySelector('#typeahead-id')).not.toBeNull(); - let optionElement: HTMLElement = document.body.querySelector('.mat-option'); - expect(optionElement.innerText).toEqual('FakeProcessValue'); - }); - })); - }); describe('and typeahead is populated via processDefinitionId', () => { diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/typeahead/typeahead.widget.ts b/ng2-components/ng2-activiti-form/src/components/widgets/typeahead/typeahead.widget.ts index 1d10fca7d5..0a35f61a8c 100644 --- a/ng2-components/ng2-activiti-form/src/components/widgets/typeahead/typeahead.widget.ts +++ b/ng2-components/ng2-activiti-form/src/components/widgets/typeahead/typeahead.widget.ts @@ -15,7 +15,7 @@ * limitations under the License. */ - /* tslint:disable:component-selector */ +/* tslint:disable:component-selector */ import { ENTER, ESCAPE } from '@angular/cdk/keycodes'; import { Component, OnInit, ViewEncapsulation } from '@angular/core'; @@ -23,7 +23,7 @@ import { LogService } from 'ng2-alfresco-core'; import { WidgetVisibilityService } from '../../../services/widget-visibility.service'; import { FormService } from './../../../services/form.service'; import { FormFieldOption } from './../core/form-field-option'; -import { baseHost , WidgetComponent } from './../widget.component'; +import { baseHost, WidgetComponent } from './../widget.component'; @Component({ selector: 'typeahead-widget', @@ -42,7 +42,7 @@ export class TypeaheadWidgetComponent extends WidgetComponent implements OnInit constructor(public formService: FormService, private visibilityService: WidgetVisibilityService, private logService: LogService) { - super(formService); + super(formService); } ngOnInit() { @@ -51,101 +51,89 @@ export class TypeaheadWidgetComponent extends WidgetComponent implements OnInit } else if (this.field.form.processDefinitionId) { this.getValuesByProcessDefinitionId(); } + if (this.isReadOnlyType()) { + this.value = this.field.value; + } } getValuesByTaskId() { this.formService .getRestFieldValues( - this.field.form.taskId, - this.field.id + this.field.form.taskId, + this.field.id ) .subscribe( - (result: FormFieldOption[]) => { - let options = result || []; - this.field.options = options; + (result: FormFieldOption[]) => { + let options = result || []; + this.field.options = options; - let fieldValue = this.field.value; - if (fieldValue) { - let toSelect = options.find(item => item.id === fieldValue); - if (toSelect) { - this.value = toSelect.name; - } + let fieldValue = this.field.value; + if (fieldValue) { + let toSelect = options.find(item => item.id === fieldValue); + if (toSelect) { + this.value = toSelect.name; } - this.field.updateForm(); - this.visibilityService.refreshEntityVisibility(this.field); - }, - err => this.handleError(err) + } + this.field.updateForm(); + this.visibilityService.refreshEntityVisibility(this.field); + }, + err => this.handleError(err) ); } getValuesByProcessDefinitionId() { this.formService .getRestFieldValuesByProcessId( - this.field.form.processDefinitionId, - this.field.id + this.field.form.processDefinitionId, + this.field.id ) .subscribe( - (result: FormFieldOption[]) => { - let options = result || []; - this.field.options = options; + (result: FormFieldOption[]) => { + let options = result || []; + this.field.options = options; - let fieldValue = this.field.value; - if (fieldValue) { - let toSelect = options.find(item => item.id === fieldValue); - if (toSelect) { - this.value = toSelect.name; - } + let fieldValue = this.field.value; + if (fieldValue) { + let toSelect = options.find(item => item.id === fieldValue); + if (toSelect) { + this.value = toSelect.name; } - this.field.updateForm(); - this.visibilityService.refreshEntityVisibility(this.field); - }, - err => this.handleError(err) + } + this.field.updateForm(); + this.visibilityService.refreshEntityVisibility(this.field); + }, + err => this.handleError(err) ); } getOptions(): FormFieldOption[] { - let val = this.value.toLocaleLowerCase(); + let val = this.value.trim().toLocaleLowerCase(); return this.field.options.filter(item => { let name = item.name.toLocaleLowerCase(); return name.indexOf(val) > -1; }); } + isValidOptionName(optionName: string): boolean { + let option = this.field.options.find(item => item.name && item.name.toLocaleLowerCase() === optionName.toLocaleLowerCase()); + return option ? true : false; + } + onKeyUp(event: KeyboardEvent) { - if (this.value && this.value.length >= this.minTermLength && this.oldValue !== this.value) { + if (this.value && this.value.trim().length >= this.minTermLength && this.oldValue !== this.value) { if (event.keyCode !== ESCAPE && event.keyCode !== ENTER) { if (this.value.length >= this.minTermLength) { this.options = this.getOptions(); this.oldValue = this.value; + if (this.isValidOptionName(this.value)) { + this.field.value = this.options[0].id; + } } } } - } - - flushValue() { - let options = this.field.options || []; - 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; - } else { - this.field.value = null; - this.value = null; - } - - this.field.updateForm(); - } - - onItemClick(item: FormFieldOption, event: Event) { - if (item) { - this.field.value = item.id; - this.value = item.name; - this.checkVisibility(); - } - if (event) { - event.preventDefault(); + if (this.isValueDefined() && this.value.trim().length === 0) { + this.oldValue = this.value; + this.options = []; } } @@ -157,6 +145,14 @@ export class TypeaheadWidgetComponent extends WidgetComponent implements OnInit } } + validate() { + this.field.value = this.value; + } + + isValueDefined() { + return this.value !== null && this.value !== undefined; + } + handleError(error: any) { this.logService.error(error); }