diff --git a/ng2-components/ng2-activiti-form/src/components/activiti-form.component.css b/ng2-components/ng2-activiti-form/src/components/activiti-form.component.css index 6eb9bd0de9..65aca84d10 100644 --- a/ng2-components/ng2-activiti-form/src/components/activiti-form.component.css +++ b/ng2-components/ng2-activiti-form/src/components/activiti-form.component.css @@ -1,6 +1,7 @@ .activiti-form-container { width: 100%; min-height: 100px; + overflow: visible; } .activiti-form-container > .mdl-card__media { diff --git a/ng2-components/ng2-activiti-form/src/components/activiti-form.component.html b/ng2-components/ng2-activiti-form/src/components/activiti-form.component.html index 82866342a4..44994aa77a 100644 --- a/ng2-components/ng2-activiti-form/src/components/activiti-form.component.html +++ b/ng2-components/ng2-activiti-form/src/components/activiti-form.component.html @@ -41,11 +41,13 @@ will be removed during future revisions -->
- +
+ +

Values

diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/container/container.widget.html b/ng2-components/ng2-activiti-form/src/components/widgets/container/container.widget.html index 6cffc9730c..f89403d7f4 100644 --- a/ng2-components/ng2-activiti-form/src/components/widgets/container/container.widget.html +++ b/ng2-components/ng2-activiti-form/src/components/widgets/container/container.widget.html @@ -46,6 +46,9 @@
+
+ +
UNKNOWN WIDGET TYPE: {{field.type}}
diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/core/form-field-types.ts b/ng2-components/ng2-activiti-form/src/components/widgets/core/form-field-types.ts index 627bcfe5d2..bddb2e3dd2 100644 --- a/ng2-components/ng2-activiti-form/src/components/widgets/core/form-field-types.ts +++ b/ng2-components/ng2-activiti-form/src/components/widgets/core/form-field-types.ts @@ -24,6 +24,7 @@ export class FormFieldTypes { static DISPLAY_VALUE: string = 'readonly'; static READONLY_TEXT: string = 'readonly-text'; static UPLOAD: string = 'upload'; + static TYPEAHEAD: string = 'typeahead'; static READONLY_TYPES: string[] = [ FormFieldTypes.HYPERLINK, diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/core/form-field.model.ts b/ng2-components/ng2-activiti-form/src/components/widgets/core/form-field.model.ts index 22bd3822ae..365a669dcc 100644 --- a/ng2-components/ng2-activiti-form/src/components/widgets/core/form-field.model.ts +++ b/ng2-components/ng2-activiti-form/src/components/widgets/core/form-field.model.ts @@ -150,9 +150,9 @@ export class FormFieldModel extends FormWidgetModel { This is needed due to Activiti issue related to reading radio button values as value string but saving back as object: { id: , name: } */ - let entry: FormFieldOption[] = this.options.filter(opt => opt.id === this.value); - if (entry.length > 0) { - this.form.values[this.id] = entry[0]; + let rbEntry: FormFieldOption[] = this.options.filter(opt => opt.id === this.value); + if (rbEntry.length > 0) { + this.form.values[this.id] = rbEntry[0]; } else if (this.options.length > 0) { this.form.values[this.id] = this.options[0]; } @@ -164,6 +164,14 @@ export class FormFieldModel extends FormWidgetModel { this.form.values[this.id] = null; } break; + case FormFieldTypes.TYPEAHEAD: + let taEntry: FormFieldOption[] = this.options.filter(opt => opt.id === this.value); + if (taEntry.length > 0) { + this.form.values[this.id] = taEntry[0]; + } else if (this.options.length > 0) { + this.form.values[this.id] = null; + } + break; default: if (!FormFieldTypes.isReadOnlyType(this.type)) { this.form.values[this.id] = this.value; diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/index.ts b/ng2-components/ng2-activiti-form/src/components/widgets/index.ts index 51ceb74e05..32d1c765ac 100644 --- a/ng2-components/ng2-activiti-form/src/components/widgets/index.ts +++ b/ng2-components/ng2-activiti-form/src/components/widgets/index.ts @@ -28,6 +28,7 @@ import { RadioButtonsWidget } from './radio-buttons/radio-buttons.widget'; import { DisplayValueWidget } from './display-value/display-value.widget'; import { DisplayTextWidget } from './display-text/display-text.widget'; import { UploadWidget } from './upload/upload.widget'; +import { TypeaheadWidget } from './typeahead/typeahead.widget'; // core export * from './widget.component'; @@ -48,6 +49,7 @@ export * from './radio-buttons/radio-buttons.widget'; export * from './display-value/display-value.widget'; export * from './display-text/display-text.widget'; export * from './upload/upload.widget'; +export * from './typeahead/typeahead.widget'; export const CONTAINER_WIDGET_DIRECTIVES: [any] = [ TabsWidget, @@ -64,7 +66,8 @@ export const PRIMITIVE_WIDGET_DIRECTIVES: [any] = [ RadioButtonsWidget, DisplayValueWidget, DisplayTextWidget, - UploadWidget + UploadWidget, + TypeaheadWidget ]; diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/typeahead/typeahead.widget.css b/ng2-components/ng2-activiti-form/src/components/widgets/typeahead/typeahead.widget.css new file mode 100644 index 0000000000..5cefa1e623 --- /dev/null +++ b/ng2-components/ng2-activiti-form/src/components/widgets/typeahead/typeahead.widget.css @@ -0,0 +1,29 @@ +.typeahead-widget { + width: 100%; +} + +.typeahead-autocomplete { + background-color: #fff; + position: absolute; + z-index: 5; + color: #555; + margin: -15px 0 0 0; +} + +.typeahead-autocomplete > ul { + list-style-type: none; + position: static; + + height: auto; + width: auto; + min-width: 124px; + padding: 8px 0; + margin: 0; + + box-shadow: 0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12); + border-radius: 2px; +} + +.typeahead-autocomplete > ul > li { + opacity: 1; +} 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 new file mode 100644 index 0000000000..ab43e475a6 --- /dev/null +++ b/ng2-components/ng2-activiti-form/src/components/widgets/typeahead/typeahead.widget.html @@ -0,0 +1,22 @@ +
+ + +
+ +
+
    +
  • + {{item.name}} +
  • +
+
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 new file mode 100644 index 0000000000..b0705fc9b1 --- /dev/null +++ b/ng2-components/ng2-activiti-form/src/components/widgets/typeahead/typeahead.widget.spec.ts @@ -0,0 +1,298 @@ +/*! + * @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 { TypeaheadWidget } from './typeahead.widget'; +import { FormService } from '../../../services/form.service'; +import { FormModel } from '../core/form.model'; +import { FormFieldModel } from '../core/form-field.model'; +import { FormFieldOption } from '../core/form-field-option'; + +describe('TypeaheadWidget', () => { + + let formService: FormService; + let widget: TypeaheadWidget; + + beforeEach(() => { + formService = new FormService(null, null); + widget = new TypeaheadWidget(formService); + widget.field = new FormFieldModel(new FormModel()); + }); + + it('should request field values from service', () => { + const taskId = ''; + const fieldId = ''; + + let form = new FormModel({ + taskId: taskId + }); + + widget.field = new FormFieldModel(form, { + id: fieldId + }); + + spyOn(formService, 'getRestFieldValues').and.returnValue(Observable.create(observer => { + observer.next(null); + observer.complete(); + })); + widget.ngOnInit(); + expect(formService.getRestFieldValues).toHaveBeenCalledWith(taskId, fieldId); + }); + + it('should handle error when requesting fields', () => { + const err = 'Error'; + spyOn(formService, 'getRestFieldValues').and.returnValue(Observable.throw(err)); + spyOn(widget, 'handleError').and.callThrough(); + + widget.ngOnInit(); + + expect(formService.getRestFieldValues).toHaveBeenCalled(); + expect(widget.handleError).toHaveBeenCalledWith(err); + }); + + it('should log error to console by default', () => { + spyOn(console, 'error').and.stub(); + widget.handleError('Err'); + expect(console.error).toHaveBeenCalledWith('Err'); + }); + + it('should show popup on key up', () => { + widget.minTermLength = 1; + widget.value = 'some value'; + + widget.popupVisible = false; + widget.onKeyUp(null); + expect(widget.popupVisible).toBeTruthy(); + }); + + it('should require value to show popup', () => { + widget.minTermLength = 1; + widget.value = ''; + + widget.popupVisible = false; + widget.onKeyUp(null); + expect(widget.popupVisible).toBeFalsy(); + }); + + it('should require value to be of min length to show popup', () => { + widget.minTermLength = 3; + widget.value = 'v'; + + // value less than constraint + widget.popupVisible = false; + widget.onKeyUp(null); + expect(widget.popupVisible).toBeFalsy(); + + // value satisfies constraint + widget.value = 'value'; + widget.onKeyUp(null); + expect(widget.popupVisible).toBeTruthy(); + + // value gets less than allowed again + widget.value = 'va'; + widget.onKeyUp(null); + expect(widget.popupVisible).toBeFalsy(); + }); + + it('should flush value on blur', (done) => { + spyOn(widget, 'flushValue').and.stub(); + widget.onBlur(); + + setTimeout(() => { + expect(widget.flushValue).toHaveBeenCalled(); + done(); + }, 200); + }); + + 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' + }; + + 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([ + { id: '1', name: 'One' }, + { id: '2', name: 'Two' } + ]); + observer.complete(); + })); + + widget.field.value = '2'; + widget.ngOnInit(); + + expect(formService.getRestFieldValues).toHaveBeenCalled(); + expect(widget.value).toBe('Two'); + }); + + it('should not setup initial value due to missing option', () => { + spyOn(formService, 'getRestFieldValues').and.returnValue(Observable.create(observer => { + observer.next([ + { id: '1', name: 'One' }, + { id: '2', name: 'Two' } + ]); + observer.complete(); + })); + + widget.field.value = '3'; + widget.ngOnInit(); + + expect(formService.getRestFieldValues).toHaveBeenCalled(); + expect(widget.value).toBeUndefined(); + }); + + it('should setup field options on load', () => { + let options: FormFieldOption[] = [ + { id: '1', name: 'One' }, + { id: '2', name: 'Two' } + ]; + + spyOn(formService, 'getRestFieldValues').and.returnValue(Observable.create(observer => { + observer.next(options); + observer.complete(); + })); + + widget.ngOnInit(); + expect(widget.field.options).toEqual(options); + }); + + it('should update form upon options setup', () => { + spyOn(formService, 'getRestFieldValues').and.returnValue(Observable.create(observer => { + observer.next([]); + observer.complete(); + })); + + spyOn(widget.field, 'updateForm').and.callThrough(); + widget.ngOnInit(); + expect(widget.field.updateForm).toHaveBeenCalled(); + }); + + it('should get filtered options', () => { + let options: FormFieldOption[] = [ + { id: '1', name: 'Item one' }, + { id: '2', name: 'Item two'} + ]; + widget.field.options = options; + widget.value = 'tw'; + + let filtered = widget.getOptions(); + expect(filtered.length).toBe(1); + expect(filtered[0]).toEqual(options[1]); + }); + + it('should be case insensitive when filtering options', () => { + let options: FormFieldOption[] = [ + { id: '1', name: 'Item one' }, + { id: '2', name: 'iTEM TWo' } + ]; + widget.field.options = options; + widget.value = 'tW'; + + let filtered = widget.getOptions(); + expect(filtered.length).toBe(1); + expect(filtered[0]).toEqual(options[1]); + }); + + it('should hide popup on flush', () => { + widget.popupVisible = true; + widget.flushValue(); + expect(widget.popupVisible).toBeFalsy(); + }); + + 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(); + }); + +}); 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 new file mode 100644 index 0000000000..2eaeae6db2 --- /dev/null +++ b/ng2-components/ng2-activiti-form/src/components/widgets/typeahead/typeahead.widget.ts @@ -0,0 +1,114 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Component, OnInit } from '@angular/core'; +import { FormService } from './../../../services/form.service'; +import { WidgetComponent } from './../widget.component'; +import { FormFieldOption } from './../core/form-field-option'; + +declare let __moduleName: string; +declare var componentHandler; + +@Component({ + moduleId: __moduleName, + selector: 'typeahead-widget', + templateUrl: './typeahead.widget.html', + styleUrls: ['./typeahead.widget.css'] +}) +export class TypeaheadWidget extends WidgetComponent implements OnInit { + + popupVisible: boolean = false; + minTermLength: number = 1; + value: string; + + constructor(private formService: FormService) { + super(); + } + + ngOnInit() { + this.formService + .getRestFieldValues( + this.field.form.taskId, + this.field.id + ) + .subscribe( + (result: FormFieldOption[]) => { + let options = result || []; + this.field.options = options; + this.field.updateForm(); + + let fieldValue = this.field.value; + if (fieldValue) { + let toSelect = options.find(item => item.id === fieldValue); + if (toSelect) { + this.value = toSelect.name; + } + } + }, + this.handleError + ); + } + + getOptions(): FormFieldOption[] { + let val = this.value.toLocaleLowerCase(); + return this.field.options.filter(item => { + let name = item.name.toLocaleLowerCase(); + return name.indexOf(val) > -1; + }); + } + + onKeyUp(event: KeyboardEvent) { + this.popupVisible = !!(this.value && this.value.length >= this.minTermLength); + } + + onBlur() { + setTimeout(() => { + this.flushValue(); + }, 200); + } + + flushValue() { + this.popupVisible = false; + + let options = this.field.options || []; + let field = options.find(item => item.name.toLocaleLowerCase() === this.value.toLocaleLowerCase()); + 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; + } + if (event) { + event.preventDefault(); + } + } + + handleError(error: any) { + console.error(error); + } + +} diff --git a/ng2-components/ng2-activiti-form/src/services/form.service.ts b/ng2-components/ng2-activiti-form/src/services/form.service.ts index a5f33bc003..3ff37b215a 100644 --- a/ng2-components/ng2-activiti-form/src/services/form.service.ts +++ b/ng2-components/ng2-activiti-form/src/services/form.service.ts @@ -212,6 +212,12 @@ export class FormService { .catch(this.handleError); } + getRestFieldValues(taskId: string, field: string): Observable { + let alfrescoApi = this.authService.getAlfrescoApi(); + return Observable.fromPromise(alfrescoApi.activiti.taskFormsApi.getRestFieldValues(taskId, field)); + } + + getFormId(res: any) { let result = null;