mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2025-07-24 17:32:15 +00:00
[AAE-8061] Add groups restriction to people widget (#7586)
* [AAE-8061] add groups restriction to people widget * Trigger travis * [AAE-8061] fix lint * [AAE-8061] fix unit tests
This commit is contained in:
@@ -75,6 +75,7 @@ export class FormFieldModel extends FormWidgetModel {
|
|||||||
selectionType: 'single' | 'multiple' = null;
|
selectionType: 'single' | 'multiple' = null;
|
||||||
rule?: FormFieldRule;
|
rule?: FormFieldRule;
|
||||||
selectLoggedUser: boolean;
|
selectLoggedUser: boolean;
|
||||||
|
groupsRestriction: string[];
|
||||||
|
|
||||||
// container model members
|
// container model members
|
||||||
numberOfColumns: number = 1;
|
numberOfColumns: number = 1;
|
||||||
@@ -183,6 +184,7 @@ export class FormFieldModel extends FormWidgetModel {
|
|||||||
this.selectionType = json.selectionType;
|
this.selectionType = json.selectionType;
|
||||||
this.rule = json.rule;
|
this.rule = json.rule;
|
||||||
this.selectLoggedUser = json.selectLoggedUser;
|
this.selectLoggedUser = json.selectLoggedUser;
|
||||||
|
this.groupsRestriction = json.groupsRestriction?.groups;
|
||||||
|
|
||||||
if (json.placeholder && json.placeholder !== '' && json.placeholder !== 'null') {
|
if (json.placeholder && json.placeholder !== '' && json.placeholder !== 'null') {
|
||||||
this.placeholder = json.placeholder;
|
this.placeholder = json.placeholder;
|
||||||
|
@@ -12,6 +12,7 @@
|
|||||||
(changedUsers)="onChangedUser($event)"
|
(changedUsers)="onChangedUser($event)"
|
||||||
[roles]="roles"
|
[roles]="roles"
|
||||||
[mode]="mode"
|
[mode]="mode"
|
||||||
|
[groupsRestriction]="groupsRestriction"
|
||||||
(blur)="markAsTouched()"
|
(blur)="markAsTouched()"
|
||||||
[matTooltip]="field.tooltip"
|
[matTooltip]="field.tooltip"
|
||||||
matTooltipPosition="above"
|
matTooltipPosition="above"
|
||||||
|
@@ -51,6 +51,7 @@ export class PeopleCloudWidgetComponent extends WidgetComponent implements OnIni
|
|||||||
title: string;
|
title: string;
|
||||||
preSelectUsers: IdentityUserModel[];
|
preSelectUsers: IdentityUserModel[];
|
||||||
search: FormControl;
|
search: FormControl;
|
||||||
|
groupsRestriction: string[];
|
||||||
|
|
||||||
constructor(formService: FormService, private identityUserService: IdentityUserService) {
|
constructor(formService: FormService, private identityUserService: IdentityUserService) {
|
||||||
super(formService);
|
super(formService);
|
||||||
@@ -62,6 +63,7 @@ export class PeopleCloudWidgetComponent extends WidgetComponent implements OnIni
|
|||||||
this.mode = this.field.optionType as ComponentSelectionMode;
|
this.mode = this.field.optionType as ComponentSelectionMode;
|
||||||
this.title = this.field.placeholder;
|
this.title = this.field.placeholder;
|
||||||
this.preSelectUsers = this.field.value ? this.field.value : [];
|
this.preSelectUsers = this.field.value ? this.field.value : [];
|
||||||
|
this.groupsRestriction = this.field.groupsRestriction;
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||||
this.search = new FormControl({value: '', disabled: this.field.readOnly}, []),
|
this.search = new FormControl({value: '', disabled: this.field.readOnly}, []),
|
||||||
|
@@ -26,7 +26,7 @@ import {
|
|||||||
} from '@alfresco/adf-core';
|
} from '@alfresco/adf-core';
|
||||||
import { ProcessServiceCloudTestingModule } from '../../testing/process-service-cloud.testing.module';
|
import { ProcessServiceCloudTestingModule } from '../../testing/process-service-cloud.testing.module';
|
||||||
import { of } from 'rxjs';
|
import { of } from 'rxjs';
|
||||||
import { mockUsers } from '../mock/user-cloud.mock';
|
import { mockInvolvedGroups, mockOAuth2, mockPreselectedUsers, mockUsers } from '../mock/user-cloud.mock';
|
||||||
import { PeopleCloudModule } from '../people-cloud.module';
|
import { PeopleCloudModule } from '../people-cloud.module';
|
||||||
import { SimpleChange } from '@angular/core';
|
import { SimpleChange } from '@angular/core';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
@@ -39,25 +39,21 @@ describe('PeopleCloudComponent', () => {
|
|||||||
let identityService: IdentityUserService;
|
let identityService: IdentityUserService;
|
||||||
let alfrescoApiService: AlfrescoApiService;
|
let alfrescoApiService: AlfrescoApiService;
|
||||||
let findUsersByNameSpy: jasmine.Spy;
|
let findUsersByNameSpy: jasmine.Spy;
|
||||||
|
let getInvolvedGroupsSpy: jasmine.Spy;
|
||||||
const mock: any = {
|
|
||||||
oauth2Auth: {
|
|
||||||
callCustomApi: () => Promise.resolve(mockUsers)
|
|
||||||
},
|
|
||||||
isEcmLoggedIn: () => false,
|
|
||||||
reply: jasmine.createSpy('reply')
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockPreselectedUsers = [
|
|
||||||
{ id: mockUsers[1].id, username: mockUsers[1].username },
|
|
||||||
{ id: mockUsers[2].id, username: mockUsers[2].username }
|
|
||||||
];
|
|
||||||
|
|
||||||
// eslint-disable-next-line prefer-arrow/prefer-arrow-functions
|
// eslint-disable-next-line prefer-arrow/prefer-arrow-functions
|
||||||
function getElement<T = HTMLElement>(selector: string): T {
|
function getElement<T = HTMLElement>(selector: string): T {
|
||||||
return fixture.nativeElement.querySelector(selector);
|
return fixture.nativeElement.querySelector(selector);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const typeInputValue = (value: string) => {
|
||||||
|
const input = getElement<HTMLInputElement>('input');
|
||||||
|
input.focus();
|
||||||
|
input.value = value;
|
||||||
|
input.dispatchEvent(new Event('keyup'));
|
||||||
|
input.dispatchEvent(new Event('input'));
|
||||||
|
};
|
||||||
|
|
||||||
setupTestBed({
|
setupTestBed({
|
||||||
imports: [
|
imports: [
|
||||||
TranslateModule.forRoot(),
|
TranslateModule.forRoot(),
|
||||||
@@ -74,7 +70,7 @@ describe('PeopleCloudComponent', () => {
|
|||||||
identityService = TestBed.inject(IdentityUserService);
|
identityService = TestBed.inject(IdentityUserService);
|
||||||
alfrescoApiService = TestBed.inject(AlfrescoApiService);
|
alfrescoApiService = TestBed.inject(AlfrescoApiService);
|
||||||
|
|
||||||
spyOn(alfrescoApiService, 'getInstance').and.returnValue(mock);
|
spyOn(alfrescoApiService, 'getInstance').and.returnValue(mockOAuth2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should populate placeholder when title is present', async () => {
|
it('should populate placeholder when title is present', async () => {
|
||||||
@@ -760,7 +756,8 @@ describe('PeopleCloudComponent', () => {
|
|||||||
message: 'INVALID_PRESELECTED_USERS',
|
message: 'INVALID_PRESELECTED_USERS',
|
||||||
users: [{
|
users: [{
|
||||||
id: mockPreselectedUsers[0].id,
|
id: mockPreselectedUsers[0].id,
|
||||||
username: mockPreselectedUsers[0].username
|
username: mockPreselectedUsers[0].username,
|
||||||
|
readonly: mockPreselectedUsers[0].readonly
|
||||||
}]
|
}]
|
||||||
};
|
};
|
||||||
component.warning.subscribe(warning => {
|
component.warning.subscribe(warning => {
|
||||||
@@ -801,11 +798,13 @@ describe('PeopleCloudComponent', () => {
|
|||||||
users: [
|
users: [
|
||||||
{
|
{
|
||||||
id: mockPreselectedUsers[0].id,
|
id: mockPreselectedUsers[0].id,
|
||||||
username: mockPreselectedUsers[0].username
|
username: mockPreselectedUsers[0].username,
|
||||||
|
readonly: mockPreselectedUsers[0].readonly
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: mockPreselectedUsers[1].id,
|
id: mockPreselectedUsers[1].id,
|
||||||
username: mockPreselectedUsers[1].username
|
username: mockPreselectedUsers[1].username,
|
||||||
|
readonly: mockPreselectedUsers[1].readonly
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
@@ -824,6 +823,75 @@ describe('PeopleCloudComponent', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Groups restriction', () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture.detectChanges();
|
||||||
|
findUsersByNameSpy = spyOn(identityService, 'findUsersByName').and.returnValue(of(mockUsers));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Shoud display all users if groups restriction is empty', async () => {
|
||||||
|
getInvolvedGroupsSpy = spyOn(identityService, 'getInvolvedGroups').and.returnValue(of(mockInvolvedGroups));
|
||||||
|
component.groupsRestriction = [];
|
||||||
|
typeInputValue('M');
|
||||||
|
|
||||||
|
await fixture.whenStable();
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(getInvolvedGroupsSpy).toHaveBeenCalledTimes(0);
|
||||||
|
expect(fixture.debugElement.queryAll(By.css('[data-automation-id="adf-people-cloud-row"]')).length).toEqual(mockUsers.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should display users that belongs to restricted groups', async () => {
|
||||||
|
getInvolvedGroupsSpy = spyOn(identityService, 'getInvolvedGroups').and.returnValue(of(mockInvolvedGroups));
|
||||||
|
component.groupsRestriction = [mockInvolvedGroups[0].name, mockInvolvedGroups[1].name];
|
||||||
|
typeInputValue('M');
|
||||||
|
|
||||||
|
await fixture.whenStable();
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(getInvolvedGroupsSpy).toHaveBeenCalledTimes(3);
|
||||||
|
expect(fixture.debugElement.queryAll(By.css('[data-automation-id="adf-people-cloud-row"]')).length).toEqual(mockUsers.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should not display users that not belongs to restricted groups', async () => {
|
||||||
|
getInvolvedGroupsSpy = spyOn(identityService, 'getInvolvedGroups').and.returnValue(of([mockInvolvedGroups[0]]));
|
||||||
|
component.groupsRestriction = [mockInvolvedGroups[0].name, mockInvolvedGroups[1].name];
|
||||||
|
typeInputValue('M');
|
||||||
|
|
||||||
|
await fixture.whenStable();
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(getInvolvedGroupsSpy).toHaveBeenCalledTimes(3);
|
||||||
|
expect(fixture.debugElement.queryAll(By.css('[data-automation-id="adf-people-cloud-row"]')).length).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should mark as invalid preselected user if is not belongs to restricted groups', (done) => {
|
||||||
|
spyOn(identityService, 'findUserById').and.returnValue(of(mockPreselectedUsers[0]));
|
||||||
|
getInvolvedGroupsSpy = spyOn(identityService, 'getInvolvedGroups').and.returnValue(of([mockInvolvedGroups[0]]));
|
||||||
|
|
||||||
|
const expectedWarning = {
|
||||||
|
message: 'INVALID_PRESELECTED_USERS',
|
||||||
|
users: [{
|
||||||
|
id: mockPreselectedUsers[0].id,
|
||||||
|
username: mockPreselectedUsers[0].username,
|
||||||
|
readonly: mockPreselectedUsers[0].readonly
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
component.warning.subscribe(warning => {
|
||||||
|
expect(warning).toEqual(expectedWarning);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
component.groupsRestriction = [mockInvolvedGroups[0].name, mockInvolvedGroups[1].name];
|
||||||
|
component.mode = 'single';
|
||||||
|
component.validate = true;
|
||||||
|
component.preSelectUsers = [mockPreselectedUsers[0]];
|
||||||
|
component.ngOnChanges({
|
||||||
|
preSelectUsers: new SimpleChange(null, [mockPreselectedUsers[0]], false)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should removeDuplicateUsers return only unique users', () => {
|
it('should removeDuplicateUsers return only unique users', () => {
|
||||||
const duplicatedUsers = [{ id: mockUsers[0].id }, { id: mockUsers[0].id }];
|
const duplicatedUsers = [{ id: mockUsers[0].id }, { id: mockUsers[0].id }];
|
||||||
expect(component.removeDuplicatedUsers(duplicatedUsers)).toEqual([{ id: mockUsers[0].id }]);
|
expect(component.removeDuplicatedUsers(duplicatedUsers)).toEqual([{ id: mockUsers[0].id }]);
|
||||||
|
@@ -33,6 +33,7 @@ import { Observable, of, BehaviorSubject, Subject } from 'rxjs';
|
|||||||
import { switchMap, debounceTime, distinctUntilChanged, mergeMap, tap, filter, map, takeUntil } from 'rxjs/operators';
|
import { switchMap, debounceTime, distinctUntilChanged, mergeMap, tap, filter, map, takeUntil } from 'rxjs/operators';
|
||||||
import {
|
import {
|
||||||
FullNamePipe,
|
FullNamePipe,
|
||||||
|
IdentityGroupModel,
|
||||||
IdentityUserModel,
|
IdentityUserModel,
|
||||||
IdentityUserService,
|
IdentityUserService,
|
||||||
LogService
|
LogService
|
||||||
@@ -103,6 +104,12 @@ export class PeopleCloudComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
@Input()
|
@Input()
|
||||||
excludedUsers: IdentityUserModel[] = [];
|
excludedUsers: IdentityUserModel[] = [];
|
||||||
|
|
||||||
|
/** Array of groups to restrict user searches.
|
||||||
|
* Mandatory property is group id
|
||||||
|
*/
|
||||||
|
@Input()
|
||||||
|
groupsRestriction: string[] = [];
|
||||||
|
|
||||||
/** FormControl to list of users */
|
/** FormControl to list of users */
|
||||||
@Input()
|
@Input()
|
||||||
userChipsCtrl: FormControl = new FormControl({ value: '', disabled: false });
|
userChipsCtrl: FormControl = new FormControl({ value: '', disabled: false });
|
||||||
@@ -216,13 +223,15 @@ export class PeopleCloudComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
this.resetSearchUsers();
|
this.resetSearchUsers();
|
||||||
}),
|
}),
|
||||||
switchMap((search) =>
|
switchMap((search) =>
|
||||||
this.identityUserService.findUsersByName(search.trim())),
|
this.findUsers(search)
|
||||||
|
),
|
||||||
mergeMap((users) => {
|
mergeMap((users) => {
|
||||||
this.resetSearchUsers();
|
this.resetSearchUsers();
|
||||||
this.searchLoading = false;
|
this.searchLoading = false;
|
||||||
return users;
|
return users;
|
||||||
}),
|
}),
|
||||||
filter(user => !this.isUserAlreadySelected(user) && !this.isExcludedUser(user)),
|
filter(user => !this.isUserAlreadySelected(user) && !this.isExcludedUser(user)),
|
||||||
|
mergeMap(user => this.filterUsersByGroupsRestriction(user)),
|
||||||
mergeMap(user => {
|
mergeMap(user => {
|
||||||
if (this.appName) {
|
if (this.appName) {
|
||||||
return this.checkUserHasAccess(user.id).pipe(
|
return this.checkUserHasAccess(user.id).pipe(
|
||||||
@@ -275,6 +284,19 @@ export class PeopleCloudComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
map((filteredUser: { hasRole: boolean; user: IdentityUserModel }) => filteredUser.user));
|
map((filteredUser: { hasRole: boolean; user: IdentityUserModel }) => filteredUser.user));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private filterUsersByGroupsRestriction(user: IdentityUserModel): Observable<IdentityUserModel> {
|
||||||
|
if (this.groupsRestriction?.length) {
|
||||||
|
return this.isUserPartOfAllRestrictedGroups(user).pipe(
|
||||||
|
mergeMap(isPartOfAllGroups => isPartOfAllGroups ? of(user) : of())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return of(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
private findUsers(search: string): Observable<IdentityUserModel[]> {
|
||||||
|
return this.identityUserService.findUsersByName(search.trim());
|
||||||
|
}
|
||||||
|
|
||||||
private isUserAlreadySelected(searchUser: IdentityUserModel): boolean {
|
private isUserAlreadySelected(searchUser: IdentityUserModel): boolean {
|
||||||
if (this.selectedUsers && this.selectedUsers.length > 0) {
|
if (this.selectedUsers && this.selectedUsers.length > 0) {
|
||||||
const result = this.selectedUsers.find((selectedUser) => this.compare(selectedUser, searchUser));
|
const result = this.selectedUsers.find((selectedUser) => this.compare(selectedUser, searchUser));
|
||||||
@@ -325,7 +347,17 @@ export class PeopleCloudComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
|
|
||||||
if (this.compare(user, validationResult)) {
|
if (this.compare(user, validationResult)) {
|
||||||
validationResult.readonly = user.readonly;
|
validationResult.readonly = user.readonly;
|
||||||
validUsers.push(validationResult);
|
if (this.groupsRestriction?.length) {
|
||||||
|
const isUserPartOfAllRestrictedGroups = await this.isUserPartOfAllRestrictedGroups(validationResult).toPromise();
|
||||||
|
|
||||||
|
if (isUserPartOfAllRestrictedGroups) {
|
||||||
|
validUsers.push(user);
|
||||||
|
} else {
|
||||||
|
this.invalidUsers.push(user);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
validUsers.push(validationResult);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.invalidUsers.push(user);
|
this.invalidUsers.push(user);
|
||||||
}
|
}
|
||||||
@@ -529,6 +561,20 @@ export class PeopleCloudComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
this.searchUsers$.next(this._searchUsers);
|
this.searchUsers$.next(this._searchUsers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private isUserPartOfAllRestrictedGroups(user: IdentityUserModel): Observable<boolean> {
|
||||||
|
return this.getUserGroups(user.id).pipe(
|
||||||
|
map(userGroups => userGroups.filter(
|
||||||
|
restrictedGroup => userGroups.includes(restrictedGroup)
|
||||||
|
).length >= this.groupsRestriction.length)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getUserGroups(userId: string): Observable<IdentityGroupModel[]> {
|
||||||
|
return this.identityUserService.getInvolvedGroups(userId).pipe(
|
||||||
|
map(groups => groups.map(({id, name}) => ({id, name})))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
getSelectedUsers(): IdentityUserModel[] {
|
getSelectedUsers(): IdentityUserModel[] {
|
||||||
return this.selectedUsers;
|
return this.selectedUsers;
|
||||||
}
|
}
|
||||||
|
@@ -15,6 +15,8 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { IdentityGroupModel } from '@alfresco/adf-core';
|
||||||
|
|
||||||
export const mockUsers = [
|
export const mockUsers = [
|
||||||
{ id: 'fake-id-1', username: 'first-name-1 last-name-1', firstName: 'first-name-1', lastName: 'last-name-1', email: 'abc@xyz.com' },
|
{ id: 'fake-id-1', username: 'first-name-1 last-name-1', firstName: 'first-name-1', lastName: 'last-name-1', email: 'abc@xyz.com' },
|
||||||
{ id: 'fake-id-2', username: 'first-name-2 last-name-2', firstName: 'first-name-2', lastName: 'last-name-2', email: 'abcd@xyz.com'},
|
{ id: 'fake-id-2', username: 'first-name-2 last-name-2', firstName: 'first-name-2', lastName: 'last-name-2', email: 'abcd@xyz.com'},
|
||||||
@@ -32,3 +34,21 @@ export const mockRoles = [
|
|||||||
{ id: 'id-4', name: 'MOCK-ROLE-1' },
|
{ id: 'id-4', name: 'MOCK-ROLE-1' },
|
||||||
{ id: 'id-5', name: 'MOCK-ROLE-2'}
|
{ id: 'id-5', name: 'MOCK-ROLE-2'}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const mockOAuth2: any = {
|
||||||
|
oauth2Auth: {
|
||||||
|
callCustomApi: () => Promise.resolve(mockUsers)
|
||||||
|
},
|
||||||
|
isEcmLoggedIn: () => false,
|
||||||
|
reply: jasmine.createSpy('reply')
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mockPreselectedUsers = [
|
||||||
|
{ id: mockUsers[1].id, username: mockUsers[1].username, readonly: false },
|
||||||
|
{ id: mockUsers[2].id, username: mockUsers[2].username, readonly: false }
|
||||||
|
];
|
||||||
|
|
||||||
|
export const mockInvolvedGroups = [
|
||||||
|
{ id: 'mock-group-id-1', name: 'Mock Group 1', path: '/mock', subGroups: [] } as IdentityGroupModel,
|
||||||
|
{ id: 'mock-group-id-2', name: 'Mock Group 2', path: '', subGroups: [] } as IdentityGroupModel
|
||||||
|
];
|
||||||
|
Reference in New Issue
Block a user