From f08ad08d0f38e9bf912bdfdd4dad4484f0af2ed3 Mon Sep 17 00:00:00 2001 From: Deepak Paul Date: Mon, 14 Jan 2019 19:20:19 +0530 Subject: [PATCH] [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 --- .../people-cloud.component.md | 45 ++++ e2e/actions/APS-cloud/identity.ts | 3 +- .../services/identity-user.service.spec.ts | 48 ++++ .../services/identity-user.service.ts | 137 ++++++++++- .../people-cloud/people-cloud.component.html | 44 +++- .../people-cloud.component.spec.ts | 203 +++++++++++---- .../people-cloud/people-cloud.component.ts | 231 +++++++++++++++--- .../start-task-cloud.component.html | 4 +- .../start-task-cloud.component.spec.ts | 60 ++--- .../components/start-task-cloud.component.ts | 6 +- 10 files changed, 630 insertions(+), 151 deletions(-) create mode 100644 docs/process-services-cloud/people-cloud.component.md diff --git a/docs/process-services-cloud/people-cloud.component.md b/docs/process-services-cloud/people-cloud.component.md new file mode 100644 index 0000000000..278bbb7a44 --- /dev/null +++ b/docs/process-services-cloud/people-cloud.component.md @@ -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 + + +``` + +## 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)`` | Emitted when an error occurs. | diff --git a/e2e/actions/APS-cloud/identity.ts b/e2e/actions/APS-cloud/identity.ts index 0137f7f696..19a325c808 100644 --- a/e2e/actions/APS-cloud/identity.ts +++ b/e2e/actions/APS-cloud/identity.ts @@ -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) { diff --git a/lib/core/userinfo/services/identity-user.service.spec.ts b/lib/core/userinfo/services/identity-user.service.spec.ts index 93a9469176..a22ed5ff88 100644 --- a/lib/core/userinfo/services/identity-user.service.spec.ts +++ b/lib/core/userinfo/services/identity-user.service.spec.ts @@ -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(); + } + ); + }); }); diff --git a/lib/core/userinfo/services/identity-user.service.ts b/lib/core/userinfo/services/identity-user.service.ts index cbd277c678..954482d890 100644 --- a/lib/core/userinfo/services/identity-user.service.ts +++ b/lib/core/userinfo/services/identity-user.service.ts @@ -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(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(key: string): T { let value; @@ -70,9 +67,123 @@ export class IdentityUserService { return value; } + /** + * Find users based on search input. + */ + findUsersByName(search: string): Observable { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { 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 { 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 { 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 { 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`; + } + } diff --git a/lib/process-services-cloud/src/lib/task/start-task/components/people-cloud/people-cloud.component.html b/lib/process-services-cloud/src/lib/task/start-task/components/people-cloud/people-cloud.component.html index 5e19f4c5fa..b1845e2311 100644 --- a/lib/process-services-cloud/src/lib/task/start-task/components/people-cloud/people-cloud.component.html +++ b/lib/process-services-cloud/src/lib/task/start-task/components/people-cloud/people-cloud.component.html @@ -1,27 +1,49 @@
- {{'ADF_CLOUD_TASK_LIST.START_TASK.FORM.LABEL.ASSIGNEE' | translate}} - + {{'ADF_TASK_LIST.START_TASK.FORM.LABEL.ASSIGNEE' | translate}} + + + {{user | fullName}} + cancel + + + + + + + - +
{{user | fullName}}
-
+
-
+
{{ 'ADF_CLOUD_START_TASK.ERROR.MESSAGE' | translate }}
warning
diff --git a/lib/process-services-cloud/src/lib/task/start-task/components/people-cloud/people-cloud.component.spec.ts b/lib/process-services-cloud/src/lib/task/start-task/components/people-cloud/people-cloud.component.spec.ts index d99ce8282a..50baa578c6 100644 --- a/lib/process-services-cloud/src/lib/task/start-task/components/people-cloud/people-cloud.component.spec.ts +++ b/lib/process-services-cloud/src/lib/task/start-task/components/people-cloud/people-cloud.component.spec.ts @@ -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; 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( mockUsers); + component.searchUsers$ = of( mockUsers); fixture.detectChanges(); let inputHTMLElement: 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 = 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 = element.querySelector('input'); + const inputHTMLElement: 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 = [{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 = [{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 = [{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 = 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 = 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 = 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 = element.querySelector('input'); + inputHTMLElement.focus(); + inputHTMLElement.value = 'M'; + inputHTMLElement.dispatchEvent(new Event('input')); + fixture.detectChanges(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + expect(checkUserHasAccessSpy).not.toHaveBeenCalled(); + }); + })); + }); diff --git a/lib/process-services-cloud/src/lib/task/start-task/components/people-cloud/people-cloud.component.ts b/lib/process-services-cloud/src/lib/task/start-task/components/people-cloud/people-cloud.component.ts index eafe2e0355..ad46898fb9 100644 --- a/lib/process-services-cloud/src/lib/task/start-task/components/people-cloud/people-cloud.component.ts +++ b/lib/process-services-cloud/src/lib/task/start-task/components/people-cloud/people-cloud.component.ts @@ -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 = new EventEmitter(); + selectUser: EventEmitter = new EventEmitter(); + + /** Emitted when a selected user is removed in multi selection mode. */ + @Output() + removeUser: EventEmitter = new EventEmitter(); /** Emitted when an error occurs. */ @Output() error: EventEmitter = new EventEmitter(); - users$: Observable; + @ViewChild('userInput') + private userInput: ElementRef; - searchUser: FormControl = new FormControl(); + private _selectedUsers: IdentityUserModel[] = []; + private _searchUsers: IdentityUserModel[] = []; + private selectedUsers: BehaviorSubject; + private searchUsers: BehaviorSubject; + selectedUsers$: Observable; + searchUsers$: Observable; + + 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(this._selectedUsers); + this.searchUsers = new BehaviorSubject(this._searchUsers); + this.selectedUsers$ = this.selectedUsers.asObservable(); + this.searchUsers$ = this.searchUsers.asObservable(); + } ngOnInit() { - this.loadUsers(); + if (this.hasPreSelectUsers()) { + this.loadPreSelectUsers(); + } + this.initSearch(); - } - 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); - } else { - this.users = await this.identityUserService.getUsersByRolesWithoutCurrentUser(roles); + if (this.appName) { + this.disableSearch(); + this.loadClientId(); } } - private searchUsers(keyword: string): Observable { - const filteredUsers = this.users.filter((user) => { - return user.username.toLowerCase().indexOf(keyword.toString().toLowerCase()) !== -1; + private initSearch() { + this.searchUserCtrl.valueChanges.pipe( + filter((value) => { + return typeof value === 'string'; + }), + tap((value) => { + if (value) { + this.setError(); + } else { + 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); + } + }) + ).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 { + 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(); + } + } diff --git a/lib/process-services-cloud/src/lib/task/start-task/components/start-task-cloud.component.html b/lib/process-services-cloud/src/lib/task/start-task/components/start-task-cloud.component.html index 71b46d5eec..486347cccb 100644 --- a/lib/process-services-cloud/src/lib/task/start-task/components/start-task-cloud.component.html +++ b/lib/process-services-cloud/src/lib/task/start-task/components/start-task-cloud.component.html @@ -62,7 +62,7 @@
- +
@@ -78,7 +78,7 @@