[AAE-1351] [GroupCloudComponent] Validate pre-selected groups (#5353)

* * Added an input flag to validate preselected groups.
* Modified demo shell groups page.

* * Added unit tests to the recent changes.

* * Fixed comments
This commit is contained in:
siva kumar
2020-01-03 15:59:29 +05:30
committed by Maurizio Vitale
parent 12d068c228
commit 68df1ad440
5 changed files with 277 additions and 30 deletions

View File

@@ -308,7 +308,11 @@
"APP_NAME": "Application Name",
"APP_FILTER_MODE": "Filter by application name",
"ROLE_FILTER_MODE": "Filter by role",
"PRESELECT_VALIDATION": "Preselect validation"
"PRESELECT_VALIDATION": "Preselect validation",
"ALL_SELECTED_USERS": "All Selected Users",
"ALL_SELECTED_GROUPS": "All Selected Groups",
"INVALID_USERS": "Invalid Users",
"INVALID_GROUPS": "Invalid Groups"
},
"SETTINGS_CLOUD": {
"MULTISELECTION": "Multiselection",

View File

@@ -40,16 +40,29 @@
[appName]="peopleAppName"
[roles]="peopleRoles"
[title]="'ADF_TASK_LIST.START_TASK.FORM.LABEL.ASSIGNEE'"
[mode]="peopleMode"></adf-cloud-people>
[mode]="peopleMode"
(selectUser)="onSelectUser($event)"
(warning)="onUsersWarning($event)"></adf-cloud-people>
</div>
<div class="app-people-list" *ngIf="canShowPeopleList()">
<h4>{{ 'PEOPLE_GROUPS_CLOUD.ALL_SELECTED_USERS' | translate }}</h4>
<mat-list role="list">
<mat-list-item *ngFor="let item of preSelectUsers" role="listitem">
<mat-icon mat-list-icon>person</mat-icon>
{{ item?.firstName }} {{ item?.lastName }}
{{item | fullName}}
</mat-list-item>
</mat-list>
<div *ngIf="invalidUsers.length > 0">
<h4>{{ 'PEOPLE_GROUPS_CLOUD.INVALID_USERS' | translate }} <mat-icon>warning</mat-icon> </h4>
<mat-list role="list">
<mat-list-item *ngFor="let invalidUser of invalidUsers" role="listitem">
<mat-icon mat-list-icon>person</mat-icon>
{{invalidUser | fullName}}
</mat-list-item>
</mat-list>
</div>
</div>
</mat-card-content>
</mat-card>
@@ -86,19 +99,39 @@
<mat-label>Preselect: {{ DEFAULT_GROUP_PLACEHOLDER }}</mat-label>
<input matInput (input)="setGroupsPreselectValue($event)" data-automation-id="app-group-preselect-input" />
</mat-form-field>
<mat-checkbox class="app-group-preselect-validation" (change)="onChangeGroupValidation($event)">{{
'PEOPLE_GROUPS_CLOUD.PRESELECT_VALIDATION' | translate }}</mat-checkbox>
</div>
<div>
<adf-cloud-group [mode]="groupMode" [roles]="groupRoles" [appName]="groupAppName" [preSelectGroups]="preSelectGroup"
(selectGroup)="onSelectGroup($event)" (removeGroup)="onRemoveGroup($event)"></adf-cloud-group>
<adf-cloud-group
[mode]="groupMode"
[validate]="groupPreselectValidation"
[roles]="groupRoles"
[appName]="groupAppName"
[preSelectGroups]="preSelectGroup"
(selectGroup)="onSelectGroup($event)"
(removeGroup)="onRemoveGroup($event)"
(warning)="onGroupsWarning($event)"></adf-cloud-group>
</div>
<div class="app-group-list" *ngIf="canShowGroupList()">
<h4>{{ 'PEOPLE_GROUPS_CLOUD.ALL_SELECTED_GROUPS' | translate }}</h4>
<mat-list role="list">
<mat-list-item *ngFor="let item of preSelectGroup" role="listitem">
<mat-icon mat-list-icon>group</mat-icon>
{{ item.name }}
</mat-list-item>
</mat-list>
<div *ngIf="invalidGroups.length > 0">
<h4>{{ 'PEOPLE_GROUPS_CLOUD.INVALID_GROUPS' | translate }} <mat-icon>warning</mat-icon> </h4>
<mat-list role="list">
<mat-list-item *ngFor="let invalidGroup of invalidGroups" role="listitem">
<mat-icon mat-list-icon>group</mat-icon>
{{invalidGroup?.name}}
</mat-list-item>
</mat-list>
</div>
</div>
</mat-card-content>
</mat-card>

View File

@@ -18,7 +18,7 @@
import { Component, ViewEncapsulation } from '@angular/core';
import { PeopleCloudComponent, GroupCloudComponent } from '@alfresco/adf-process-services-cloud';
import { MatRadioChange, MatCheckboxChange } from '@angular/material';
import { IdentityGroupModel } from '@alfresco/adf-core';
import { IdentityGroupModel, IdentityUserModel } from '@alfresco/adf-core';
@Component({
selector: 'app-people-groups-cloud',
@@ -33,14 +33,17 @@ export class PeopleGroupCloudDemoComponent {
DEFAULT_PEOPLE_PLACEHOLDER: string = `[{"id": "1", email": "user@user.com", "firstName":"user", "lastName": "lastName", "username": "user"}]`;
peopleMode: string = PeopleCloudComponent.MODE_SINGLE;
preSelectUsers: string[] = [];
preSelectUsers: IdentityUserModel[] = [];
invalidUsers: IdentityGroupModel[] = [];
peopleRoles: string[] = [];
peopleAppName: string;
peopleFilterMode: string = this.DEFAULT_FILTER_MODE;
peoplePreselectValidation: Boolean = false;
groupPreselectValidation = false;
groupMode: string = GroupCloudComponent.MODE_SINGLE;
preSelectGroup: IdentityGroupModel[] = [];
invalidGroups: IdentityGroupModel[] = [];
selectedGroupList: IdentityGroupModel[] = [];
groupRoles: string[];
groupAppName: string;
@@ -117,6 +120,25 @@ export class PeopleGroupCloudDemoComponent {
onChangePeopleValidation(event: MatCheckboxChange) {
this.peoplePreselectValidation = event.checked;
this.preSelectUsers = [...this.preSelectUsers];
if (!this.peoplePreselectValidation) {
this.invalidUsers = [];
}
}
onChangeGroupValidation(event: MatCheckboxChange) {
this.groupPreselectValidation = event.checked;
this.preSelectGroup = [...this.preSelectGroup];
if (!this.groupPreselectValidation) {
this.invalidGroups = [];
}
}
onGroupsWarning(warning: any) {
this.invalidGroups = warning.groups;
}
onUsersWarning(warning: any) {
this.invalidUsers = warning.users;
}
isStringArray(str: string) {
@@ -149,6 +171,12 @@ export class PeopleGroupCloudDemoComponent {
this.preSelectGroup = this.preSelectGroup.filter((value: any) => value.id !== group.id);
}
onSelectUser(user: IdentityUserModel) {
if (this.peopleMode === PeopleCloudComponent.MODE_MULTIPLE) {
this.preSelectUsers.push(user);
}
}
onSelectGroup(group: IdentityGroupModel) {
if (this.groupMode === GroupCloudComponent.MODE_MULTIPLE) {
this.preSelectGroup.push(group);

View File

@@ -324,8 +324,8 @@ describe('GroupCloudComponent', () => {
component.preSelectGroups = [];
fixture.detectChanges();
fixture.whenStable().then(() => {
const selectedUser = component.searchGroupsControl.value;
expect(selectedUser).toBeNull();
const selectedGroup = component.searchGroupsControl.value;
expect(selectedGroup).toBeNull();
done();
});
});
@@ -355,7 +355,7 @@ describe('GroupCloudComponent', () => {
it('should pre-select all preSelectGroups when mode=multiple', (done) => {
component.mode = 'multiple';
fixture.detectChanges();
component.ngOnChanges({ 'preSelectUsers': change });
component.ngOnChanges({ 'preSelectGroups': change });
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
@@ -407,7 +407,7 @@ describe('GroupCloudComponent', () => {
});
});
it('should not filter groups if user does not have any specified role', (done) => {
it('should not filter groups if group does not have any specified role', (done) => {
fixture.detectChanges();
checkGroupHasRoleSpy.and.returnValue(of(false));
const inputHTMLElement: HTMLInputElement = <HTMLInputElement> element.querySelector('input');
@@ -498,4 +498,64 @@ describe('GroupCloudComponent', () => {
});
});
});
describe('Multiple Mode and Pre-selected groups with validate flag', () => {
beforeEach(async(() => {
component.mode = 'multiple';
component.validate = true;
component.preSelectGroups = <any> mockIdentityGroups;
element = fixture.nativeElement;
alfrescoApiService = TestBed.get(AlfrescoApiService);
fixture.detectChanges();
}));
it('should emit warning if are invalid groups', (done) => {
findGroupsByNameSpy.and.returnValue(Promise.resolve([]));
const warnMessage = { message: 'INVALID_PRESELECTED_GROUPS', groups: [{ name: 'invalidGroupOne' }, { name: 'invalidGroupTwo' }] };
component.validate = true;
component.preSelectGroups = <any> [{ name: 'invalidGroupOne' }, { name: 'invalidGroupTwo' }];
fixture.detectChanges();
component.loadSinglePreselectGroup();
component.warning.subscribe((response) => {
expect(response).toEqual(warnMessage);
expect(response.message).toEqual(warnMessage.message);
expect(response.groups).toEqual(warnMessage.groups);
expect(response.groups[0].name).toEqual('invalidGroupOne');
done();
});
});
it('should filter group by name if validate true', (done) => {
findGroupsByNameSpy.and.returnValue(of(mockIdentityGroups));
component.mode = 'multiple';
component.validate = true;
component.preSelectGroups = <any> [{ name: mockIdentityGroups[1].name }, { name: mockIdentityGroups[2].name }];
fixture.detectChanges();
fixture.whenStable().then(() => {
component.filterPreselectGroups().then((result) => {
expect(findGroupsByNameSpy).toHaveBeenCalled();
expect(component.groupExists(result[0])).toEqual(true);
expect(component.groupExists(result[1])).toEqual(true);
done();
});
});
});
it('should not preselect any group if name is invalid and validation enable', (done) => {
findGroupsByNameSpy.and.returnValue(of([]));
component.mode = 'single';
component.validate = true;
component.preSelectGroups = <any> [{ name: 'invalid group' }];
fixture.detectChanges();
fixture.whenStable().then(() => {
component.validatePreselectGroups().then((result) => {
fixture.detectChanges();
expect(findGroupsByNameSpy).toHaveBeenCalled();
expect(result.length).toEqual(0);
done();
});
});
});
});
});

View File

@@ -27,14 +27,15 @@ import {
SimpleChanges,
OnChanges,
OnDestroy,
ChangeDetectionStrategy
ChangeDetectionStrategy,
SimpleChange
} from '@angular/core';
import { FormControl } from '@angular/forms';
import { trigger, state, style, transition, animate } from '@angular/animations';
import { Observable, of, BehaviorSubject, Subject } from 'rxjs';
import { debounceTime } from 'rxjs/internal/operators/debounceTime';
import { distinctUntilChanged, switchMap, mergeMap, filter, tap, map, takeUntil } from 'rxjs/operators';
import { IdentityGroupModel, IdentityGroupSearchParam, IdentityGroupService } from '@alfresco/adf-core';
import { IdentityGroupModel, IdentityGroupSearchParam, IdentityGroupService, LogService } from '@alfresco/adf-core';
@Component({
selector: 'adf-cloud-group',
@@ -57,7 +58,7 @@ export class GroupCloudComponent implements OnInit, OnChanges, OnDestroy {
static MODE_SINGLE = 'single';
static MODE_MULTIPLE = 'multiple';
/** Name of the application. If specified this shows the users who have access to the app. */
/** Name of the application. If specified this shows the groups who have access to the app. */
@Input()
appName: string;
@@ -65,14 +66,21 @@ export class GroupCloudComponent implements OnInit, OnChanges, OnDestroy {
@Input()
title: string;
/** User selection mode (single/multiple). */
/** Group selection mode (single/multiple). */
@Input()
mode: string = GroupCloudComponent.MODE_SINGLE;
/** Array of users to be pre-selected. This pre-selects all users in multi selection mode and only the first user of the array in single selection mode. */
/** Array of groups to be pre-selected. This pre-selects all groups in multi selection mode and only the first group of the array in single selection mode. */
@Input()
preSelectGroups: IdentityGroupModel[] = [];
/** This flag enables the validation on the preSelectGroups passed as input.
* In case the flag is true the components call the identity service to verify the validity of the information passed as input.
* Otherwise, no check will be done.
*/
@Input()
validate: Boolean = false;
/** Show the info in readonly mode
*/
@Input()
@@ -98,6 +106,10 @@ export class GroupCloudComponent implements OnInit, OnChanges, OnDestroy {
@Output()
changedGroups = new EventEmitter<IdentityGroupModel[]>();
/** Emitted when an warning occurs. */
@Output()
warning = new EventEmitter<any>();
@ViewChild('groupInput')
private groupInput: ElementRef<HTMLInputElement>;
@@ -121,8 +133,12 @@ export class GroupCloudComponent implements OnInit, OnChanges, OnDestroy {
private onDestroy$ = new Subject<boolean>();
currentTimeout: any;
invalidGroups: IdentityGroupModel[] = [];
constructor(private identityGroupService: IdentityGroupService) { }
constructor(
private identityGroupService: IdentityGroupService,
private logService: LogService
) { }
ngOnInit() {
this.initSearch();
@@ -130,13 +146,15 @@ export class GroupCloudComponent implements OnInit, OnChanges, OnDestroy {
ngOnChanges(changes: SimpleChanges) {
if (changes.preSelectGroups && this.hasPreSelectGroups()) {
if (this.isPreselectedGroupsChanged(changes)) {
if (this.isValidationEnabled()) {
this.loadPreSelectGroups();
} else {
this.searchGroupsControl.setValue('');
this.loadNoValidationPreselectGroups();
}
}
if (changes.appName && this.isAppNameChanged(changes.appName)) {
if (this.isAppNameChanged(changes.appName)) {
this.disableSearch();
this.loadClientId();
} else {
@@ -144,8 +162,14 @@ export class GroupCloudComponent implements OnInit, OnChanges, OnDestroy {
}
}
private isAppNameChanged(change): boolean {
return change.previousValue !== change.currentValue && this.appName && this.appName.length > 0;
private isPreselectedGroupsChanged(changes: SimpleChanges): boolean {
return changes.preSelectGroups
&& changes.preSelectGroups.previousValue !== changes.preSelectGroups.currentValue
&& this.hasPreSelectGroups();
}
private isAppNameChanged(change: SimpleChange): boolean {
return change && change.previousValue !== change.currentValue && this.appName && this.appName.length > 0;
}
private async loadClientId() {
@@ -220,20 +244,114 @@ export class GroupCloudComponent implements OnInit, OnChanges, OnDestroy {
return false;
}
private loadPreSelectGroups() {
if (this.isMultipleMode()) {
const groups = this.removeDuplicatedGroups([...this.preSelectGroups]);
async searchGroup(groupName: any): Promise<IdentityGroupModel> {
return (await this.identityGroupService.findGroupsByName(this.createSearchParam(groupName)).toPromise())[0];
}
async filterPreselectGroups() {
const promiseBatch = this.preSelectGroups.map(async (group: IdentityGroupModel) => {
let result: any;
try {
result = await this.searchGroup(group.name);
} catch (error) {
result = [];
this.logService.error(error);
}
const isGroupValid: boolean = this.groupExists(result);
return isGroupValid ? result : null;
});
return Promise.all(promiseBatch);
}
public groupExists(result: IdentityGroupModel): boolean {
return result
&& (result.id !== undefined
|| result.name !== undefined);
}
private isValidGroup(filteredGroups: IdentityGroupModel[], group: IdentityGroupModel): IdentityGroupModel {
return filteredGroups.find((filteredGroup: IdentityGroupModel) => {
return filteredGroup &&
(filteredGroup.id === group.id ||
filteredGroup.name === group.name);
});
}
async validatePreselectGroups(): Promise<IdentityGroupModel[]> {
let filteredPreselectGroups: IdentityGroupModel[];
let validGroups: IdentityGroupModel[] = [];
try {
filteredPreselectGroups = await this.filterPreselectGroups();
} catch (error) {
validGroups = [];
this.logService.error(error);
}
await this.preSelectGroups.map((group: IdentityGroupModel) => {
const validGroup = this.isValidGroup(filteredPreselectGroups, group);
if (validGroup) {
validGroups.push(validGroup);
} else {
this.invalidGroups.push(group);
}
});
validGroups = this.removeDuplicatedGroups(validGroups);
return validGroups;
}
public async loadSinglePreselectGroup() {
const groups = await this.validatePreselectGroups();
if (groups && groups.length > 0) {
this.checkPreselectValidationErrors();
this.searchGroupsControl.setValue(groups[0]);
} else {
this.checkPreselectValidationErrors();
}
}
public async loadMultiplePreselectGroups() {
const groups = await this.validatePreselectGroups();
if (groups && groups.length > 0) {
this.checkPreselectValidationErrors();
this.selectedGroups = [...groups];
this.selectedGroups$.next(this.selectedGroups);
} else {
this.checkPreselectValidationErrors();
}
}
public checkPreselectValidationErrors() {
if (this.invalidGroups.length > 0) {
this.warning.emit({
message: 'INVALID_PRESELECTED_GROUPS',
groups: this.invalidGroups
});
}
}
private loadPreSelectGroups() {
if (!this.isMultipleMode()) {
this.loadSinglePreselectGroup();
} else {
this.loadMultiplePreselectGroups();
}
}
loadNoValidationPreselectGroups() {
this.selectedGroups = [...this.removeDuplicatedGroups([...this.preSelectGroups])];
if (this.isMultipleMode()) {
this.selectedGroups$.next(this.selectedGroups);
} else {
if (this.currentTimeout) {
clearTimeout(this.currentTimeout);
}
this.currentTimeout = setTimeout(() => {
this.searchGroupsControl.setValue(this.preSelectGroups[0]);
this.onSelect(this.preSelectGroups[0]);
this.searchGroupsControl.setValue(this.selectedGroups[0]);
this.onSelect(this.selectedGroups[0]);
}, 0);
}
}
@@ -290,7 +408,7 @@ export class GroupCloudComponent implements OnInit, OnChanges, OnDestroy {
private removeDuplicatedGroups(groups: IdentityGroupModel[]): IdentityGroupModel[] {
return groups.filter((group, index, self) =>
index === self.findIndex((auxGroup) => {
return group.id === auxGroup.id;
return group.id === auxGroup.id && group.name === auxGroup.name;
}));
}
@@ -333,6 +451,10 @@ export class GroupCloudComponent implements OnInit, OnChanges, OnDestroy {
this.isFocused = isFocused;
}
isValidationEnabled() {
return this.validate === true;
}
hasErrorMessage(): boolean {
return !this.isFocused && this.hasError();
}