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 b6e828b34a..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 @@ -52,6 +52,9 @@
+
+ +
UNKNOWN WIDGET TYPE: {{field.type}}
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 index dd3769c367..66ee9f1154 100644 --- 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 @@ -23,4 +23,13 @@ export class GroupUserModel { 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/functional-group/functional-group.widget.ts b/ng2-components/ng2-activiti-form/src/components/widgets/functional-group/functional-group.widget.ts index 7725e841c6..001b6f264f 100644 --- 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 @@ -21,7 +21,6 @@ import { FormService } from '../../../services/form.service'; import { GroupModel } from './../core/group.model'; declare let __moduleName: string; -declare var componentHandler; @Component({ moduleId: __moduleName, 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 8fa82892fe..47846bc631 100644 --- a/ng2-components/ng2-activiti-form/src/components/widgets/index.ts +++ b/ng2-components/ng2-activiti-form/src/components/widgets/index.ts @@ -30,6 +30,7 @@ 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'; @@ -52,6 +53,7 @@ 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, @@ -70,7 +72,8 @@ export const PRIMITIVE_WIDGET_DIRECTIVES: [any] = [ DisplayTextWidget, UploadWidget, TypeaheadWidget, - FunctionalGroupWidget + 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/services/form.service.ts b/ng2-components/ng2-activiti-form/src/services/form.service.ts index 34040c5b1b..95ab51d0cc 100644 --- a/ng2-components/ng2-activiti-form/src/services/form.service.ts +++ b/ng2-components/ng2-activiti-form/src/services/form.service.ts @@ -22,6 +22,7 @@ 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 { @@ -240,6 +241,38 @@ export class FormService { }); } + 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;