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 @@
+
+
+
+
+
+
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;