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 f89403d7f4..5b136d1bf1 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 @@ -49,6 +49,12 @@
+
+ +
+
+ +
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 bddb2e3dd2..a43aa1de2e 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 @@ -25,6 +25,8 @@ export class FormFieldTypes { static READONLY_TEXT: string = 'readonly-text'; static UPLOAD: string = 'upload'; static TYPEAHEAD: string = 'typeahead'; + static FUNCTIONAL_GROUP: string = 'functional-group'; + static PEOPLE: string = 'people'; static READONLY_TYPES: string[] = [ FormFieldTypes.HYPERLINK, diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/core/group-user.model.ts b/ng2-components/ng2-activiti-form/src/components/widgets/core/group-user.model.ts new file mode 100644 index 0000000000..66ee9f1154 --- /dev/null +++ b/ng2-components/ng2-activiti-form/src/components/widgets/core/group-user.model.ts @@ -0,0 +1,35 @@ +/*! + * @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. + */ + +export class GroupUserModel { + + company: string; + email: string; + firstName: string; + id: string; + lastName: string; + + constructor(json?: any) { + if (json) { + this.company = json.company; + this.email = json.email; + this.firstName = json.firstName; + this.id = json.id; + this.lastName = json.lastName; + } + } +} diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/core/group.model.ts b/ng2-components/ng2-activiti-form/src/components/widgets/core/group.model.ts new file mode 100644 index 0000000000..bf14a9d2a7 --- /dev/null +++ b/ng2-components/ng2-activiti-form/src/components/widgets/core/group.model.ts @@ -0,0 +1,36 @@ +/*! + * @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. + */ + +export class GroupModel { + + externalId: string; + groups: any; + id: string; + name: string; + status: string; + + constructor(json?: any) { + if (json) { + this.externalId = json.externalId; + this.groups = json.groups; + this.id = json.id; + this.name = json.name; + this.status = json.status; + } + } + +} diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/functional-group/functional-group.widget.css b/ng2-components/ng2-activiti-form/src/components/widgets/functional-group/functional-group.widget.css new file mode 100644 index 0000000000..6533c16189 --- /dev/null +++ b/ng2-components/ng2-activiti-form/src/components/widgets/functional-group/functional-group.widget.css @@ -0,0 +1,29 @@ +.functional-group-widget { + width: 100%; +} + +.functional-group-widget--autocomplete { + background-color: #fff; + position: absolute; + z-index: 5; + color: #555; + margin: -15px 0 0 0; +} + +.functional-group-widget--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; +} + +.functional-group-widget--autocomplete > ul > li { + opacity: 1; +} diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/functional-group/functional-group.widget.html b/ng2-components/ng2-activiti-form/src/components/widgets/functional-group/functional-group.widget.html new file mode 100644 index 0000000000..5562b58d6e --- /dev/null +++ b/ng2-components/ng2-activiti-form/src/components/widgets/functional-group/functional-group.widget.html @@ -0,0 +1,21 @@ +
+ + +
+ +
+ +
diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/functional-group/functional-group.widget.spec.ts b/ng2-components/ng2-activiti-form/src/components/widgets/functional-group/functional-group.widget.spec.ts new file mode 100644 index 0000000000..238d9d6739 --- /dev/null +++ b/ng2-components/ng2-activiti-form/src/components/widgets/functional-group/functional-group.widget.spec.ts @@ -0,0 +1,176 @@ +/*! + * @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 { FunctionalGroupWidget } from './functional-group.widget'; +import { FormService } from '../../../services/form.service'; +import { FormModel } from '../core/form.model'; +import { FormFieldModel } from '../core/form-field.model'; +import { GroupModel } from '../core/group.model'; + +describe('FunctionalGroupWidget', () => { + + let formService: FormService; + let widget: FunctionalGroupWidget; + + beforeEach(() => { + formService = new FormService(null, null); + widget = new FunctionalGroupWidget(formService); + widget.field = new FormFieldModel(new FormModel()); + }); + + it('should setup text from underlying field on init', () => { + let group = new GroupModel({ name: 'group-1'}); + widget.field.value = group; + widget.ngOnInit(); + expect(widget.value).toBe(group.name); + }); + + it('should not setup text on init', () => { + widget.field.value = null; + widget.ngOnInit(); + expect(widget.value).toBeUndefined(); + }); + + 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 item click', () => { + let item = new GroupModel({ name: 'group-1' }); + + widget.onItemClick(item, null); + expect(widget.field.value).toBe(item); + expect(widget.value).toBe(item.name); + }); + + 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 groups: GroupModel[] = [ + new GroupModel({ id: '1', name: 'group 1' }), + new GroupModel({ id: '2', name: 'group 2' }) + ]; + + widget.groups = groups; + widget.value = 'group 2'; + widget.flushValue(); + + expect(widget.value).toBe(groups[1].name); + expect(widget.field.value).toBe(groups[1]); + }); + + it('should be case insensitive when flushing value', () => { + let groups: GroupModel[] = [ + new GroupModel({ id: '1', name: 'group 1' }), + new GroupModel({ id: '2', name: 'gRoUp 2' }) + ]; + + widget.groups = groups; + widget.value = 'GROUP 2'; + widget.flushValue(); + + expect(widget.value).toBe(groups[1].name); + expect(widget.field.value).toBe(groups[1]); + }); + + it('should hide popup on key up', () => { + widget.popupVisible = true; + widget.onKeyUp(null); + expect(widget.popupVisible).toBeFalsy(); + }); + + it('should fetch groups and show popup on key up', () => { + let groups: GroupModel[] = [ + new GroupModel(), + new GroupModel() + ]; + spyOn(formService, 'getWorkflowGroups').and.returnValue( + Observable.create(observer => { + observer.next(groups); + observer.complete(); + }) + ); + + widget.value = 'group'; + widget.onKeyUp(null); + + expect(formService.getWorkflowGroups).toHaveBeenCalledWith('group'); + expect(widget.groups).toBe(groups); + expect(widget.popupVisible).toBeTruthy(); + }); + + it('should hide popup when fetching empty group list', () => { + spyOn(formService, 'getWorkflowGroups').and.returnValue( + Observable.create(observer => { + observer.next(null); + observer.complete(); + }) + ); + + widget.value = 'group'; + widget.onKeyUp(null); + + expect(formService.getWorkflowGroups).toHaveBeenCalledWith('group'); + expect(widget.groups.length).toBe(0); + expect(widget.popupVisible).toBeFalsy(); + }); + + it('should not fetch groups when value is missing', () => { + spyOn(formService, 'getWorkflowGroups').and.stub(); + + widget.value = null; + widget.onKeyUp(null); + + expect(formService.getWorkflowGroups).not.toHaveBeenCalled(); + expect(widget.popupVisible).toBeFalsy(); + }); + + it('should not fetch groups when value violates constraints', () => { + spyOn(formService, 'getWorkflowGroups').and.stub(); + + widget.minTermLength = 4; + widget.value = '123'; + widget.onKeyUp(null); + + expect(formService.getWorkflowGroups).not.toHaveBeenCalled(); + expect(widget.popupVisible).toBeFalsy(); + }); +}); diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/functional-group/functional-group.widget.ts b/ng2-components/ng2-activiti-form/src/components/widgets/functional-group/functional-group.widget.ts new file mode 100644 index 0000000000..001b6f264f --- /dev/null +++ b/ng2-components/ng2-activiti-form/src/components/widgets/functional-group/functional-group.widget.ts @@ -0,0 +1,95 @@ +/*! + * @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 { WidgetComponent } from './../widget.component'; +import { FormService } from '../../../services/form.service'; +import { GroupModel } from './../core/group.model'; + +declare let __moduleName: string; + +@Component({ + moduleId: __moduleName, + selector: 'functional-group-widget', + templateUrl: './functional-group.widget.html', + styleUrls: ['./functional-group.widget.css'] +}) +export class FunctionalGroupWidget extends WidgetComponent implements OnInit { + + value: string; + popupVisible: boolean = false; + groups: GroupModel[] = []; + minTermLength: number = 1; + + constructor(private formService: FormService) { + super(); + } + + // TODO: investigate, called 2 times + // https://github.com/angular/angular/issues/6782 + ngOnInit() { + let group = this.field.value; + if (group) { + this.value = group.name; + } + } + + onKeyUp(event: KeyboardEvent) { + if (this.value && this.value.length >= this.minTermLength) { + this.formService.getWorkflowGroups(this.value) + .subscribe((result: GroupModel[]) => { + this.groups = result || []; + this.popupVisible = this.groups.length > 0; + }); + } else { + this.popupVisible = false; + } + } + + onBlur() { + setTimeout(() => { + this.flushValue(); + }, 200); + } + + flushValue() { + this.popupVisible = false; + + let option = this.groups.find(item => item.name.toLocaleLowerCase() === this.value.toLocaleLowerCase()); + + if (option) { + this.field.value = option; + this.value = option.name; + } else { + this.field.value = null; + this.value = null; + } + + this.field.updateForm(); + } + + // TODO: still causes onBlur execution + onItemClick(item: GroupModel, event: Event) { + if (item) { + this.field.value = item; + this.value = item.name; + } + if (event) { + event.preventDefault(); + } + } +} 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 32d1c765ac..47846bc631 100644 --- a/ng2-components/ng2-activiti-form/src/components/widgets/index.ts +++ b/ng2-components/ng2-activiti-form/src/components/widgets/index.ts @@ -29,6 +29,8 @@ 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'; +import { FunctionalGroupWidget } from './functional-group/functional-group.widget'; +import { PeopleWidget } from './people/people.widget'; // core export * from './widget.component'; @@ -50,6 +52,8 @@ 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 * from './functional-group/functional-group.widget'; +export * from './people/people.widget'; export const CONTAINER_WIDGET_DIRECTIVES: [any] = [ TabsWidget, @@ -67,7 +71,9 @@ export const PRIMITIVE_WIDGET_DIRECTIVES: [any] = [ DisplayValueWidget, DisplayTextWidget, UploadWidget, - TypeaheadWidget + TypeaheadWidget, + FunctionalGroupWidget, + PeopleWidget ]; diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/people/people.widget.css b/ng2-components/ng2-activiti-form/src/components/widgets/people/people.widget.css new file mode 100644 index 0000000000..de35519d8b --- /dev/null +++ b/ng2-components/ng2-activiti-form/src/components/widgets/people/people.widget.css @@ -0,0 +1,29 @@ +.people-widget { + width: 100%; +} + +.people-widget--autocomplete { + background-color: #fff; + position: absolute; + z-index: 5; + color: #555; + margin: -15px 0 0 0; +} + +.people-widget--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; +} + +.people-widget--autocomplete > ul > li { + opacity: 1; +} diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/people/people.widget.html b/ng2-components/ng2-activiti-form/src/components/widgets/people/people.widget.html new file mode 100644 index 0000000000..0d660b2bb4 --- /dev/null +++ b/ng2-components/ng2-activiti-form/src/components/widgets/people/people.widget.html @@ -0,0 +1,21 @@ +
+ + +
+ +
+ +
diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/people/people.widget.spec.ts b/ng2-components/ng2-activiti-form/src/components/widgets/people/people.widget.spec.ts new file mode 100644 index 0000000000..c20fe413fa --- /dev/null +++ b/ng2-components/ng2-activiti-form/src/components/widgets/people/people.widget.spec.ts @@ -0,0 +1,208 @@ +/*! + * @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 { PeopleWidget } from './people.widget'; +import { FormService } from '../../../services/form.service'; +import { FormModel } from '../core/form.model'; +import { FormFieldModel } from '../core/form-field.model'; +// import { GroupModel } from '../core/group.model'; +import { GroupUserModel } from '../core/group-user.model'; + +describe('PeopleWidget', () => { + + let formService: FormService; + let widget: PeopleWidget; + + beforeEach(() => { + formService = new FormService(null, null); + widget = new PeopleWidget(formService); + widget.field = new FormFieldModel(new FormModel()); + }); + + it('should return empty display name for missing model', () => { + expect(widget.getDisplayName(null)).toBe(''); + }); + + it('should return full name for a given model', () => { + let model = new GroupUserModel({ + firstName: 'John', + lastName: 'Doe' + }); + expect(widget.getDisplayName(model)).toBe('John Doe'); + }); + + it('should flush value on blur', (done) => { + spyOn(widget, 'flushValue').and.stub(); + widget.onBlur(); + + setTimeout(() => { + expect(widget.flushValue).toHaveBeenCalled(); + done(); + }, 200); + }); + + it('should init value from the field', () => { + widget.field.value = new GroupUserModel({ + firstName: 'John', + lastName: 'Doe' + }); + widget.ngOnInit(); + expect(widget.value).toBe('John Doe'); + }); + + 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 item click', () => { + let item = new GroupUserModel({ firstName: 'John', lastName: 'Doe' }); + + widget.onItemClick(item, null); + expect(widget.field.value).toBe(item); + expect(widget.value).toBe('John Doe'); + }); + + it('should require form field to setup values on init', () => { + widget.field = null; + widget.ngOnInit(); + + expect(widget.value).toBeUndefined(); + expect(widget.groupId).toBeUndefined(); + }); + + it('should setup group restriction', () => { + widget.ngOnInit(); + expect(widget.groupId).toBeUndefined(); + + widget.field.params = { restrictWithGroup: { id: '' } }; + widget.ngOnInit(); + expect(widget.groupId).toBe(''); + }); + + it('should fetch users by search term', () => { + let users = [{}, {}]; + spyOn(formService, 'getWorkflowUsers').and.returnValue(Observable.create(observer => { + observer.next(users); + observer.complete(); + })); + + widget.value = 'user1'; + widget.onKeyUp(null); + + expect(formService.getWorkflowUsers).toHaveBeenCalledWith(widget.value, widget.groupId); + expect(widget.users).toBe(users); + expect(widget.popupVisible).toBeTruthy(); + }); + + it('should fetch users by search term and group id', () => { + let users = [{}, {}]; + spyOn(formService, 'getWorkflowUsers').and.returnValue(Observable.create(observer => { + observer.next(users); + observer.complete(); + })); + + widget.value = 'user1'; + widget.groupId = '1001'; + widget.onKeyUp(null); + + expect(formService.getWorkflowUsers).toHaveBeenCalledWith(widget.value, widget.groupId); + expect(widget.users).toBe(users); + expect(widget.popupVisible).toBeTruthy(); + }); + + it('should fetch users and show no popup', () => { + spyOn(formService, 'getWorkflowUsers').and.returnValue(Observable.create(observer => { + observer.next(null); + observer.complete(); + })); + + widget.value = 'user1'; + widget.onKeyUp(null); + + expect(formService.getWorkflowUsers).toHaveBeenCalledWith(widget.value, widget.groupId); + expect(widget.users).toEqual([]); + expect(widget.popupVisible).toBeFalsy(); + }); + + it('should require search term to fetch users', () => { + spyOn(formService, 'getWorkflowUsers').and.stub(); + + widget.value = null; + widget.onKeyUp(null); + + expect(formService.getWorkflowUsers).not.toHaveBeenCalled(); + }); + + it('should not fetch users due to constraint violation', () => { + spyOn(formService, 'getWorkflowUsers').and.stub(); + + widget.value = '123'; + widget.minTermLength = 4; + widget.onKeyUp(null); + + expect(formService.getWorkflowUsers).not.toHaveBeenCalled(); + }); + + it('should hide popup on value 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 value and update field', () => { + widget.users = [ + new GroupUserModel({ firstName: 'Tony', lastName: 'Stark' }), + new GroupUserModel({ firstName: 'John', lastName: 'Doe' }) + ]; + widget.value = 'John Doe'; + widget.flushValue(); + + expect(widget.value).toBe('John Doe'); + expect(widget.field.value).toBe(widget.users[1]); + }); + + it('should be case insensitive when flushing field', () => { + widget.users = [ + new GroupUserModel({ firstName: 'Tony', lastName: 'Stark' }), + new GroupUserModel({ firstName: 'John', lastName: 'Doe' }) + ]; + widget.value = 'TONY sTaRk'; + widget.flushValue(); + + expect(widget.value).toBe('Tony Stark'); + expect(widget.field.value).toBe(widget.users[0]); + }); + + it('should reset value and field on flush', () => { + widget.value = 'Missing User'; + widget.field.value = {}; + widget.flushValue(); + + expect(widget.value).toBeNull(); + expect(widget.field.value).toBeNull(); + }); +}); diff --git a/ng2-components/ng2-activiti-form/src/components/widgets/people/people.widget.ts b/ng2-components/ng2-activiti-form/src/components/widgets/people/people.widget.ts new file mode 100644 index 0000000000..9eb2a468f6 --- /dev/null +++ b/ng2-components/ng2-activiti-form/src/components/widgets/people/people.widget.ts @@ -0,0 +1,117 @@ +/*! + * @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 { WidgetComponent } from './../widget.component'; +import { FormService } from '../../../services/form.service'; +import { GroupModel } from '../core/group.model'; +import { GroupUserModel } from '../core/group-user.model'; + +declare let __moduleName: string; + +@Component({ + moduleId: __moduleName, + selector: 'people-widget', + templateUrl: './people.widget.html', + styleUrls: ['./people.widget.css'] +}) +export class PeopleWidget extends WidgetComponent implements OnInit { + + popupVisible: boolean = false; + minTermLength: number = 1; + value: string; + users: GroupUserModel[] = []; + groupId: string; + + constructor(private formService: FormService) { + super(); + } + + // TODO: investigate, called 2 times + // https://github.com/angular/angular/issues/6782 + ngOnInit() { + if (this.field) { + let user: GroupUserModel = this.field.value; + if (user) { + this.value = this.getDisplayName(user); + } + + let params = this.field.params; + if (params && params['restrictWithGroup']) { + let restrictWithGroup = params['restrictWithGroup']; + this.groupId = restrictWithGroup.id; + } + } + } + + onKeyUp(event: KeyboardEvent) { + if (this.value && this.value.length >= this.minTermLength) { + this.formService.getWorkflowUsers(this.value, this.groupId) + .subscribe((result: GroupUserModel[]) => { + this.users = result || []; + this.popupVisible = this.users.length > 0; + }); + } else { + this.popupVisible = false; + } + } + + onBlur() { + setTimeout(() => { + this.flushValue(); + }, 200); + } + + flushValue() { + this.popupVisible = false; + + let option = this.users.find(item => { + let fullName = this.getDisplayName(item).toLocaleLowerCase(); + return fullName === this.value.toLocaleLowerCase(); + }); + + if (option) { + this.field.value = option; + this.value = this.getDisplayName(option); + } else { + this.field.value = null; + this.value = null; + } + + this.field.updateForm(); + } + + getDisplayName(model: GroupUserModel) { + if (model) { + let displayName = `${model.firstName} ${model.lastName}`; + return displayName.trim(); + } + + return ''; + } + + // TODO: still causes onBlur execution + onItemClick(item: GroupUserModel, event: Event) { + if (item) { + this.field.value = item; + this.value = this.getDisplayName(item); + } + if (event) { + event.preventDefault(); + } + } +} 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 ab43e475a6..332465da94 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 @@ -11,9 +11,9 @@ -
+
    -
  • {{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 index b0705fc9b1..5dbfdd09eb 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 @@ -72,6 +72,9 @@ describe('TypeaheadWidget', () => { }); it('should show popup on key up', () => { + + spyOn(widget, 'getOptions').and.returnValue([{}, {}]); + widget.minTermLength = 1; widget.value = 'some value'; @@ -80,6 +83,15 @@ describe('TypeaheadWidget', () => { expect(widget.popupVisible).toBeTruthy(); }); + it('should require items to show popup', () => { + widget.minTermLength = 1; + widget.value = 'some value'; + + widget.popupVisible = false; + widget.onKeyUp(null); + expect(widget.popupVisible).toBeFalsy(); + }); + it('should require value to show popup', () => { widget.minTermLength = 1; widget.value = ''; @@ -90,6 +102,8 @@ describe('TypeaheadWidget', () => { }); it('should require value to be of min length to show popup', () => { + spyOn(widget, 'getOptions').and.returnValue([{}, {}]); + widget.minTermLength = 3; widget.value = 'v'; 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 2eaeae6db2..d58ccf6f83 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 @@ -34,6 +34,7 @@ export class TypeaheadWidget extends WidgetComponent implements OnInit { popupVisible: boolean = false; minTermLength: number = 1; value: string; + options: FormFieldOption[] = []; constructor(private formService: FormService) { super(); @@ -72,7 +73,12 @@ export class TypeaheadWidget extends WidgetComponent implements OnInit { } onKeyUp(event: KeyboardEvent) { - this.popupVisible = !!(this.value && this.value.length >= this.minTermLength); + if (this.value && this.value.length >= this.minTermLength) { + this.options = this.getOptions(); + this.popupVisible = this.options.length > 0; + } else { + this.popupVisible = false; + } } onBlur() { @@ -97,6 +103,7 @@ export class TypeaheadWidget extends WidgetComponent implements OnInit { this.field.updateForm(); } + // TODO: still causes onBlur execution onItemClick(item: FormFieldOption, event: Event) { if (item) { this.field.value = item.id; 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 cd26ce0ca2..95ab51d0cc 100644 --- a/ng2-components/ng2-activiti-form/src/services/form.service.ts +++ b/ng2-components/ng2-activiti-form/src/services/form.service.ts @@ -17,10 +17,12 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs/Rx'; -import { AlfrescoAuthenticationService } from 'ng2-alfresco-core'; +import { AlfrescoApiService } from 'ng2-alfresco-core'; import { FormValues } from './../components/widgets/core/index'; import { FormDefinitionModel } from '../models/form-definition.model'; import { EcmModelService } from './ecm-model.service'; +import { GroupModel } from './../components/widgets/core/group.model'; +import { GroupUserModel } from './../components/widgets/core/group-user.model'; @Injectable() export class FormService { @@ -28,15 +30,15 @@ export class FormService { static UNKNOWN_ERROR_MESSAGE: string = 'Unknown error'; static GENERIC_ERROR_MESSAGE: string = 'Server error'; - constructor(private authService: AlfrescoAuthenticationService, - private ecmModelService: EcmModelService) { + constructor(private ecmModelService: EcmModelService, + private apiService: AlfrescoApiService) { } /** * Create a Form with a fields for each metadata properties * @returns {Observable} */ - public createFormFromANode(formName: string): Observable { + createFormFromANode(formName: string): Observable { return Observable.create(observer => { this.createForm(formName).subscribe( form => { @@ -57,7 +59,7 @@ export class FormService { * Create a Form * @returns {Observable} */ - public createForm(formName: string): Observable { + createForm(formName: string): Observable { let dataModel = { name: formName, description: '', @@ -65,27 +67,27 @@ export class FormService { stencilSet: 0 }; - return Observable.fromPromise(this.authService.getAlfrescoApi().activiti.modelsApi.createModel(dataModel)); + return Observable.fromPromise(this.apiService.getInstance().activiti.modelsApi.createModel(dataModel)); } /** * Add Fileds to A form * @returns {Observable} */ - public addFieldsToAForm(formId: string, formModel: FormDefinitionModel): Observable { - return Observable.fromPromise(this.authService.getAlfrescoApi().activiti.editorApi.saveForm(formId, formModel)); + addFieldsToAForm(formId: string, formModel: FormDefinitionModel): Observable { + return Observable.fromPromise(this.apiService.getInstance().activiti.editorApi.saveForm(formId, formModel)); } /** * Search For A Form by name * @returns {Observable} */ - public searchFrom(name: string): Observable { + searchFrom(name: string): Observable { let opts = { 'modelType': 2 }; - return Observable.fromPromise(this.authService.getAlfrescoApi().activiti.modelsApi.getModels(opts)).map(function (forms: any) { + return Observable.fromPromise(this.apiService.getInstance().activiti.modelsApi.getModels(opts)).map(function (forms: any) { return forms.data.find(formdata => formdata.name === name); }).catch(this.handleError); } @@ -94,20 +96,20 @@ export class FormService { * Get All the forms * @returns {Observable} */ - public getForms(): Observable { + getForms(): Observable { let opts = { 'modelType': 2 }; - return Observable.fromPromise(this.authService.getAlfrescoApi().activiti.modelsApi.getModels(opts)); + return Observable.fromPromise(this.apiService.getInstance().activiti.modelsApi.getModels(opts)); } /** * Get Process Definition * @returns {Observable} */ - public getProcessDefinitions(): Observable { - return Observable.fromPromise(this.authService.getAlfrescoApi().activiti.processApi.getProcessDefinitions({})) + getProcessDefinitions(): Observable { + return Observable.fromPromise(this.apiService.getInstance().activiti.processApi.getProcessDefinitions({})) .map(this.toJsonArray) .catch(this.handleError); } @@ -117,8 +119,8 @@ export class FormService { * @param taskId Task Id * @returns {Observable} */ - public getTasks(): Observable { - return Observable.fromPromise(this.authService.getAlfrescoApi().activiti.taskApi.listTasks({})) + getTasks(): Observable { + return Observable.fromPromise(this.apiService.getInstance().activiti.taskApi.listTasks({})) .map(this.toJsonArray) .catch(this.handleError); } @@ -128,8 +130,8 @@ export class FormService { * @param taskId Task Id * @returns {Observable} */ - public getTask(taskId: string): Observable { - return Observable.fromPromise(this.authService.getAlfrescoApi().activiti.taskApi.getTask(taskId)) + getTask(taskId: string): Observable { + return Observable.fromPromise(this.apiService.getInstance().activiti.taskApi.getTask(taskId)) .map(this.toJson) .catch(this.handleError); } @@ -140,10 +142,10 @@ export class FormService { * @param formValues Form Values * @returns {Observable} */ - public saveTaskForm(taskId: string, formValues: FormValues): Observable { + saveTaskForm(taskId: string, formValues: FormValues): Observable { let body = JSON.stringify({values: formValues}); - return Observable.fromPromise(this.authService.getAlfrescoApi().activiti.taskApi.saveTaskForm(taskId, body)) + return Observable.fromPromise(this.apiService.getInstance().activiti.taskApi.saveTaskForm(taskId, body)) .catch(this.handleError); } @@ -154,14 +156,14 @@ export class FormService { * @param outcome Form Outcome * @returns {Observable} */ - public completeTaskForm(taskId: string, formValues: FormValues, outcome?: string): Observable { + completeTaskForm(taskId: string, formValues: FormValues, outcome?: string): Observable { let data: any = {values: formValues}; if (outcome) { data.outcome = outcome; } let body = JSON.stringify(data); - return Observable.fromPromise(this.authService.getAlfrescoApi().activiti.taskApi.completeTaskForm(taskId, body)) + return Observable.fromPromise(this.apiService.getInstance().activiti.taskApi.completeTaskForm(taskId, body)) .catch(this.handleError); } @@ -170,8 +172,8 @@ export class FormService { * @param taskId Task Id * @returns {Observable} */ - public getTaskForm(taskId: string): Observable { - return Observable.fromPromise(this.authService.getAlfrescoApi().activiti.taskApi.getTaskForm(taskId)) + getTaskForm(taskId: string): Observable { + return Observable.fromPromise(this.apiService.getInstance().activiti.taskApi.getTaskForm(taskId)) .map(this.toJson) .catch(this.handleError); } @@ -181,8 +183,8 @@ export class FormService { * @param formId Form Id * @returns {Observable} */ - public getFormDefinitionById(formId: string): Observable { - return Observable.fromPromise(this.authService.getAlfrescoApi().activiti.editorApi.getForm(formId)) + getFormDefinitionById(formId: string): Observable { + return Observable.fromPromise(this.apiService.getInstance().activiti.editorApi.getForm(formId)) .map(this.toJson) .catch(this.handleError); } @@ -192,23 +194,85 @@ export class FormService { * @param name * @returns {Promise|Promise} */ - public getFormDefinitionByName(name: string): Observable { + getFormDefinitionByName(name: string): Observable { let opts = { 'filter': 'myReusableForms', 'filterText': name, 'modelType': 2 }; - return Observable.fromPromise(this.authService.getAlfrescoApi().activiti.modelsApi.getModels(opts)) + return Observable.fromPromise(this.apiService.getInstance().activiti.modelsApi.getModels(opts)) .map(this.getFormId) .catch(this.handleError); } getRestFieldValues(taskId: string, field: string): Observable { - let alfrescoApi = this.authService.getAlfrescoApi(); + let alfrescoApi = this.apiService.getInstance(); return Observable.fromPromise(alfrescoApi.activiti.taskFormsApi.getRestFieldValues(taskId, field)); } + // TODO: uses private webApp api + getWorkflowGroups(filter: string): Observable { + return Observable.create(observer => { + + let xhr: XMLHttpRequest = new XMLHttpRequest(); + xhr.withCredentials = true; + + xhr.onreadystatechange = () => { + if (xhr.readyState === 4) { + if (xhr.status === 200) { + let json = JSON.parse(xhr.response); + let data: GroupModel[] = (json.data || []).map(item => item); + // console.log(json); + observer.next(data); + observer.complete(); + } else { + console.error(xhr.response); + Observable.throw(new Error(xhr.response)); + } + } + }; + + let host = this.apiService.getInstance().config.hostBpm; + let url = `${host}/activiti-app/app/rest/workflow-groups?filter=${filter}`; + xhr.open('GET', url, true); + xhr.setRequestHeader('Authorization', this.apiService.getInstance().getTicketBpm()); + xhr.send(); + }); + } + + getWorkflowUsers(filter: string, groupId?: string): Observable { + return Observable.create(observer => { + + let xhr: XMLHttpRequest = new XMLHttpRequest(); + xhr.withCredentials = true; + + xhr.onreadystatechange = () => { + if (xhr.readyState === 4) { + if (xhr.status === 200) { + let json = JSON.parse(xhr.response); + let data: GroupUserModel[] = (json.data || []).map(item => item); + // console.log(json); + observer.next(data); + observer.complete(); + } else { + console.error(xhr.response); + Observable.throw(new Error(xhr.response)); + } + } + }; + + let host = this.apiService.getInstance().config.hostBpm; + let url = `${host}/activiti-app/app/rest/workflow-users?filter=${filter}`; + if (groupId) { + url += `&groupId=${groupId}`; + } + xhr.open('GET', url, true); + xhr.setRequestHeader('Authorization', this.apiService.getInstance().getTicketBpm()); + xhr.send(); + }); + } + getFormId(res: any) { let result = null;