[ADF-3812] Add multi selection and roles filtering to adf-cloud-people component (#4068)

* [ADF-3812] Added multiple user selection and user pre-selection

* [ADF-3812] Added tests

* [ADF-3812] Added jsdoc

* [ADF-3812] Improved variable naming

* [ADF-3812] Improved mode selection

* [ADF-3812] Changed input name and emit logic

* [ADF-3812] Used modified emitter name in start task

* [ADF-3812] Improved default role selection

* Use the new strategy to fetch the authorized users

* * Fixed pre-selection in single mode

* * Added invalid selection validation

* * Added start task assignee validation

* * Improved preset loading

* * Improved tests

* * Added test to validate default assignee

* * Added methods to check user has access to an app

* * Added app access to people cloud and start task

* * Refactored methods and removed unused input - showCurrentUser

* * Added tests

* * Changed service names and removed unwated services

* * Used formControl error instead of manual error flag

* * Used new hasError method of people component inside start task

* * Improved tests

* * Updated callCustomApi call signature

* * Added documentation

* * Disabled search until clientId is retrieved

* * Changed realm name

* * Added jsdoc for service methods

* Remove the useless doc
This commit is contained in:
Deepak Paul
2019-01-14 19:20:19 +05:30
committed by Eugenio Romano
parent 46150a65f2
commit f08ad08d0f
10 changed files with 630 additions and 151 deletions

View File

@@ -0,0 +1,45 @@
---
Title: People Cloud Component
Added: v3.0.0
Status: Active
Last reviewed: 2019-09-01
---
# [People Cloud Component](../../lib/process-services-cloud/src/lib/process-services-cloud/src/lib/task/start-task/components/people-cloud/people-cloud.component.ts")
An autosuggest input control that allows single or multiple users to be selected based on the input parameters.
## Contents
- [Basic Usage](#basic-usage)
- [Class members](#class-members)
- [Properties](#properties)
- [Events](#events)
## Basic Usage
```html
<adf-cloud-people
[appName]="'simple-app'"
[mode]="'multiple'">
</adf-cloud-people>
```
## Class members
### Properties
| Name | Type | Default value | Description |
| ---- | ---- | ------------- | ----------- |
| appName | `string` | | Name of the application. If specified, shows the users who have access to the app. |
| mode | `string` | 'single' | Mode of the user selection (single/multiple). |
| roles | `string[]` | | Role names of the users to be listed. |
| preSelectUsers | `IdentityUserModel[]` | | Array of users to be pre-selected. Pre-select all users in `multiple` mode and only the first user of the array in `single` mode. |
### Events
| Name | Type | Description |
| ---- | ---- | ----------- |
| selectUser | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`IdentityUserModel`](../../lib/core/userinfo/models/identity-user.model.ts)`>` | Emitted when a user is selected. |
| removeUser | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`IdentityUserModel`](../../lib/core/userinfo/models/identity-user.model.ts)`>` | Emitted when a selected user is removed in `multiple` mode. |
| error | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<Any>` | Emitted when an error occurs. |

View File

@@ -17,12 +17,13 @@
import { ApiService } from '../APS-cloud/apiservice';
import { Util } from '../../util/util';
import { AppConfigService } from '@alfresco/adf-core';
export class Identity {
api: ApiService = new ApiService();
constructor() {
constructor(appConfig: AppConfigService) {
}
async init(username, password) {

View File

@@ -199,4 +199,52 @@ describe('IdentityUserService', () => {
}
);
});
it('should return true when user has access to an application', (done) => {
spyOn(service, 'getClientIdByApplicationName').and.returnValue(of('mock-client'));
spyOn(service, 'getClientRoles').and.returnValue(of(mockRoles));
service.checkUserHasClientApp('user-id', 'app-name').subscribe(
(res: boolean) => {
expect(res).toBeTruthy();
done();
}
);
});
it('should return false when user does not have access to an application', (done) => {
spyOn(service, 'getClientIdByApplicationName').and.returnValue(of('mock-client'));
spyOn(service, 'getClientRoles').and.returnValue(of([]));
service.checkUserHasClientApp('user-id', 'app-name').subscribe(
(res: boolean) => {
expect(res).toBeFalsy();
done();
}
);
});
it('should return true when user has any given application role', (done) => {
spyOn(service, 'getClientIdByApplicationName').and.returnValue(of('mock-client'));
spyOn(service, 'getClientRoles').and.returnValue(of(mockRoles));
service.checkUserHasAnyClientAppRole('user-id', 'app-name', [mockRoles[1].name] ).subscribe(
(res: boolean) => {
expect(res).toBeTruthy();
done();
}
);
});
it('should return false when user does not have any given application role', (done) => {
spyOn(service, 'getClientIdByApplicationName').and.returnValue(of('mock-client'));
spyOn(service, 'getClientRoles').and.returnValue(of([]));
service.checkUserHasAnyClientAppRole('user-id', 'app-name', [mockRoles[1].name]).subscribe(
(res: boolean) => {
expect(res).toBeFalsy();
done();
}
);
});
});

View File

@@ -16,8 +16,8 @@
*/
import { Injectable } from '@angular/core';
import { Observable, from } from 'rxjs';
import { map } from 'rxjs/operators';
import { Observable, from, of } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
import { IdentityUserModel } from '../models/identity-user.model';
import { JwtHelperService } from '../../services/jwt-helper.service';
@@ -44,7 +44,6 @@ export class IdentityUserService {
/**
* Gets the name and other basic details of the current user.
* @returns User details
*/
getCurrentUserInfo(): IdentityUserModel {
const familyName = this.getValueFromToken<string>(IdentityUserService.FAMILY_NAME);
@@ -57,8 +56,6 @@ export class IdentityUserService {
/**
* Gets a named value from the user access token.
* @param key Key name of the field to retrieve
* @returns Value associated with the key
*/
getValueFromToken<T>(key: string): T {
let value;
@@ -70,9 +67,123 @@ export class IdentityUserService {
return <T> value;
}
/**
* Find users based on search input.
*/
findUsersByName(search: string): Observable<any> {
if (search === '') {
return of([]);
}
const url = this.buildUserUrl();
const httpMethod = 'GET', pathParams = {}, queryParams = {search: search}, bodyParam = {}, headerParams = {},
formParams = {}, contentTypes = ['application/json'], accepts = ['application/json'];
return (from(this.apiService.getInstance().oauth2Auth.callCustomApi(
url, httpMethod, pathParams, queryParams,
headerParams, formParams, bodyParam,
contentTypes, accepts, Object, null, null)
));
}
/**
* Get client roles of a user for a particular client.
*/
getClientRoles(userId: string, clientId: string): Observable<any[]> {
const url = this.buildUserClientRoleMapping(userId, clientId);
const httpMethod = 'GET', pathParams = {}, queryParams = {}, bodyParam = {}, headerParams = {},
formParams = {}, contentTypes = ['application/json'], accepts = ['application/json'];
return from(this.apiService.getInstance().oauth2Auth.callCustomApi(
url, httpMethod, pathParams, queryParams,
headerParams, formParams, bodyParam,
contentTypes, accepts, Object, null, null)
);
}
/**
* Checks whether user has access to a client app.
*/
checkUserHasClientApp(userId: string, clientId: string): Observable<boolean> {
return this.getClientRoles(userId, clientId).pipe(
map((clientRoles: any[]) => {
if (clientRoles.length > 0) {
return true;
}
return false;
})
);
}
/**
* Checks whether user has any of client app role.
*/
checkUserHasAnyClientAppRole(userId: string, clientId: string, roleNames: string[]): Observable<boolean> {
return this.getClientRoles(userId, clientId).pipe(
map((clientRoles: any[]) => {
let hasRole = false;
if (clientRoles.length > 0) {
roleNames.forEach((roleName) => {
const role = clientRoles.find((availableRole) => {
return availableRole.name === roleName;
});
if (role) {
hasRole = true;
return;
}
});
}
return hasRole;
})
);
}
/**
* Get client id for an application.
*/
getClientIdByApplicationName(applicationName: string): Observable<string> {
const url = this.buildGetClientsUrl();
const httpMethod = 'GET', pathParams = {}, queryParams = {clientId: applicationName}, bodyParam = {}, headerParams = {}, formParams = {},
contentTypes = ['application/json'], accepts = ['application/json'];
return from(this.apiService.getInstance()
.oauth2Auth.callCustomApi(url, httpMethod, pathParams, queryParams, headerParams,
formParams, bodyParam, contentTypes,
accepts, Object, null, null)
).pipe(
map((response: any[]) => {
const clientId = response && response.length > 0 ? response[0].id : '';
return clientId;
})
);
}
/**
* Checks a user has access to an application
* @param userId Id of the user
* @param applicationName Name of the application
* @returns Boolean
*/
checkUserHasApplicationAccess(userId: string, applicationName: string): Observable<boolean> {
return this.getClientIdByApplicationName(applicationName).pipe(
switchMap((clientId: string) => {
return this.checkUserHasClientApp(userId, clientId);
})
);
}
/**
* Checks a user has any application role
*/
checkUserHasAnyApplicationRole(userId: string, applicationName: string, roleNames: string[]): Observable<boolean> {
return this.getClientIdByApplicationName(applicationName).pipe(
switchMap((clientId: string) => {
return this.checkUserHasAnyClientAppRole(userId, clientId, roleNames);
})
);
}
/**
* Gets details for all users.
* @returns Array of user info objects
*/
getUsers(): Observable<IdentityUserModel[]> {
const url = this.buildUserUrl();
@@ -92,8 +203,6 @@ export class IdentityUserService {
/**
* Gets a list of roles for a user.
* @param userId ID of the user
* @returns Array of role info objects
*/
getUserRoles(userId: string): Observable<IdentityRoleModel[]> {
const url = this.buildRolesUrl(userId);
@@ -113,8 +222,6 @@ export class IdentityUserService {
/**
* Gets an array of users (including the current user) who have any of the roles in the supplied list.
* @param roleNames List of role names to look for
* @returns Array of user info objects
*/
async getUsersByRolesWithCurrentUser(roleNames: string[]): Promise<IdentityUserModel[]> {
const filteredUsers: IdentityUserModel[] = [];
@@ -134,8 +241,6 @@ export class IdentityUserService {
/**
* Gets an array of users (not including the current user) who have any of the roles in the supplied list.
* @param roleNames List of role names to look for
* @returns Array of user info objects
*/
async getUsersByRolesWithoutCurrentUser(roleNames: string[]): Promise<IdentityUserModel[]> {
const filteredUsers: IdentityUserModel[] = [];
@@ -173,8 +278,16 @@ export class IdentityUserService {
return `${this.appConfigService.get('identityHost')}/users`;
}
private buildUserClientRoleMapping(userId: string, clientId: string): any {
return `${this.appConfigService.get('identityHost')}/users/${userId}/role-mappings/clients/${clientId}`;
}
private buildRolesUrl(userId: string): any {
return `${this.appConfigService.get('identityHost')}/users/${userId}/role-mappings/realm/composite`;
}
private buildGetClientsUrl() {
return `${this.appConfigService.get('identityHost')}/clients`;
}
}

View File

@@ -1,18 +1,40 @@
<form>
<mat-form-field class="adf-people-cloud">
<mat-label id="assignee-id">{{'ADF_CLOUD_TASK_LIST.START_TASK.FORM.LABEL.ASSIGNEE' | translate}}</mat-label>
<input #inputValue
<mat-label id="assignee-id">{{'ADF_TASK_LIST.START_TASK.FORM.LABEL.ASSIGNEE' | translate}}</mat-label>
<mat-chip-list #userChipList *ngIf="isMultipleMode(); else singleSelection">
<mat-chip
*ngFor="let user of selectedUsers$ | async"
(removed)="onRemove(user)">
{{user | fullName}}
<mat-icon matChipRemove>cancel</mat-icon>
</mat-chip>
<input
#userInput
matInput
[formControl]="searchUserCtrl"
[matAutocomplete]="auto"
[matChipInputFor]="userChipList"
class="adf-cloud-input"
(focus)="setFocus(true)"
(blur)="setFocus(false)"
data-automation-id="adf-people-cloud-search-input">
</mat-chip-list>
<ng-template #singleSelection>
<input matInput
(focus)="setFocus(true)"
(blur)="setFocus(false)"
class="adf-cloud-input"
data-automation-id="adf-people-cloud-search-input"
type="text"
[formControl]="searchUser"
[formControl]="searchUserCtrl"
[matAutocomplete]="auto">
</ng-template>
<mat-autocomplete autoActiveFirstOption class="adf-people-cloud-list"
#auto="matAutocomplete"
(optionSelected)="onSelect($event.option.value)"
[displayWith]="getDisplayName">
<mat-option *ngFor="let user of users$ | async; let i = index" [value]="user">
<mat-option *ngFor="let user of searchUsers$ | async; let i = index" [value]="user">
<div class="adf-people-cloud-row" id="adf-people-cloud-user-{{i}}">
<div [outerHTML]="user | usernameInitials:'adf-people-widget-pic'"></div>
<span class="adf-people-label-name"> {{user | fullName}}</span>
@@ -21,7 +43,7 @@
</mat-autocomplete>
</mat-form-field>
<div class="adf-start-task-cloud-error">
<div *ngIf="dataError" fxLayout="row" fxLayoutAlign="start start" [@transitionMessages]="_subscriptAnimationState">
<div *ngIf="hasErrorMessage()" fxLayout="row" fxLayoutAlign="start start" [@transitionMessages]="_subscriptAnimationState">
<div class="adf-start-task-cloud-error-message">{{ 'ADF_CLOUD_START_TASK.ERROR.MESSAGE' | translate }}</div>
<mat-icon class="adf-start-task-cloud-error-icon">warning</mat-icon>
</div>

View File

@@ -20,7 +20,7 @@ import { By } from '@angular/platform-browser';
import { PeopleCloudComponent } from './people-cloud.component';
import { StartTaskCloudTestingModule } from '../../testing/start-task-cloud.testing.module';
import { LogService, setupTestBed, IdentityUserService, IdentityUserModel } from '@alfresco/adf-core';
import { mockUsers, mockRoles } from '../../mock/user-cloud.mock';
import { mockUsers } from '../../mock/user-cloud.mock';
import { of } from 'rxjs';
import { ProcessServiceCloudTestingModule } from '../../../../testing/process-service-cloud.testing.module';
@@ -29,8 +29,8 @@ describe('PeopleCloudComponent', () => {
let fixture: ComponentFixture<PeopleCloudComponent>;
let element: HTMLElement;
let identityService: IdentityUserService;
let getRolesByUserIdSpy: jasmine.Spy;
let getUserSpy: jasmine.Spy;
let findUsersSpy: jasmine.Spy;
let checkUserHasAccessSpy: jasmine.Spy;
setupTestBed({
imports: [ProcessServiceCloudTestingModule, StartTaskCloudTestingModule],
@@ -42,40 +42,17 @@ describe('PeopleCloudComponent', () => {
component = fixture.componentInstance;
element = fixture.nativeElement;
identityService = TestBed.get(IdentityUserService);
getRolesByUserIdSpy = spyOn(identityService, 'getUserRoles').and.returnValue(of(mockRoles));
getUserSpy = spyOn(identityService, 'getUsers').and.returnValue(of(mockUsers));
findUsersSpy = spyOn(identityService, 'findUsersByName').and.returnValue(of(mockUsers));
checkUserHasAccessSpy = spyOn(identityService, 'checkUserHasClientApp').and.returnValue(of(true));
spyOn(identityService, 'getClientIdByApplicationName').and.returnValue(of('mock-client-id'));
});
it('should create PeopleCloudComponent', () => {
expect(component instanceof PeopleCloudComponent).toBeTruthy();
});
it('should able to fetch users', () => {
fixture.detectChanges();
expect(getUserSpy).toHaveBeenCalled();
});
it('should able to fetch roles by user id', async(() => {
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(getRolesByUserIdSpy).toHaveBeenCalled();
});
}));
it('should not list the current logged in user when showCurrentUser is false', async(() => {
spyOn(identityService, 'getCurrentUserInfo').and.returnValue(mockUsers[1]);
component.showCurrentUser = false;
fixture.detectChanges();
fixture.whenStable().then(() => {
const currentUser = component.users.find((user) => {
return user.username === mockUsers[1].username;
});
expect(currentUser).toBeUndefined();
});
}));
it('should show the users if the typed result match', async(() => {
component.users$ = of(<IdentityUserModel[]> mockUsers);
component.searchUsers$ = of(<IdentityUserModel[]> mockUsers);
fixture.detectChanges();
let inputHTMLElement: HTMLInputElement = <HTMLInputElement> element.querySelector('input');
inputHTMLElement.focus();
@@ -90,7 +67,7 @@ describe('PeopleCloudComponent', () => {
});
}));
it('should hide result list if input is empty', () => {
it('should hide result list if input is empty', async(() => {
fixture.detectChanges();
let inputHTMLElement: HTMLInputElement = <HTMLInputElement> element.querySelector('input');
inputHTMLElement.focus();
@@ -99,40 +76,176 @@ describe('PeopleCloudComponent', () => {
inputHTMLElement.dispatchEvent(new Event('input'));
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('mat-option'))).toBeNull();
expect(fixture.debugElement.query(By.css('#adf-people-cloud-user-0'))).toBeNull();
});
});
}));
it('should emit selectedUser if option is valid', async() => {
it('should emit selectedUser if option is valid', async(() => {
fixture.detectChanges();
let selectEmitSpy = spyOn(component.selectedUser, 'emit');
let selectEmitSpy = spyOn(component.selectUser, 'emit');
component.onSelect(new IdentityUserModel({ username: 'username'}));
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(selectEmitSpy).toHaveBeenCalled();
});
});
}));
it('should show an error message if the user is invalid', async(() => {
getUserSpy.and.returnValue(of([]));
getRolesByUserIdSpy.and.returnValue(of([]));
component.dataError = true;
checkUserHasAccessSpy.and.returnValue(of(false));
findUsersSpy.and.returnValue(of([]));
fixture.detectChanges();
let inputHTMLElement: HTMLInputElement = <HTMLInputElement> element.querySelector('input');
const inputHTMLElement: HTMLInputElement = <HTMLInputElement> element.querySelector('input');
inputHTMLElement.focus();
inputHTMLElement.dispatchEvent(new Event('input'));
inputHTMLElement.dispatchEvent(new Event('keyup'));
inputHTMLElement.dispatchEvent(new Event('keydown'));
inputHTMLElement.value = 'ZZZ';
inputHTMLElement.dispatchEvent(new Event('input'));
fixture.detectChanges();
fixture.whenStable().then(() => {
inputHTMLElement.blur();
fixture.detectChanges();
const errorMessage = element.querySelector('.adf-start-task-cloud-error-message');
expect(element.querySelector('.adf-start-task-cloud-error')).not.toBeNull();
expect(errorMessage).not.toBeNull();
expect(errorMessage.textContent).toContain('ADF_CLOUD_START_TASK.ERROR.MESSAGE');
});
}));
it('should show chip list when mode=multiple', async(() => {
component.mode = 'multiple';
fixture.detectChanges();
fixture.whenStable().then(() => {
const chip = element.querySelector('mat-chip-list');
expect(chip).toBeDefined();
});
}));
it('should not show chip list when mode=single', async(() => {
component.mode = 'single';
fixture.detectChanges();
fixture.whenStable().then(() => {
const chip = element.querySelector('mat-chip-list');
expect(chip).toBeNull();
});
}));
it('should pre-select all preSelectUsers when mode=multiple', async(() => {
spyOn(identityService, 'getUsersByRolesWithCurrentUser').and.returnValue(Promise.resolve(mockUsers));
component.mode = 'multiple';
component.preSelectUsers = <any> [{id: mockUsers[1].id}, {id: mockUsers[2].id}];
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
const chips = fixture.debugElement.queryAll(By.css('mat-chip'));
expect(chips.length).toBe(2);
});
}));
it('should not pre-select any user when preSelectUsers is empty and mode=multiple', async(() => {
spyOn(identityService, 'getUsersByRolesWithCurrentUser').and.returnValue(Promise.resolve(mockUsers));
component.mode = 'multiple';
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
const chip = fixture.debugElement.query(By.css('mat-chip'));
expect(chip).toBeNull();
});
}));
it('should pre-select preSelectUsers[0] when mode=single', async(() => {
spyOn(identityService, 'getUsersByRolesWithCurrentUser').and.returnValue(Promise.resolve(mockUsers));
component.mode = 'single';
component.preSelectUsers = <any> [{id: mockUsers[1].id}, {id: mockUsers[2].id}];
fixture.detectChanges();
fixture.whenStable().then(() => {
const selectedUser = component.searchUserCtrl.value;
expect(selectedUser.id).toBe(mockUsers[1].id);
});
}));
it('should not pre-select any user when preSelectUsers is empty and mode=single', async(() => {
spyOn(identityService, 'getUsersByRolesWithCurrentUser').and.returnValue(Promise.resolve(mockUsers));
component.mode = 'single';
fixture.detectChanges();
fixture.whenStable().then(() => {
const selectedUser = component.searchUserCtrl.value;
expect(selectedUser).toBeNull();
});
}));
it('should emit removeUser when a selected user is removed if mode=multiple', async(() => {
spyOn(identityService, 'getUsersByRolesWithCurrentUser').and.returnValue(Promise.resolve(mockUsers));
let removeUserSpy = spyOn(component.removeUser, 'emit');
component.mode = 'multiple';
component.preSelectUsers = <any> [{id: mockUsers[1].id}, {id: mockUsers[2].id}];
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
const removeIcon = fixture.debugElement.query(By.css('mat-chip mat-icon'));
removeIcon.nativeElement.click();
expect(removeUserSpy).toHaveBeenCalledWith({ id: mockUsers[1].id });
});
}));
it('should list users who have access to the app when appName is specified', async(() => {
component.appName = 'sample-app';
fixture.detectChanges();
let inputHTMLElement: HTMLInputElement = <HTMLInputElement> element.querySelector('input');
inputHTMLElement.focus();
inputHTMLElement.value = 'M';
inputHTMLElement.dispatchEvent(new Event('input'));
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
const usersList = fixture.debugElement.queryAll(By.css('mat-option'));
expect(usersList.length).toBe(mockUsers.length);
});
}));
it('should not list users who do not have access to the app when appName is specified', async(() => {
checkUserHasAccessSpy.and.returnValue(of(false));
component.appName = 'sample-app';
fixture.detectChanges();
let inputHTMLElement: HTMLInputElement = <HTMLInputElement> element.querySelector('input');
inputHTMLElement.focus();
inputHTMLElement.value = 'M';
inputHTMLElement.dispatchEvent(new Event('input'));
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
const usersList = fixture.debugElement.queryAll(By.css('mat-option'));
expect(usersList.length).toBe(0);
});
}));
it('should validate access to the app when appName is specified', async(() => {
component.appName = 'sample-app';
fixture.detectChanges();
let inputHTMLElement: HTMLInputElement = <HTMLInputElement> element.querySelector('input');
inputHTMLElement.focus();
inputHTMLElement.value = 'M';
inputHTMLElement.dispatchEvent(new Event('input'));
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(checkUserHasAccessSpy).toHaveBeenCalledTimes(mockUsers.length);
});
}));
it('should not validate access to the app when appName is not specified', async(() => {
fixture.detectChanges();
let inputHTMLElement: HTMLInputElement = <HTMLInputElement> element.querySelector('input');
inputHTMLElement.focus();
inputHTMLElement.value = 'M';
inputHTMLElement.dispatchEvent(new Event('input'));
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(checkUserHasAccessSpy).not.toHaveBeenCalled();
});
}));
});

View File

@@ -16,8 +16,9 @@
*/
import { FormControl } from '@angular/forms';
import { Component, OnInit, Output, EventEmitter, ViewEncapsulation, Input } from '@angular/core';
import { Observable, of } from 'rxjs';
import { Component, OnInit, Output, EventEmitter, ViewEncapsulation, Input, ViewChild, ElementRef } from '@angular/core';
import { Observable, of, BehaviorSubject } from 'rxjs';
import { switchMap, debounceTime, distinctUntilChanged, mergeMap, tap, filter } from 'rxjs/operators';
import { FullNamePipe, IdentityUserModel, IdentityUserService } from '@alfresco/adf-core';
import { trigger, state, style, transition, animate } from '@angular/animations';
@@ -40,71 +41,227 @@ import { trigger, state, style, transition, animate } from '@angular/animations'
export class PeopleCloudComponent implements OnInit {
static ROLE_ACTIVITI_ADMIN = 'ACTIVITI_ADMIN';
static ROLE_ACTIVITI_USER = 'ACTIVITI_USER';
static ROLE_ACTIVITI_MODELER = 'ACTIVITI_MODELER';
static MODE_SINGLE = 'single';
static MODE_MULTIPLE = 'multiple';
/** Show current user in the list or not. */
/** Name of the application. If specified, shows the users who have access to the app. */
@Input()
showCurrentUser: boolean = true;
appName: string;
/** Mode of the user selection (single/multiple). */
@Input()
mode: string = PeopleCloudComponent.MODE_SINGLE;
/** Role names of the users to be listed. */
@Input()
roles: string[];
/** Array of users to be pre-selected. Pre-select all users in multi selection mode and only the first user of the array in single selection mode. */
@Input()
preSelectUsers: IdentityUserModel[];
/** Emitted when a user is selected. */
@Output()
selectedUser: EventEmitter<IdentityUserModel> = new EventEmitter<IdentityUserModel>();
selectUser: EventEmitter<IdentityUserModel> = new EventEmitter<IdentityUserModel>();
/** Emitted when a selected user is removed in multi selection mode. */
@Output()
removeUser: EventEmitter<IdentityUserModel> = new EventEmitter<IdentityUserModel>();
/** Emitted when an error occurs. */
@Output()
error: EventEmitter<any> = new EventEmitter<any>();
users$: Observable<IdentityUserModel[]>;
@ViewChild('userInput')
private userInput: ElementRef<HTMLInputElement>;
searchUser: FormControl = new FormControl();
private _selectedUsers: IdentityUserModel[] = [];
private _searchUsers: IdentityUserModel[] = [];
private selectedUsers: BehaviorSubject<IdentityUserModel[]>;
private searchUsers: BehaviorSubject<IdentityUserModel[]>;
selectedUsers$: Observable<IdentityUserModel[]>;
searchUsers$: Observable<IdentityUserModel[]>;
searchUserCtrl: FormControl = new FormControl();
_subscriptAnimationState: string = 'enter';
users: IdentityUserModel[] = [];
clientId: string;
dataError = false;
isFocused: boolean;
currentUser: IdentityUserModel;
constructor(private identityUserService: IdentityUserService) { }
constructor(private identityUserService: IdentityUserService) {
this.selectedUsers = new BehaviorSubject<IdentityUserModel[]>(this._selectedUsers);
this.searchUsers = new BehaviorSubject<IdentityUserModel[]>(this._searchUsers);
this.selectedUsers$ = this.selectedUsers.asObservable();
this.searchUsers$ = this.searchUsers.asObservable();
}
ngOnInit() {
this.loadUsers();
if (this.hasPreSelectUsers()) {
this.loadPreSelectUsers();
}
this.initSearch();
if (this.appName) {
this.disableSearch();
this.loadClientId();
}
}
initSearch() {
this.searchUser.valueChanges.subscribe((keyword) => {
this.users$ = this.searchUsers(keyword);
});
}
private async loadUsers() {
const roles = [PeopleCloudComponent.ROLE_ACTIVITI_ADMIN, PeopleCloudComponent.ROLE_ACTIVITI_MODELER, PeopleCloudComponent.ROLE_ACTIVITI_USER];
if (this.showCurrentUser) {
this.users = await this.identityUserService.getUsersByRolesWithCurrentUser(roles);
private initSearch() {
this.searchUserCtrl.valueChanges.pipe(
filter((value) => {
return typeof value === 'string';
}),
tap((value) => {
if (value) {
this.setError();
} else {
this.users = await this.identityUserService.getUsersByRolesWithoutCurrentUser(roles);
this.clearError();
}
}),
debounceTime(500),
distinctUntilChanged(),
tap(() => {
this.resetSearchUsers();
}),
switchMap((search) => this.identityUserService.findUsersByName(search)),
mergeMap((users) => {
return users;
}),
filter((user: any) => {
return !this.isUserAlreadySelected(user);
}),
mergeMap((user: any) => {
if (this.appName) {
return this.checkUserHasAccess(user.id).pipe(
mergeMap((hasRole) => {
return hasRole ? of(user) : of();
})
);
} else {
return of(user);
}
private searchUsers(keyword: string): Observable<IdentityUserModel[]> {
const filteredUsers = this.users.filter((user) => {
return user.username.toLowerCase().indexOf(keyword.toString().toLowerCase()) !== -1;
})
).subscribe((user) => {
this._searchUsers.push(user);
this.searchUsers.next(this._searchUsers);
});
this.dataError = filteredUsers.length === 0;
return of(filteredUsers);
}
onSelect(selectedUser: IdentityUserModel) {
this.selectedUser.emit(selectedUser);
this.dataError = false;
private checkUserHasAccess(userId: string): Observable<boolean> {
if (this.hasRoles()) {
return this.identityUserService.checkUserHasAnyClientAppRole(userId, this.clientId, this.roles);
} else {
return this.identityUserService.checkUserHasClientApp(userId, this.clientId);
}
}
private hasRoles(): boolean {
return this.roles && this.roles.length > 0;
}
private isUserAlreadySelected(user: IdentityUserModel): boolean {
if (this._selectedUsers && this._selectedUsers.length > 0) {
const result = this._selectedUsers.find((selectedUser) => {
return selectedUser.id === user.id;
});
return !!result;
}
return false;
}
private loadPreSelectUsers() {
if (this.isMultipleMode()) {
if (this.preSelectUsers && this.preSelectUsers.length > 0) {
this.selectedUsers.next(this.preSelectUsers);
}
} else {
this.selectedUsers.next(this.preSelectUsers);
this.searchUserCtrl.setValue(this.preSelectUsers[0]);
}
}
private async loadClientId() {
this.clientId = await this.identityUserService.getClientIdByApplicationName(this.appName).toPromise();
if (this.clientId) {
this.enableSearch();
}
}
onSelect(user: IdentityUserModel) {
if (this.isMultipleMode()) {
if (!this.isUserAlreadySelected(user)) {
this._selectedUsers.push(user);
this.selectedUsers.next(this._selectedUsers);
this.selectUser.emit(user);
}
this.userInput.nativeElement.value = '';
this.searchUserCtrl.setValue('');
} else {
this.selectUser.emit(user);
}
this.clearError();
this.resetSearchUsers();
}
onRemove(user: IdentityUserModel) {
this.removeUser.emit(user);
const indexToRemove = this._selectedUsers.findIndex((selectedUser) => { return selectedUser.id === user.id; });
this._selectedUsers.splice(indexToRemove, 1);
this.selectedUsers.next(this._selectedUsers);
}
getDisplayName(user): string {
return FullNamePipe.prototype.transform(user);
}
isMultipleMode(): boolean {
return this.mode === PeopleCloudComponent.MODE_MULTIPLE;
}
private hasPreSelectUsers(): boolean {
return this.preSelectUsers && this.preSelectUsers.length > 0;
}
private resetSearchUsers() {
this._searchUsers = [];
this.searchUsers.next(this._searchUsers);
}
private setError() {
this.searchUserCtrl.setErrors({invalid: true});
}
private clearError() {
this.searchUserCtrl.setErrors(null);
}
setFocus(isFocused: boolean) {
this.isFocused = isFocused;
}
hasError(): boolean {
return !!this.searchUserCtrl.errors;
}
hasErrorMessage(): boolean {
return !this.isFocused && this.hasError();
}
private disableSearch() {
this.searchUserCtrl.disable();
}
private enableSearch() {
this.searchUserCtrl.enable();
}
}

View File

@@ -62,7 +62,7 @@
</mat-form-field>
<div fxFlex>
<adf-cloud-people (selectedUser)="onAssigneeSelect($event)"></adf-cloud-people>
<adf-cloud-people #peopleInput *ngIf="currentUser" [appName]="appName" [preSelectUsers]="[currentUser]" (selectUser)="onAssigneeSelect($event)"></adf-cloud-people>
</div>
</div>
</mat-card-content>
@@ -78,7 +78,7 @@
</button>
<button
color="primary"
type="submit" [disabled]="dateError || !taskForm.valid || submitted"
type="submit" [disabled]="dateError || !taskForm.valid || submitted || assignee.hasError()"
mat-button
id="button-start">
{{'ADF_CLOUD_TASK_LIST.START_TASK.FORM.ACTION.START'|translate}}

View File

@@ -32,7 +32,6 @@ import { taskDetailsMock } from '../mock/task-details.mock';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { ProcessServiceCloudTestingModule } from './../../../testing/process-service-cloud.testing.module';
import { StartTaskCloudTestingModule } from '../testing/start-task-cloud.testing.module';
import { mockRoles, mockUsers } from '../mock/user-cloud.mock';
import { TaskDetailsCloudModel } from '../models/task-details-cloud.model';
describe('StartTaskCloudComponent', () => {
@@ -43,8 +42,6 @@ describe('StartTaskCloudComponent', () => {
let identityService: IdentityUserService;
let element: HTMLElement;
let createNewTaskSpy: jasmine.Spy;
let getRolesByUserIdSpy: jasmine.Spy;
let getUserSpy: jasmine.Spy;
setupTestBed({
imports: [ProcessServiceCloudTestingModule, StartTaskCloudTestingModule],
@@ -60,9 +57,7 @@ describe('StartTaskCloudComponent', () => {
service = TestBed.get(StartTaskCloudService);
identityService = TestBed.get(IdentityUserService);
createNewTaskSpy = spyOn(service, 'createNewTask').and.returnValue(of(taskDetailsMock));
getRolesByUserIdSpy = spyOn(identityService, 'getUserRoles').and.returnValue(of(mockRoles));
getUserSpy = spyOn(identityService, 'getUsers').and.returnValue(of(mockUsers));
spyOn(identityService, 'getCurrentUserInfo').and.returnValue(new IdentityUserModel({username: 'currentUser'}));
spyOn(identityService, 'getCurrentUserInfo').and.returnValue(new IdentityUserModel({username: 'currentUser', firstName: 'Test', lastName: 'User'}));
fixture.detectChanges();
}));
@@ -70,27 +65,8 @@ describe('StartTaskCloudComponent', () => {
expect(component instanceof StartTaskCloudComponent).toBe(true, 'should create StartTaskCloudComponent');
});
it('should defined adf-cloud-people and fetch users ', () => {
component.ngOnInit();
fixture.detectChanges();
const peopleElement = fixture.debugElement.nativeElement.querySelector('adf-cloud-people');
expect(peopleElement).toBeDefined();
expect(getRolesByUserIdSpy).toHaveBeenCalled();
expect(getUserSpy).toHaveBeenCalled();
});
describe('create task', () => {
beforeEach(() => {
createNewTaskSpy.and.returnValue(of(
{
id: 91,
name: 'fakeName',
assignee: 'fake-assignee'
}
));
});
it('should create new task when start button is clicked', async(() => {
let successSpy = spyOn(component.success, 'emit');
component.taskForm.controls['name'].setValue('fakeName');
@@ -113,11 +89,7 @@ describe('StartTaskCloudComponent', () => {
createTaskButton.click();
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(successSpy).toHaveBeenCalledWith({
id: 91,
name: 'fakeName',
assignee: 'fake-assignee'
});
expect(successSpy).toHaveBeenCalledWith(taskDetailsMock);
});
}));
@@ -143,20 +115,18 @@ describe('StartTaskCloudComponent', () => {
expect(successSpy).not.toHaveBeenCalled();
});
it('should assign task when an assignee is selected', async(() => {
let successSpy = spyOn(component.success, 'emit');
it('should assign task to the logged in user when invalid assignee is selected', async(() => {
component.taskForm.controls['name'].setValue('fakeName');
component.assigneeName = 'fake-assignee';
fixture.detectChanges();
let createTaskButton = <HTMLElement> element.querySelector('#button-start');
const assigneeInput = <HTMLElement> element.querySelector('input.adf-cloud-input');
assigneeInput.nodeValue = 'a';
fixture.detectChanges();
const createTaskButton = <HTMLElement> element.querySelector('#button-start');
createTaskButton.click();
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(successSpy).toHaveBeenCalledWith({
id: 91,
name: 'fakeName',
assignee: 'fake-assignee'
});
const taskRequest = new TaskDetailsCloudModel({ name: 'fakeName', assignee: 'currentUser'});
expect(createNewTaskSpy).toHaveBeenCalledWith(taskRequest);
});
}));
@@ -173,12 +143,18 @@ describe('StartTaskCloudComponent', () => {
}));
});
it('should select logged in user as assignee by default', () => {
fixture.detectChanges();
const assignee = fixture.nativeElement.querySelector('[data-automation-id="adf-people-cloud-search-input"]');
expect(assignee.value).toBe('Test User');
});
it('should show start task button', () => {
component.taskForm.controls['name'].setValue('fakeName');
fixture.detectChanges();
expect(element.querySelector('#button-start')).toBeDefined();
expect(element.querySelector('#button-start')).not.toBeNull();
expect(element.querySelector('#button-start').textContent).toContain('ADF_CLOUD_TASK_LIST.START_TASK.FORM.ACTION.START');
const startButton = element.querySelector('#button-start');
expect(startButton).toBeDefined();
expect(startButton.textContent).toContain('ADF_CLOUD_TASK_LIST.START_TASK.FORM.ACTION.START');
});
it('should disable start button if name is empty', () => {

View File

@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation, OnDestroy } from '@angular/core';
import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation, OnDestroy, ViewChild } from '@angular/core';
import { DateAdapter, MAT_DATE_FORMATS } from '@angular/material/core';
import { MOMENT_DATE_FORMATS, MomentDateAdapter } from '@alfresco/adf-core';
import moment from 'moment-es6';
@@ -29,6 +29,7 @@ import {
IdentityUserService,
IdentityUserModel
} from '@alfresco/adf-core';
import { PeopleCloudComponent } from './people-cloud/people-cloud.component';
@Component({
selector: 'adf-cloud-start-task',
@@ -70,6 +71,9 @@ export class StartTaskCloudComponent implements OnInit, OnDestroy {
@Output()
error: EventEmitter<any> = new EventEmitter<any>();
@ViewChild('peopleInput')
assignee: PeopleCloudComponent;
users$: Observable<any[]>;
taskId: string;