[ADF-794] Add people assignment component (#1977)

* Add people component

* exported people service

* added people-list component to show the involved user list

* changed people-search component layout

* changed people-list usage in people component

* changed people-list data table from custom template to data adapter

* changes people-search component related to people-list

* changes in activiti-people related to people-list and people-search component

* changed data adapter to direct data column setting to data-table

* removed ngChanges and added User and UserEvent models

* added User and UserEvent model in emitter and other emitter handler

* added user event model

* changed activiti-people component with latest UX changes

* addedand changed translate keys to the components

* added hasUser method to check the condition in html

* fixed tslint issue and test case issue in activiti-people component

* added test case for actviti-people-list component

* test case added for activiti-people-search component

* changed activiti-people test cases according to latest UX changes

* added description for activiti-people component

* changed test case to fix component.upgradeElement issue

* changes requested by Vito Albano #1

* splitted getDisplayUser into getDisplayUser and getInitialUsername
This commit is contained in:
Infad Kachancheri
2017-06-21 01:32:48 -07:00
committed by Vito
parent 8d2ccb40d9
commit 7f4614cebf
19 changed files with 632 additions and 200 deletions
@@ -425,6 +425,31 @@ The purpose of the component is populate the local variable called `properties`
"description": "string"
}
```
## Task People Component
This component displays involved users to a specified task
```html
<activiti-people
[people]="YOUR_INVOLVED_PEOPLE_LIST"
[taskId]="YOUR_TASK_ID"
[readOnly]="YOUR_READ_ONLY_FLAG">
</activiti-people>
```
![activiti-people](docs/assets/activiti_people.png)
### Properties
| Name | Type | Description |
| --- | --- | --- |
| people | User[] | The array of User object to display |
| taskId | string | The numeric ID of the task |
| readOnly | boolean | The boolean flag |
#### Events
No Events
## Build from sources
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

@@ -36,11 +36,13 @@ import {
ActivitiStartTaskButton,
ActivitiPeopleSearch,
TaskAttachmentListComponent,
ActivitiCreateTaskAttachmentComponent
ActivitiCreateTaskAttachmentComponent,
ActivitiPeopleList
} from './src/components/index';
export * from './src/components/index';
export * from './src/services/activiti-tasklist.service';
export * from './src/services/activiti-people.service';
export * from './src/models/index';
export const ACTIVITI_TASKLIST_DIRECTIVES: any[] = [
@@ -56,7 +58,8 @@ export const ACTIVITI_TASKLIST_DIRECTIVES: any[] = [
ActivitiStartTaskButton,
ActivitiPeopleSearch,
TaskAttachmentListComponent,
ActivitiCreateTaskAttachmentComponent
ActivitiCreateTaskAttachmentComponent,
ActivitiPeopleList
];
export const ACTIVITI_TASKLIST_PROVIDERS: any[] = [
@@ -0,0 +1,7 @@
<alfresco-datatable
[rows]="users"
[actions]="hasActions()"
(rowClick)="selectUser($event)"
(showRowActionsMenu)="onShowRowActionsMenu($event)"
(executeRowAction)="onExecuteRowAction($event)">
</alfresco-datatable>
@@ -0,0 +1,100 @@
/*!
* @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 { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { Observable } from 'rxjs/Observable';
import { CoreModule, AlfrescoTranslationService } from 'ng2-alfresco-core';
import { ActivitiPeopleList } from './activiti-people-list.component';
import { User, UserEventModel } from '../models/index';
import { DataTableModule, ObjectDataRow, DataRowEvent, DataRowActionEvent } from 'ng2-alfresco-datatable';
declare let jasmine: any;
const fakeUser: User = new User({
id: '1',
firstName: 'fake-name',
lastName: 'fake-last',
email: 'fake@mail.com'
});
describe('ActivitiPeopleList', () => {
let activitiPeopleListComponent: ActivitiPeopleList;
let fixture: ComponentFixture<ActivitiPeopleList>;
let element: HTMLElement;
let componentHandler;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
CoreModule.forRoot(),
DataTableModule
],
declarations: [
ActivitiPeopleList
]
}).compileComponents().then(() => {
let translateService = TestBed.get(AlfrescoTranslationService);
spyOn(translateService, 'addTranslationFolder').and.stub();
spyOn(translateService.translate, 'get').and.callFake((key) => { return Observable.of(key); });
fixture = TestBed.createComponent(ActivitiPeopleList);
activitiPeopleListComponent = fixture.componentInstance;
element = fixture.nativeElement;
componentHandler = jasmine.createSpyObj('componentHandler', [
'upgradeAllRegistered'
]);
window['componentHandler'] = componentHandler;
fixture.detectChanges();
});
}));
it('should emit row click event', (done) => {
let row = new ObjectDataRow(fakeUser);
let rowEvent = new DataRowEvent(row, null);
activitiPeopleListComponent.clickRow.subscribe(selectedUser => {
expect(selectedUser.id).toEqual('1');
expect(selectedUser.email).toEqual('fake@mail.com');
expect(activitiPeopleListComponent.user.id).toEqual('1');
expect(activitiPeopleListComponent.user.email).toEqual('fake@mail.com');
done();
});
activitiPeopleListComponent.selectUser(rowEvent);
});
it('should emit row action event', (done) => {
let row = new ObjectDataRow(fakeUser);
let removeObj = {
name: 'remove',
title: 'Remove'
};
let rowActionEvent = new DataRowActionEvent(row, removeObj);
activitiPeopleListComponent.clickAction.subscribe((selectedAction: UserEventModel) => {
expect(selectedAction.type).toEqual('remove');
expect(selectedAction.value.id).toEqual('1');
expect(selectedAction.value.email).toEqual('fake@mail.com');
done();
});
activitiPeopleListComponent.onExecuteRowAction(rowActionEvent);
});
});
@@ -0,0 +1,99 @@
/*!
* @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, Input, Output, EventEmitter, ViewChild, ContentChild } from '@angular/core';
import { User, UserEventModel } from '../models/index';
import { DataColumnListComponent } from 'ng2-alfresco-core';
import { DataTableComponent } from 'ng2-alfresco-datatable';
declare let componentHandler: any;
@Component({
selector: 'activiti-people-list',
templateUrl: './activiti-people-list.component.html',
styleUrls: ['./activiti-people-list.component.css']
})
export class ActivitiPeopleList {
@ContentChild(DataColumnListComponent) columnList: DataColumnListComponent;
@ViewChild(DataTableComponent)
peopleDataTable: DataTableComponent;
@Input()
users: User[];
@Input()
actions: boolean = false;
@Output()
clickRow: EventEmitter<User> = new EventEmitter<User>();
@Output()
clickAction: EventEmitter<UserEventModel> = new EventEmitter<UserEventModel>();
user: User;
constructor() {
}
ngAfterContentInit() {
this.peopleDataTable.columnList = this.columnList;
}
ngAfterViewInit() {
this.setupMaterialComponents(componentHandler);
}
setupMaterialComponents(handler?: any): boolean {
// workaround for MDL issues with dynamic components
let isUpgraded: boolean = false;
if (handler) {
handler.upgradeAllRegistered();
isUpgraded = true;
}
return isUpgraded;
}
selectUser(event: any) {
this.user = event.value.obj;
this.clickRow.emit(this.user);
}
hasActions(): boolean {
return this.actions;
}
onShowRowActionsMenu(event: any) {
let removeAction = {
title: 'Remove',
name: 'remove'
};
event.value.actions = [
removeAction
];
}
onExecuteRowAction(event: any) {
let args = event.value;
let action = args.action;
this.clickAction.emit(new UserEventModel({type: action.name, value: args.row.obj}));
}
}
@@ -33,4 +33,60 @@
.mdl-chip-search-people__text{
padding-left: 10px;
}
}
.search-text-header{
font-weight: bold;
opacity: 0.54;
}
.search-list-container{
max-height: 152px;
width: 100%;
overflow-y: auto;
}
activiti-people-list >>> alfresco-datatable >>> thead {
display: none;
}
.search-list-action-container {
border-top: 1px solid #eee;
text-align: right;
padding: 5px 0px;
margin-top: 5px;
}
.search-list-action-container>button{
opacity: 0.54;
font-weight: bolder;
}
.search-list-action-container>button:hover{
color: rgb(255, 152, 0);
}
activiti-people-list >>> alfresco-datatable >>> .people-full-name {
font-family: 'Muli';
}
activiti-people-list >>> alfresco-datatable >>> .people-pic {
background: #ffc800;
padding: 10px 6px;
border-radius: 100px;
color: #fff;
text-align: center;
font-weight: bolder;
font-size: 16px;
font-family: Muli;
text-transform: uppercase;
min-width: 30px;
}
activiti-people-list >>> alfresco-datatable >>> td.mdl-data-table__cell--non-numeric.non-selectable.data-cell{
padding: 4px 12px;
}
.mdl-textfield {
width: 100%;
}
@@ -1,15 +1,33 @@
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
<input class="mdl-textfield__input" type="text" id="userSearchText" [value]="" [formControl]="searchUser"/>
<label class="mdl-textfield__label" for="userSearchText">Search user</label>
</div>
<ul class='mdl-list'>
<li class="mdl-list__item fix-element-user-list" *ngFor="let user of userList">
<button (click)="onRowClick(user)" id="user-{{user.id}}" class="mdl-chip mdl-chip--contact mdl-chip-search-people">
<img class="mdl-chip__contact" [src]="iconImageUrl" />
<span class="mdl-chip__text mdl-chip-search-people__text">{{getDisplayUser(user)}}</span>
</button>
</li>
<div *ngIf="userList?.length === 0" id="no-user-found">
{{'PEOPLE.SEARCH.NO_USERS' | translate }}
<div class="search-text-header">{{ 'TASK_DETAILS.LABELS.ADD_PEOPLE' | translate }}</div>
<div class="search-text-container">
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
<input class="mdl-textfield__input" type="text" id="userSearchText" [value]="" [formControl]="searchUser"/>
<label class="mdl-textfield__label" for="userSearchText">Search user</label>
</div>
</ul>
</div>
<div class="search-list-container" id="search-people-list" *ngIf="hasUsers()">
<activiti-people-list
[users]="users"
(clickRow)="onRowClick($event)">
<data-columns>
<data-column key="firstName">
<ng-template let-entry="$implicit">
<div class="people-pic">{{getInitialUserName(entry.row.obj.firstName, entry.row.obj.lastName)}}</div>
</ng-template>
</data-column>
<data-column key="email" class="full-width">
<ng-template let-entry="$implicit">
<div class="people-full-name">{{ getDisplayUser(entry.row.obj.firstName, entry.row.obj.lastName, ' ') }}</div>
</ng-template>
</data-column>
</data-columns>
</activiti-people-list>
</div>
<div class="search-list-action-container">
<button type="button" id="close-people-search" (click)="closeSearchList()" class="mdl-button close">
{{'PEOPLE.DIALOG_CLOSE' | translate }}
</button>
<button type="button" id="add-people" (click)="addInvolvedUser()" class="mdl-button close">
{{'PEOPLE.ADD_USER' | translate }}
</button>
</div>
@@ -19,6 +19,8 @@ import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { Observable } from 'rxjs/Observable';
import { CoreModule, AlfrescoTranslationService } from 'ng2-alfresco-core';
import { ActivitiPeopleSearch } from './activiti-people-search.component';
import { ActivitiPeopleList } from './activiti-people-list.component';
import { DataTableModule } from 'ng2-alfresco-datatable';
import { User } from '../models/user.model';
declare let jasmine: any;
@@ -49,10 +51,12 @@ describe('ActivitiPeopleSearch', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
CoreModule.forRoot()
CoreModule.forRoot(),
DataTableModule
],
declarations: [
ActivitiPeopleSearch
ActivitiPeopleSearch,
ActivitiPeopleList
]
}).compileComponents().then(() => {
@@ -78,28 +82,25 @@ describe('ActivitiPeopleSearch', () => {
expect(element.querySelector('#userSearchText')).not.toBeNull();
});
it('should show no user found to involve message', () => {
it('should hide people-list container', () => {
fixture.detectChanges();
fixture.whenStable()
.then(() => {
expect(element.querySelector('#no-user-found')).not.toBeNull();
expect(element.querySelector('#no-user-found').textContent).toContain('PEOPLE.SEARCH.NO_USERS');
expect(element.querySelector('#search-people-list')).toBeNull();
});
});
it('should show user which can be involved ', (done) => {
activitiPeopleSearchComponent.onSearch.subscribe(() => {
activitiPeopleSearchComponent.searchPeople.subscribe(() => {
activitiPeopleSearchComponent.results = Observable.of(userArray);
activitiPeopleSearchComponent.ngOnInit();
fixture.detectChanges();
fixture.whenStable()
.then(() => {
expect(element.querySelector('#user-1')).not.toBeNull();
expect(element.querySelector('#user-1').textContent)
.toContain('fake-name - fake-last');
expect(element.querySelector('#user-2')).not.toBeNull();
expect(element.querySelector('#user-2').textContent)
.toContain('fake-involve-name - fake-involve-last');
let gatewayElement: any = element.querySelector('#search-people-list tbody');
expect(gatewayElement).not.toBeNull();
expect(gatewayElement.children.length).toBe(2);
done();
});
});
@@ -110,7 +111,7 @@ describe('ActivitiPeopleSearch', () => {
});
it('should send an event when an user is clicked', (done) => {
activitiPeopleSearchComponent.onRowClicked.subscribe((user) => {
activitiPeopleSearchComponent.success.subscribe((user) => {
expect(user).toBeDefined();
expect(user.firstName).toBe('fake-name');
done();
@@ -120,8 +121,9 @@ describe('ActivitiPeopleSearch', () => {
fixture.detectChanges();
fixture.whenStable()
.then(() => {
let userToSelect = <HTMLElement> element.querySelector('#user-1');
userToSelect.click();
activitiPeopleSearchComponent.onRowClick(fakeUser);
let addUserButton = <HTMLElement> element.querySelector('#add-people');
addUserButton.click();
});
});
@@ -129,13 +131,16 @@ describe('ActivitiPeopleSearch', () => {
activitiPeopleSearchComponent.results = Observable.of(userArray);
activitiPeopleSearchComponent.ngOnInit();
fixture.detectChanges();
let userToSelect = <HTMLElement> element.querySelector('#user-1');
userToSelect.click();
activitiPeopleSearchComponent.onRowClick(fakeUser);
let addUserButton = <HTMLElement> element.querySelector('#add-people');
addUserButton.click();
fixture.detectChanges();
fixture.whenStable()
.then(() => {
expect(element.querySelector('#user-1')).toBeNull();
let gatewayElement: any = element.querySelector('#search-people-list tbody');
expect(gatewayElement).not.toBeNull();
expect(gatewayElement.children.length).toBe(1);
done();
});
});
@@ -32,21 +32,23 @@ declare var require: any;
export class ActivitiPeopleSearch implements OnInit, AfterViewInit {
@Input()
iconImageUrl: string = require('../assets/images/user.jpg');
@Input()
results: Observable<User[]>;
@Output()
onSearch: EventEmitter<any> = new EventEmitter();
searchPeople: EventEmitter<any> = new EventEmitter();
@Output()
onRowClicked: EventEmitter<any> = new EventEmitter();
success: EventEmitter<User> = new EventEmitter<User>();
@Output()
closeSearch = new EventEmitter();
searchUser: FormControl = new FormControl();
userList: User[] = [];
users: User[] = [];
selectedUser: User;
constructor(private translateService: AlfrescoTranslationService) {
if (translateService) {
@@ -58,16 +60,16 @@ export class ActivitiPeopleSearch implements OnInit, AfterViewInit {
.debounceTime(200)
.subscribe((event: string) => {
if (event && event.trim()) {
this.onSearch.emit(event);
this.searchPeople.emit(event);
} else {
this.userList = [];
this.users = [];
}
});
}
ngOnInit() {
this.results.subscribe((list) => {
this.userList = list;
this.users = list;
});
}
@@ -85,17 +87,38 @@ export class ActivitiPeopleSearch implements OnInit, AfterViewInit {
return isUpgraded;
}
onRowClick(userClicked: User) {
this.onRowClicked.emit(userClicked);
this.userList = this.userList.filter((user) => {
onRowClick(user: User) {
this.selectedUser = user;
}
closeSearchList() {
this.closeSearch.emit();
}
addInvolvedUser() {
if (this.selectedUser === undefined) {
return;
}
this.success.emit(this.selectedUser);
this.users = this.users.filter((user) => {
this.searchUser.reset();
return user.id !== userClicked.id;
return user.id !== this.selectedUser.id;
});
}
getDisplayUser(user: User): string {
let firstName = user.firstName && user.firstName !== 'null' ? user.firstName : 'N/A';
let lastName = user.lastName && user.lastName !== 'null' ? user.lastName : 'N/A';
return firstName + ' - ' + lastName;
getDisplayUser(firstName: string, lastName: string, delimiter: string = '-'): string {
firstName = (firstName !== null ? firstName : '');
lastName = (lastName !== null ? lastName : '');
return firstName + delimiter + lastName;
}
getInitialUserName(firstName: string, lastName: string) {
firstName = (firstName !== null && firstName !== '' ? firstName[0] : '');
lastName = (lastName !== null && lastName !== '' ? lastName[0] : '');
return this.getDisplayUser(firstName, lastName, '');
}
hasUsers() {
return (this.users && this.users.length > 0);
}
}
@@ -1,23 +1,74 @@
:host {
.assignment-header{
width: 100%;
border-bottom: 1px solid #eee;
padding: 6px 20px;
}
.assigment-count{
float: left;
padding: 10px 0px;
font-weight: bolder;
font-family: Muli;
opacity: 0.54;
}
.add-people{
float: right;
padding: 8px;
height: 26px;
opacity: 0.54;
cursor: pointer;
}
.add-people:hover{
color: #ff9100;
}
.assignment-top-container{
border-top: 2px solid #eee;
margin: 8px;
padding: 0px;
}
.assignment-container{
padding: 10px 20px;
border-bottom: 1px solid #eee;
width: 100%;
}
.activiti-label {
.assignment-list-container {
padding: 0px;
}
activiti-people-list >>> alfresco-datatable >>> thead {
display: none;
}
activiti-people-list >>> alfresco-datatable >>> .people-full-name {
font-family: 'Muli';
}
activiti-people-list >>> alfresco-datatable >>> .people-email {
font-family: 'Muli';
opacity: 0.54;
}
activiti-people-list >>> alfresco-datatable >>> .people-edit-label {
font-family: 'Muli';
}
activiti-people-list >>> alfresco-datatable >>> .people-pic {
background: #ffc800;
padding: 12px 10px;
border-radius: 100px;
color: #fff;
text-align: center;
font-weight: bolder;
font-size: 18px;
font-family: Muli;
text-transform: uppercase
}
.material-icons.people__icon:hover {
color: rgb(255, 152, 0);
}
.add-people-dialog__content {
padding: 20px 24px 2px;
}
.mdl-tooltip {
will-change: unset;
}
.material-icons {
cursor: pointer;
activiti-people-list >>> alfresco-datatable >>> td.mdl-data-table__cell--non-numeric.non-selectable.data-cell{
padding: 10px;
}
@@ -1,36 +1,42 @@
<span class="activiti-label mdl-badge" id="people-title"
[attr.data-badge]="people?.length">{{ 'TASK_DETAILS.LABELS.PEOPLE' | translate }}</span>
<div *ngIf="!readOnly" id="addPeople" (click)="showDialog()" class="icon material-icons people__icon">add</div>
<div *ngIf="!readOnly" class="mdl-tooltip" data-mdl-for="addPeople">
Add a person
</div>
<div class="menu-container" *ngIf="people?.length > 0">
<ul class='mdl-list'>
<li class="mdl-list__item" *ngFor="let user of people">
<span class="mdl-chip mdl-chip--contact mdl-chip--deletable">
<img class="mdl-chip__contact" [src]="iconImageUrl" />
<span id="user-{{user.id}}" class="mdl-chip__text">{{getDisplayUser(user)}}</span>
<a *ngIf="!readOnly" class="mdl-chip__action"><i id="remove-{{user.id}}" (click)="removeInvolvedUser(user)" class="material-icons people__icon">cancel</i></a>
</span>
</li>
</ul>
</div>
<div *ngIf="people?.length === 0" id="no-people-label">
{{ 'TASK_DETAILS.PEOPLE.NONE' | translate }}
</div>
<dialog class="mdl-dialog" id="add-people-dialog" #dialog>
<h4 class="mdl-dialog__title" id="add-people-dialog-title">Involve User</h4>
<div class="mdl-dialog__content add-people-dialog__content">
<activiti-people-search (onSearch)="searchUser($event)"
(onRowClicked)="involveUser($event)"
[results]="peopleSearch$"
[iconImageUrl]="iconImageUrl" #activitipeoplesearch>
<div class="mdl-grid mdl-shadow--2dp assignment-top-container">
<div class="assignment-header">
<div *ngIf="hasPeople()" class="assigment-count" id="people-title">
{{ 'TASK_DETAILS.LABELS.PEOPLE' | translate }} {{ ' (' + people.length + ')' }}
</div>
<div *ngIf="!hasPeople()" class="assigment-count" id="no-people-label">
{{ 'TASK_DETAILS.PEOPLE.NONE' | translate }}
</div>
<div *ngIf="isEditMode()" class="add-people" (click)="onAddAssignement()">
<i class="material-icons">person_add</i>
</div>
</div>
<div class="assignment-container" *ngIf="showAssignment">
<activiti-people-search
(searchPeople)="searchUser($event)"
(success)="involveUser($event)"
(closeSearch)="onCloseSearch()"
[results]="peopleSearch$">
</activiti-people-search>
</div>
<div class="mdl-dialog__actions">
<button type="button" id="close-people-dialog" (click)="closeDialog()" class="mdl-button close">
{{'PEOPLE.DIALOG_CLOSE' | translate }}
</button>
<div class="assignment-list-container" id="assignment-people-list" *ngIf="hasPeople()">
<activiti-people-list
[users]="people"
[actions]="isEditMode()"
(clickAction)="onClickAction($event)">
<data-columns>
<data-column key="firstName">
<ng-template let-entry="$implicit">
<div class="people-pic">{{getInitialUserName(entry.row.obj.firstName, entry.row.obj.lastName)}}</div>
</ng-template>
</data-column>
<data-column key="email" class="full-width">
<ng-template let-entry="$implicit">
<div class="people-full-name">{{ getDisplayUser(entry.row.obj.firstName, entry.row.obj.lastName, ' ') }}</div>
<div class="people-email">{{ entry.row.obj.email }}</div>
<div class="people-edit-label">can edit</div>
</ng-template>
</data-column>
</data-columns>
</activiti-people-list>
</div>
</dialog>
</div>
@@ -16,12 +16,14 @@
*/
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { Observable } from 'rxjs/Rx';
import { Observable } from 'rxjs/Observable';
import { CoreModule, AlfrescoTranslationService, LogService } from 'ng2-alfresco-core';
import { ActivitiPeopleService } from '../services/activiti-people.service';
import { ActivitiPeople } from './activiti-people.component';
import { ActivitiPeopleSearch } from './activiti-people-search.component';
import { ActivitiPeopleList } from './activiti-people-list.component';
import { ActivitiPeople } from './activiti-people.component';
import { DataTableModule } from 'ng2-alfresco-datatable';
import { User } from '../models/user.model';
import { ActivitiPeopleService } from '../services/activiti-people.service';
declare let jasmine: any;
@@ -32,7 +34,7 @@ const fakeUser: User = new User({
email: 'fake@mail.com'
});
const fakeUserToInvolve: User = new User({
const fakeSecondUser: User = new User({
id: 'fake-involve-id',
firstName: 'fake-involve-name',
lastName: 'fake-involve-last',
@@ -45,21 +47,25 @@ describe('ActivitiPeople', () => {
let fixture: ComponentFixture<ActivitiPeople>;
let element: HTMLElement;
let componentHandler;
let userArray = [fakeUser, fakeSecondUser];
let logService: LogService;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
CoreModule.forRoot()
CoreModule.forRoot(),
DataTableModule
],
declarations: [
ActivitiPeople,
ActivitiPeopleSearch
ActivitiPeopleSearch,
ActivitiPeopleList,
ActivitiPeople
],
providers: [
ActivitiPeopleService
]
}).compileComponents().then(() => {
logService = TestBed.get(LogService);
let translateService = TestBed.get(AlfrescoTranslationService);
@@ -74,16 +80,24 @@ describe('ActivitiPeople', () => {
]);
window['componentHandler'] = componentHandler;
activitiPeopleComponent.people = [];
activitiPeopleComponent.readOnly = true;
fixture.detectChanges();
});
}));
it('should show people component title', () => {
expect(element.querySelector('#people-title')).toBeDefined();
expect(element.querySelector('#people-title')).not.toBeNull();
});
it('should show people component title', async(() => {
activitiPeopleComponent.people = [...userArray];
fixture.detectChanges();
fixture.whenStable()
.then(() => {
expect(element.querySelector('#people-title')).toBeDefined();
expect(element.querySelector('#people-title')).not.toBeNull();
});
}));
it('should show no people involved message', () => {
fixture.detectChanges();
fixture.whenStable()
.then(() => {
expect(element.querySelector('#no-people-label')).not.toBeNull();
@@ -91,51 +105,11 @@ describe('ActivitiPeople', () => {
});
});
describe('when interact with people dialog', () => {
beforeEach(() => {
activitiPeopleComponent.taskId = 'fake-task-id';
activitiPeopleComponent.people = [];
fixture.detectChanges();
});
it('should show dialog when clicked on add', () => {
expect(element.querySelector('#addPeople')).not.toBeNull();
activitiPeopleComponent.showDialog();
expect(element.querySelector('#add-people-dialog')).not.toBeNull();
expect(element.querySelector('#add-people-dialog-title')).not.toBeNull();
expect(element.querySelector('#add-people-dialog-title').textContent).toContain('Involve User');
});
it('should close dialog when clicked on cancel', () => {
activitiPeopleComponent.showDialog();
expect(element.querySelector('#addPeople')).not.toBeNull();
activitiPeopleComponent.closeDialog();
let dialogWindow = <HTMLElement> element.querySelector('#add-people-dialog');
expect(dialogWindow.getAttribute('open')).toBeNull();
});
it('should reset search input when the dialog is closed', () => {
let userInputSearch: HTMLInputElement;
activitiPeopleComponent.showDialog();
expect(element.querySelector('#addPeople')).not.toBeNull();
userInputSearch = <HTMLInputElement> element.querySelector('#userSearchText');
userInputSearch.value = 'fake-search-value';
activitiPeopleComponent.closeDialog();
activitiPeopleComponent.showDialog();
userInputSearch = <HTMLInputElement> element.querySelector('#userSearchText');
expect(userInputSearch).not.toBeNull();
expect(userInputSearch.value).toBeFalsy();
});
});
describe('when there are involved people', () => {
beforeEach(() => {
activitiPeopleComponent.taskId = 'fake-task-id';
activitiPeopleComponent.people.push(fakeUser);
activitiPeopleComponent.people.push(...userArray);
fixture.detectChanges();
});
@@ -147,11 +121,14 @@ describe('ActivitiPeople', () => {
jasmine.Ajax.uninstall();
});
it('should show people involved', () => {
expect(element.querySelector('#user-fake-id')).not.toBeNull();
expect(element.querySelector('#user-fake-id').textContent).toContain('fake-name');
expect(element.querySelector('#user-fake-id').textContent).toContain('fake-last');
});
it('should show people involved', async(() => {
fixture.whenStable()
.then(() => {
let gatewayElement: any = element.querySelector('#assignment-people-list tbody');
expect(gatewayElement).not.toBeNull();
expect(gatewayElement.children.length).toBe(2);
});
}));
it('should remove pepole involved', async(() => {
activitiPeopleComponent.removeInvolvedUser(fakeUser);
@@ -161,21 +138,23 @@ describe('ActivitiPeople', () => {
fixture.whenStable()
.then(() => {
fixture.detectChanges();
expect(element.querySelector('#user-fake-id')).toBeNull();
let gatewayElement: any = element.querySelector('#assignment-people-list tbody');
expect(gatewayElement).not.toBeNull();
expect(gatewayElement.children.length).toBe(1);
});
}));
it('should involve pepole', async(() => {
activitiPeopleComponent.involveUser(fakeUserToInvolve);
activitiPeopleComponent.involveUser(fakeUser);
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200
});
fixture.whenStable()
.then(() => {
fixture.detectChanges();
expect(element.querySelector('#user-fake-involve-id')).not.toBeNull();
expect(element.querySelector('#user-fake-involve-id').textContent)
.toBe('fake-involve-name fake-involve-last');
let gatewayElement: any = element.querySelector('#assignment-people-list tbody');
expect(gatewayElement).not.toBeNull();
expect(gatewayElement.children.length).toBe(3);
});
}));
@@ -226,6 +205,8 @@ describe('ActivitiPeople', () => {
beforeEach(() => {
jasmine.Ajax.install();
activitiPeopleComponent.people.push(...userArray);
fixture.detectChanges();
});
afterEach(() => {
@@ -243,8 +224,6 @@ describe('ActivitiPeople', () => {
}));
it('should not remove user if remove involved user fail', async(() => {
activitiPeopleComponent.people.push(fakeUser);
fixture.detectChanges();
activitiPeopleComponent.removeInvolvedUser(fakeUser);
jasmine.Ajax.requests.mostRecent().respondWith({
status: 403
@@ -252,22 +231,23 @@ describe('ActivitiPeople', () => {
fixture.whenStable()
.then(() => {
fixture.detectChanges();
expect(element.querySelector('#user-fake-id')).not.toBeNull();
expect(element.querySelector('#user-fake-id').textContent)
.toBe('fake-name fake-last');
let gatewayElement: any = element.querySelector('#assignment-people-list tbody');
expect(gatewayElement).not.toBeNull();
expect(gatewayElement.children.length).toBe(2);
});
}));
it('should not involve user if involve user fail', async(() => {
activitiPeopleComponent.involveUser(fakeUserToInvolve);
activitiPeopleComponent.involveUser(fakeUser);
jasmine.Ajax.requests.mostRecent().respondWith({
status: 403
});
fixture.whenStable()
.then(() => {
fixture.detectChanges();
expect(element.querySelector('#user-fake-id')).toBeNull();
expect(element.querySelector('#no-people-label').textContent).toContain('TASK_DETAILS.PEOPLE.NONE');
let gatewayElement: any = element.querySelector('#assignment-people-list tbody');
expect(gatewayElement).not.toBeNull();
expect(gatewayElement.children.length).toBe(2);
});
}));
});
@@ -15,13 +15,13 @@
* limitations under the License.
*/
import { Component, Input, ViewChild } from '@angular/core';
import { Component, Input, AfterViewInit } from '@angular/core';
import { Observer, Observable } from 'rxjs/Rx';
import { AlfrescoTranslationService, LogService } from 'ng2-alfresco-core';
import { User } from '../models/user.model';
import { User, UserEventModel } from '../models/index';
import { ActivitiPeopleService } from '../services/activiti-people.service';
declare let dialogPolyfill: any;
declare let componentHandler: any;
declare var require: any;
@Component({
@@ -29,7 +29,7 @@ declare var require: any;
templateUrl: './activiti-people.component.html',
styleUrls: ['./activiti-people.component.css']
})
export class ActivitiPeople {
export class ActivitiPeople implements AfterViewInit {
@Input()
iconImageUrl: string = require('../assets/images/user.jpg');
@@ -43,11 +43,7 @@ export class ActivitiPeople {
@Input()
readOnly: boolean = false;
@ViewChild('dialog')
dialog: any;
@ViewChild('activitipeoplesearch')
activitipeoplesearch: any;
showAssignment: boolean = false;
private peopleSearchObserver: Observer<User[]>;
peopleSearch$: Observable<User[]>;
@@ -66,21 +62,18 @@ export class ActivitiPeople {
this.peopleSearch$ = new Observable<User[]>(observer => this.peopleSearchObserver = observer).share();
}
public showDialog() {
if (!this.dialog.nativeElement.showModal) {
dialogPolyfill.registerDialog(this.dialog.nativeElement);
}
if (this.dialog) {
this.dialog.nativeElement.showModal();
}
ngAfterViewInit() {
this.setupMaterialComponents(componentHandler);
}
public closeDialog() {
if (this.dialog) {
this.dialog.nativeElement.close();
this.peopleSearchObserver.next([]);
this.activitipeoplesearch.searchUser.reset();
setupMaterialComponents(handler?: any): boolean {
// workaround for MDL issues with dynamic components
let isUpgraded: boolean = false;
if (handler) {
handler.upgradeAllRegistered();
isUpgraded = true;
}
return isUpgraded;
}
searchUser(searchedWord: string) {
@@ -91,9 +84,10 @@ export class ActivitiPeople {
}
involveUser(user: User) {
this.showAssignment = false;
this.peopleService.involveUserWithTask(this.taskId, user.id.toString())
.subscribe(() => {
this.people.push(user);
this.people = [...this.people, user];
}, error => this.logService.error('Impossible to involve user with task'));
}
@@ -106,10 +100,38 @@ export class ActivitiPeople {
}, error => this.logService.error('Impossible to remove involved user from task'));
}
getDisplayUser(user: User): string {
let firstName = user.firstName && user.firstName !== 'null' ? user.firstName : 'N/A';
let lastName = user.lastName && user.lastName !== 'null' ? user.lastName : 'N/A';
return firstName + ' ' + lastName;
getDisplayUser(firstName: string, lastName: string, delimiter: string = '-'): string {
firstName = (firstName !== null ? firstName : '');
lastName = (lastName !== null ? lastName : '');
return firstName + delimiter + lastName;
}
getInitialUserName(firstName: string, lastName: string) {
firstName = (firstName !== null && firstName !== '' ? firstName[0] : '');
lastName = (lastName !== null && lastName !== '' ? lastName[0] : '');
return this.getDisplayUser(firstName, lastName, '');
}
onAddAssignement() {
this.showAssignment = true;
}
onClickAction(event: UserEventModel) {
if (event.type === 'remove') {
this.removeInvolvedUser(event.value);
}
}
hasPeople() {
return this.people && this.people.length > 0;
}
isEditMode() {
return !this.readOnly;
}
onCloseSearch() {
this.showAssignment = false;
}
}
@@ -28,3 +28,4 @@ export * from './activiti-task-details.component';
export * from './activiti-start-task.component';
export * from './activiti-people-search.component';
export * from './adf-create-task-attachment.component';
export * from './activiti-people-list.component';
@@ -12,9 +12,10 @@
"ASSIGNEE": "Assignee",
"DUE": "Due",
"FORM": "Form",
"PEOPLE": "People",
"PEOPLE": "People this task is shared with",
"COMMENTS": "Comments",
"CHECKLIST": "Checklist"
"CHECKLIST": "Checklist",
"ADD_PEOPLE": "Add people & groups"
},
"BUTTON": {
"COMPLETE": "Complete",
@@ -84,6 +85,7 @@
},
"PEOPLE": {
"DIALOG_CLOSE": "CLOSE",
"ADD_USER": "ADD",
"SEARCH": {
"NO_USERS": "No user found to involve"
}
@@ -21,3 +21,4 @@ export * from './icon.model';
export * from './user.model';
export * from './task-details.model';
export * from './task-details.event';
export * from './user-event.model';
@@ -0,0 +1,33 @@
/*!
* @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.
*/
/**
*
* This object represent the User Event.
*
*
* @returns {UserEventModel} .
*/
export class UserEventModel {
type: string = '';
value: any = {};
constructor(obj?: any) {
this.type = obj && obj.type;
this.value = obj && obj.value || {};
}
}