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;