[AAE-1372] Refactor People/Group cloud component (#5355)

* [AAE-1372] Fix read-only preselected users can be deleted

* [AAE-1372] Fix people/group cloud component readonly mode

* [AAE-1372] Refactor People/Group Cloud components

* [AAE-1372] Refactor People/Group Cloud components

* [AAE-1372] Clear invalid user in single mode after replacing with a valid user

* [AAE-1372] Add progress bar while validation loading. When user gets removed remove from validation

* [AAE-1372] Fix lint errors

* [AAE-1372] Fix single selection e2e

* [AAE-1372] Fix unit tests - people/group cloud components

* [AAE-1372] Fix e2e, set People/Group formControls invalid when has preselect errors

* [AAE-1372] Fix invalid form control bug
This commit is contained in:
arditdomi
2020-01-21 13:37:57 +00:00
committed by Maurizio Vitale
parent 91abe87ccc
commit 3c3aa7599a
17 changed files with 1005 additions and 987 deletions

View File

@@ -1,61 +1,71 @@
<div class="adf-cloud-group">
<mat-form-field>
<mat-label id="adf-group-cloud-title-id">{{ (title || 'ADF_CLOUD_GROUPS.SEARCH-GROUP') | translate }}</mat-label>
<mat-chip-list #groupChipList *ngIf="isMultipleMode()" [disabled]="isDisabled" data-automation-id="adf-cloud-group-chip-list" class="apa-group-chip-list">
<mat-chip
*ngFor="let group of selectedGroups$ | async"
[removable]="!(group.readonly)"
(removed)="onRemove(group)"
matTooltip="{{ (group.readonly ? 'ADF_CLOUD_GROUPS.MANDATORY' : '') | translate }}"
[attr.data-automation-id]="'adf-cloud-group-chip-' + group.name">
{{group.name}}
<mat-icon
*ngIf="!(group.readonly || readOnly)"
matChipRemove [attr.data-automation-id]="'adf-cloud-group-chip-remove-icon-' + group.name">
cancel
</mat-icon>
</mat-chip>
<input matInput
[formControl]="searchGroupsControl"
class="adf-group-input"
id="group-name"
[attr.disabled]="isDisabled"
data-automation-id="adf-cloud-group-search-input"
(focus)="setFocus(true)"
(blur)="setFocus(false)"
[matAutocomplete]="auto"
[matChipInputFor]="groupChipList" #groupInput>
</mat-chip-list>
<form>
<mat-form-field class="adf-cloud-group">
<mat-label
id="adf-group-cloud-title-id">{{ (title || 'ADF_CLOUD_GROUPS.SEARCH-GROUP') | translate }}</mat-label>
<mat-chip-list #groupChipList [disabled]="isReadonly() || isValidationLoading()" data-automation-id="adf-cloud-group-chip-list" class="apa-group-chip-list">
<mat-chip
*ngFor="let group of selectedGroups"
[removable]="!(group.readonly)"
[attr.data-automation-id]="'adf-cloud-group-chip-' + group.name"
(removed)="onRemove(group)"
matTooltip="{{ (group.readonly ? 'ADF_CLOUD_GROUPS.MANDATORY' : '') | translate }}">
{{group.name}}
<mat-icon
*ngIf="!(group.readonly || readOnly)"
matChipRemove [attr.data-automation-id]="'adf-cloud-group-chip-remove-icon-' + group.name">
cancel
</mat-icon>
</mat-chip>
<input matInput
[formControl]="searchGroupsControl"
[matAutocomplete]="auto"
[matChipInputFor]="groupChipList"
(focus)="setFocus(true)"
(blur)="setFocus(false)"
class="adf-group-input"
id="group-name"
data-automation-id="adf-cloud-group-search-input" #groupInput>
</mat-chip-list>
<input *ngIf="!isMultipleMode()"
matInput
class="adf-group-input"
data-automation-id="adf-cloud-group-search-input"
(focus)="setFocus(true)"
(blur)="setFocus(false)"
[formControl]="searchGroupsControl"
[matAutocomplete]="auto">
<mat-autocomplete
autoActiveFirstOption
#auto="matAutocomplete"
class="adf-cloud-group-list"
(optionSelected)="onSelect($event.option.value)"
[displayWith]="getDisplayName"
data-automation-id="adf-cloud-group-autocomplete">
<mat-option *ngFor="let group of searchGroups$ | async; let i = index" [value]="group"
[attr.data-automation-id]="'adf-cloud-group-chip-' + group.name">
<div class="adf-cloud-group-row" id="adf-group-{{i}}" fxLayout="row" fxLayoutAlign="start center"
fxLayoutGap="20px">
<button class="adf-group-short-name" mat-fab>{{group | groupNameInitial }}</button>
<span>{{group.name}}</span>
</div>
</mat-option>
</mat-autocomplete>
</mat-form-field>
<mat-progress-bar
*ngIf="isLoading"
mode="indeterminate">
</mat-progress-bar>
<mat-autocomplete
#auto="matAutocomplete"
class="adf-cloud-group-list"
(optionSelected)="onSelect($event.option.value)"
[displayWith]="getDisplayName"
data-automation-id="adf-cloud-group-autocomplete">
<mat-option *ngFor="let group of searchGroups$ | async; let i = index" [value]="group" [attr.data-automation-id]="'adf-cloud-group-chip-' + group.name">
<div class="adf-cloud-group-row" id="adf-group-{{i}}" fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="20px">
<button class="adf-group-short-name" mat-fab>{{group | groupNameInitial }}</button>
<span>{{group.name}}</span>
</div>
</mat-option>
</mat-autocomplete>
</mat-form-field>
<div class="adf-cloud-group-error" *ngIf="hasErrorMessage()">
<div fxLayout="row" fxLayoutAlign="start start" [@transitionMessages]="_subscriptAnimationState">
<div class="adf-cloud-group-error-message">
{{ 'ADF_CLOUD_GROUPS.ERROR.NOT_FOUND' | translate : { groupName : searchedValue } }}
</div>
<mat-icon class="adf-cloud-group-error-icon">warning</mat-icon>
</div>
</div>
</div>
<mat-error *ngIf="hasPreselectError() && !isValidationLoading()">
<mat-icon class="adf-start-task-cloud-error-icon">warning</mat-icon>
{{ 'ADF_CLOUD_GROUPS.ERROR.NOT_FOUND' | translate : { groupName : validateGroupsMessage } }}</mat-error>
<mat-error *ngIf="searchGroupsControl.hasError('pattern')">
<mat-icon class="adf-start-task-cloud-error-icon">warning</mat-icon>
{{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.INVALID_PATTERN' | translate: { pattern: getValidationPattern() } }}</mat-error>
<mat-error *ngIf="searchGroupsControl.hasError('maxlength')">
<mat-icon class="adf-start-task-cloud-error-icon">warning</mat-icon>
{{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.INVALID_MAX_LENGTH' | translate: { requiredLength: getValidationMaxLength() } }}
</mat-error>
<mat-error *ngIf="searchGroupsControl.hasError('minlength')">
<mat-icon class="adf-start-task-cloud-error-icon">warning</mat-icon>
{{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.INVALID_MIN_LENGTH' | translate: { requiredLength: getValidationMinLength() } }}</mat-error>
<mat-error *ngIf="searchGroupsControl.hasError('required')">
<mat-icon class="adf-start-task-cloud-error-icon">warning</mat-icon>
{{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.REQUIRED' | translate }} </mat-error>
<mat-error *ngIf="searchGroupsControl.hasError('searchTypingError') && !this.isFocused" data-automation-id="invalid-groups-typing-error">
<mat-icon class="adf-start-task-cloud-error-icon">warning</mat-icon>
{{ 'ADF_CLOUD_GROUPS.ERROR.NOT_FOUND' | translate : { groupName : searchedValue } }}</mat-error>
</form>

View File

@@ -6,7 +6,14 @@
$foreground: map-get($theme, foreground);
.adf {
&-cloud-group-list {
margin: 5px 0;
padding: 10px 0;
}
&-cloud-group {
width: 100%;
.mat-form-field {
width: 100%;
}

View File

@@ -72,7 +72,7 @@ describe('GroupCloudComponent', () => {
component.title = 'TITLE_KEY';
fixture.detectChanges();
const matLabel: HTMLInputElement = <HTMLInputElement> element.querySelector('#adf-group-cloud-title-id');
fixture.whenStable().then( () => {
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(matLabel.textContent).toEqual('TITLE_KEY');
});
@@ -86,14 +86,14 @@ describe('GroupCloudComponent', () => {
findGroupsByNameSpy = spyOn(identityGroupService, 'findGroupsByName').and.returnValue(of(mockIdentityGroups));
}));
it('should list the group if the typed result match', (done) => {
findGroupsByNameSpy.and.returnValue(of(mockIdentityGroups));
it('should list the groups as dropdown options if the search term has results', (done) => {
const inputHTMLElement: HTMLInputElement = <HTMLInputElement> element.querySelector('input');
inputHTMLElement.focus();
inputHTMLElement.value = 'Mock';
inputHTMLElement.dispatchEvent(new Event('keyup'));
inputHTMLElement.dispatchEvent(new Event('input'));
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(fixture.debugElement.queryAll(By.css('mat-option')).length).toEqual(5);
@@ -116,13 +116,19 @@ describe('GroupCloudComponent', () => {
});
});
it('should emit selectedGroup if option is valid', (done) => {
it('should selectedGroup and groupsChanged emit, update selected groups when a group is selected', (done) => {
const group = { name: 'groupname' };
fixture.detectChanges();
spyOn(component, 'hasGroupIdOrName').and.returnValue(true);
const selectEmitSpy = spyOn(component.selectGroup, 'emit');
component.onSelect({ name: 'groupname' });
const changedGroupsSpy = spyOn(component.changedGroups, 'emit');
component.onSelect(group);
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(selectEmitSpy).toHaveBeenCalled();
expect(selectEmitSpy).toHaveBeenCalledWith(group);
expect(changedGroupsSpy).toHaveBeenCalledWith([group]);
expect(component.getSelectedGroups()[0]).toEqual(group);
done();
});
});
@@ -139,9 +145,9 @@ describe('GroupCloudComponent', () => {
fixture.whenStable().then(() => {
inputHTMLElement.blur();
fixture.detectChanges();
const errorMessage = element.querySelector('.adf-cloud-group-error-message');
const errorMessage = element.querySelector('[data-automation-id="invalid-groups-typing-error"]');
expect(errorMessage).not.toBeNull();
expect(errorMessage.textContent).toContain(' ADF_CLOUD_GROUPS.ERROR.NOT_FOUND ');
expect(errorMessage.textContent).toContain('ADF_CLOUD_GROUPS.ERROR.NOT_FOUND');
done();
});
});
@@ -162,13 +168,13 @@ describe('GroupCloudComponent', () => {
element = fixture.nativeElement;
}));
it('should fetch the client ID if appName specified', async(() => {
it('should fetch the client ID if appName specified', async (() => {
const getClientIdByApplicationNameSpy = spyOn(identityGroupService, 'getClientIdByApplicationName').and.callThrough();
component.appName = 'mock-app-name';
const change = new SimpleChange(null, 'mock-app-name', false);
component.ngOnChanges({ 'appName': change });
fixture.detectChanges();
fixture.whenStable().then( () => {
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(getClientIdByApplicationNameSpy).toHaveBeenCalled();
});
@@ -294,89 +300,31 @@ describe('GroupCloudComponent', () => {
fixture.whenStable().then(() => {
inputHTMLElement.blur();
fixture.detectChanges();
const errorMessage = element.querySelector('.adf-cloud-group-error-message');
const errorMessage = element.querySelector('[data-automation-id="invalid-groups-typing-error"]');
expect(errorMessage).not.toBeNull();
expect(errorMessage.textContent).toContain(' ADF_CLOUD_GROUPS.ERROR.NOT_FOUND ');
expect(errorMessage.textContent).toContain('ADF_CLOUD_GROUPS.ERROR.NOT_FOUND');
done();
});
});
});
describe('Single Mode and Pre-selected groups', () => {
describe('No preselected groups', () => {
beforeEach(async () => {
fixture.detectChanges();
});
beforeEach(async(() => {
it('should not pre-select any group when preSelectGroups is empty - single mode', () => {
component.mode = 'single';
component.preSelectGroups = <any> mockIdentityGroups;
fixture.detectChanges();
element = fixture.nativeElement;
}));
it('should not show chip list when mode=single', (done) => {
fixture.detectChanges();
fixture.whenStable().then(() => {
const chip = element.querySelector('mat-chip-list');
expect(chip).toBeNull();
done();
});
const chips = fixture.debugElement.queryAll(By.css('mat-chip'));
expect(chips.length).toEqual(0);
});
it('should not pre-select any group when preSelectGroups is empty and mode=single', (done) => {
component.preSelectGroups = [];
fixture.detectChanges();
fixture.whenStable().then(() => {
const selectedGroup = component.searchGroupsControl.value;
expect(selectedGroup).toBeNull();
done();
});
});
});
describe('Multiple Mode and Pre-selected groups', () => {
const change = new SimpleChange(null, mockIdentityGroups, false);
beforeEach(async(() => {
component.mode = 'multiple';
component.preSelectGroups = <any> mockIdentityGroups;
component.ngOnChanges({ 'preSelectGroups': change });
fixture.detectChanges();
element = fixture.nativeElement;
}));
it('should show chip list when mode=multiple', (done) => {
fixture.detectChanges();
fixture.whenStable().then(() => {
const chip = element.querySelector('mat-chip-list');
expect(chip).toBeDefined();
done();
});
});
it('should pre-select all preSelectGroups when mode=multiple', (done) => {
it('should not pre-select any group when preSelectGroups is empty - multiple mode', () => {
component.mode = 'multiple';
fixture.detectChanges();
component.ngOnChanges({ 'preSelectGroups': change });
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
const chips = fixture.debugElement.queryAll(By.css('mat-chip'));
expect(chips.length).toBe(5);
done();
});
});
it('should emit removeGroup when a selected group is removed', (done) => {
const removeSpy = spyOn(component.removeGroup, 'emit');
component.mode = 'multiple';
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
const removeIcon = fixture.debugElement.query(By.css('mat-chip mat-icon'));
removeIcon.nativeElement.click();
fixture.detectChanges();
expect(removeSpy).toHaveBeenCalled();
done();
});
const chips = fixture.debugElement.queryAll(By.css('mat-chip'));
expect(chips.length).toEqual(0);
});
});
@@ -439,57 +387,114 @@ describe('GroupCloudComponent', () => {
});
});
describe('Single Mode with pre-selected groups', () => {
const changes = new SimpleChange(null, mockIdentityGroups, false);
beforeEach(async(() => {
component.mode = 'single';
component.preSelectGroups = <any> mockIdentityGroups;
component.ngOnChanges({ 'preSelectGroups': changes });
fixture.detectChanges();
element = fixture.nativeElement;
}));
it('should show only one mat chip with the first preSelectedGroup', () => {
const chips = fixture.debugElement.queryAll(By.css('mat-chip'));
expect(chips.length).toEqual(1);
expect(chips[0].attributes['data-automation-id']).toEqual(`adf-cloud-group-chip-${mockIdentityGroups[0].name}`);
});
});
describe('Multiple Mode with pre-selected groups', () => {
const change = new SimpleChange(null, mockIdentityGroups, false);
beforeEach(async(() => {
component.mode = 'multiple';
component.preSelectGroups = <any> mockIdentityGroups;
component.ngOnChanges({ 'preSelectGroups': change });
fixture.detectChanges();
element = fixture.nativeElement;
}));
it('should pre-select all preSelectGroups', () => {
component.mode = 'multiple';
fixture.detectChanges();
component.ngOnChanges({ 'preSelectGroups': change });
fixture.detectChanges();
const chips = fixture.debugElement.queryAll(By.css('mat-chip'));
expect(chips.length).toBe(5);
});
it('should removeGroup and changedGroups emit when a selected group is removed', (done) => {
const removeGroupEmitterSpy = spyOn(component.removeGroup, 'emit');
const changedGroupsEmitterSpy = spyOn(component.changedGroups, 'emit');
const groupToRemove = mockIdentityGroups[0];
component.mode = 'multiple';
fixture.detectChanges();
const removeIcon = fixture.debugElement.query(By.css('mat-chip mat-icon'));
removeIcon.nativeElement.click();
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(removeGroupEmitterSpy).toHaveBeenCalledWith(groupToRemove);
expect(changedGroupsEmitterSpy).toHaveBeenCalledWith([mockIdentityGroups[1], mockIdentityGroups[2], mockIdentityGroups[3], mockIdentityGroups[4]]);
expect(component.getSelectedGroups().indexOf({
id: groupToRemove.id,
name: groupToRemove.name,
path: groupToRemove.path
})).toEqual(-1);
done();
});
});
});
describe('Multiple Mode with read-only', () => {
it('Should not show remove icon for pre-selected groups if readonly property set to true', (done) => {
fixture.detectChanges();
const preselectedGroups = [
component.preSelectGroups = [
{ id: mockIdentityGroups[0].id, name: mockIdentityGroups[0].name, readonly: true },
{ id: mockIdentityGroups[1].id, name: mockIdentityGroups[1].name, readonly: true }
];
component.preSelectGroups = preselectedGroups;
const change = new SimpleChange(null, preselectedGroups, false);
const change = new SimpleChange(null, component.preSelectGroups, false);
component.mode = 'multiple';
component.ngOnChanges({ 'preSelectGroups': change });
fixture.detectChanges();
const chipList = fixture.nativeElement.querySelectorAll('mat-chip-list mat-chip');
const removeIcon = <HTMLElement> fixture.nativeElement.querySelector('[data-automation-id="adf-cloud-group-chip-remove-icon-Mock Group 1"]');
expect(chipList.length).toBe(2);
expect(component.preSelectGroups[0].readonly).toBeTruthy();
expect(component.preSelectGroups[1].readonly).toBeTruthy();
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
const chipList = fixture.nativeElement.querySelectorAll('mat-chip-list mat-chip');
const removeIcon = <HTMLElement> fixture.nativeElement.querySelector('[data-automation-id="adf-cloud-group-chip-remove-icon-Mock Group 1"]');
expect(chipList.length).toBe(2);
expect(component.preSelectGroups[0].readonly).toBeTruthy();
expect(component.preSelectGroups[1].readonly).toBeTruthy();
expect(removeIcon).toBeNull();
expect(component.preSelectGroups.length).toBe(2);
expect(component.preSelectGroups[0].readonly).toBe(true, 'Not removable');
expect(component.preSelectGroups[1].readonly).toBe(true, 'Not removable');
expect(fixture.nativeElement.querySelectorAll('mat-chip-list mat-chip').length).toBe(2);
done();
});
});
it('Should be able to remove preselected groups if readonly property set to false', (done) => {
fixture.detectChanges();
const preselectedGroups = [
component.preSelectGroups = [
{ id: mockIdentityGroups[0].id, name: mockIdentityGroups[0].name, readonly: false },
{ id: mockIdentityGroups[1].id, name: mockIdentityGroups[1].name, readonly: false }
];
component.preSelectGroups = preselectedGroups;
const change = new SimpleChange(null, preselectedGroups, false);
const change = new SimpleChange(null, component.preSelectGroups, false);
component.mode = 'multiple';
const removeGroupSpy = spyOn(component.removeGroup, 'emit');
component.ngOnChanges({ 'preSelectGroups': change });
const removeGroupSpy = spyOn(component.removeGroup, 'emit');
fixture.detectChanges();
const chipList = fixture.nativeElement.querySelectorAll('mat-chip-list mat-chip');
const removeIcon = <HTMLElement> fixture.nativeElement.querySelector('[data-automation-id="adf-cloud-group-chip-remove-icon-Mock Group 1"]');
expect(chipList.length).toBe(2);
expect(component.preSelectGroups[0].readonly).toBe(false, 'Removable');
expect(component.preSelectGroups[1].readonly).toBe(false, 'Removable');
removeIcon.click();
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
const chips = fixture.debugElement.queryAll(By.css('mat-chip'));
const removeIcon = <HTMLElement> fixture.nativeElement.querySelector('[data-automation-id="adf-cloud-group-chip-remove-icon-Mock Group 1"]');
expect(chips.length).toBe(2);
expect(component.preSelectGroups[0].readonly).toBe(false, 'Removable');
expect(component.preSelectGroups[1].readonly).toBe(false, 'Removable');
removeIcon.click();
fixture.detectChanges();
expect(removeGroupSpy).toHaveBeenCalled();
@@ -497,65 +502,101 @@ describe('GroupCloudComponent', () => {
done();
});
});
});
describe('Multiple Mode and Pre-selected groups with validate flag', () => {
describe('Component readonly mode', () => {
const change = new SimpleChange(null, mockIdentityGroups, false);
beforeEach(async(() => {
component.mode = 'multiple';
component.validate = true;
component.preSelectGroups = <any> mockIdentityGroups;
element = fixture.nativeElement;
alfrescoApiService = TestBed.get(AlfrescoApiService);
fixture.detectChanges();
}));
it('should chip list be disabled and show one single chip - single mode', () => {
component.mode = 'single';
component.readOnly = true;
component.preSelectGroups = <any> mockIdentityGroups;
component.ngOnChanges({ 'preSelectGroups': change });
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();
fixture.detectChanges();
const chips = fixture.debugElement.queryAll(By.css('mat-chip'));
const chipList = fixture.nativeElement.querySelector('mat-chip-list');
expect(chips).toBeDefined();
expect(chipList).toBeDefined();
expect(chips.length).toBe(1);
expect(chipList.attributes['ng-reflect-disabled'].value).toEqual('true');
});
it('should chip list be disabled and show all the chips - multiple mode', () => {
component.mode = 'multiple';
component.readOnly = true;
component.preSelectGroups = <any> mockIdentityGroups;
component.ngOnChanges({ 'preSelectGroups': change });
fixture.detectChanges();
const chips = fixture.debugElement.queryAll(By.css('mat-chip'));
const chipList = fixture.nativeElement.querySelector('mat-chip-list');
expect(chips).toBeDefined();
expect(chipList).toBeDefined();
expect(chips.length).toBe(5);
expect(chipList.attributes['ng-reflect-disabled'].value).toEqual('true');
});
});
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);
describe('Preselected groups and validation enabled', () => {
it('should check validation only for the first group and emit warning when group is invalid - single mode', (done) => {
spyOn(identityGroupService, 'findGroupsByName').and.returnValue(Promise.resolve([]));
spyOn(component, 'hasGroupIdOrName').and.returnValue(false);
const expectedWarning = {
message: 'INVALID_PRESELECTED_GROUPS',
groups: [{
id: mockIdentityGroups[0].id,
name: mockIdentityGroups[0].name,
path: mockIdentityGroups[0].path,
subGroups: []
}]
};
component.warning.subscribe(warning => {
expect(warning).toEqual(expectedWarning);
done();
});
component.mode = 'single';
component.validate = true;
component.preSelectGroups = <any> [mockIdentityGroups[0], mockIdentityGroups[1]];
component.ngOnChanges({ 'preSelectGroups': new SimpleChange(null, [mockIdentityGroups[0], mockIdentityGroups[1]], false) });
});
it('should check validation for all the groups and emit warning - multiple mode', (done) => {
spyOn(identityGroupService, 'findGroupsByName').and.returnValue(Promise.resolve(undefined));
const expectedWarning = {
message: 'INVALID_PRESELECTED_GROUPS',
groups: [
{
id: mockIdentityGroups[0].id,
name: mockIdentityGroups[0].name,
path: mockIdentityGroups[0].path,
subGroups: []
},
{
id: mockIdentityGroups[1].id,
name: mockIdentityGroups[1].name,
path: mockIdentityGroups[1].path,
subGroups: []
}]
};
component.warning.subscribe(warning => {
expect(warning).toEqual(expectedWarning);
done();
});
component.mode = 'multiple';
component.validate = true;
component.preSelectGroups = <any> [mockIdentityGroups[0], mockIdentityGroups[1]];
component.ngOnChanges({ 'preSelectGroups': new SimpleChange(null, [mockIdentityGroups[0], mockIdentityGroups[1]], false) });
});
});
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();
});
});
it('should removeDuplicatedGroups return only unique groups', () => {
const duplicatedGroups = [{ name: mockIdentityGroups[0].name }, { name: mockIdentityGroups[0].name }];
expect(component.removeDuplicatedGroups(duplicatedGroups)).toEqual([{ name: mockIdentityGroups[0].name }]);
});
});
});

View File

@@ -35,7 +35,12 @@ 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, LogService } from '@alfresco/adf-core';
import {
IdentityGroupModel,
IdentityGroupSearchParam,
IdentityGroupService,
LogService
} from '@alfresco/adf-core';
@Component({
selector: 'adf-cloud-group',
@@ -88,7 +93,7 @@ export class GroupCloudComponent implements OnInit, OnChanges, OnDestroy {
/** FormControl to search the group */
@Input()
searchGroupsControl: FormControl = new FormControl();
searchGroupsControl: FormControl = new FormControl({ value: '', disabled: false });
/** Role names of the groups to be listed. */
@Input()
@@ -113,61 +118,59 @@ export class GroupCloudComponent implements OnInit, OnChanges, OnDestroy {
@ViewChild('groupInput')
private groupInput: ElementRef<HTMLInputElement>;
private selectedGroups: IdentityGroupModel[] = [];
private searchGroups: IdentityGroupModel[] = [];
private searchGroupsSubject: BehaviorSubject<IdentityGroupModel[]>;
private onDestroy$ = new Subject<boolean>();
searchGroups$ = new BehaviorSubject<IdentityGroupModel[]>([]);
selectedGroups$ = new BehaviorSubject<IdentityGroupModel[]>([]);
selectedGroups: IdentityGroupModel[] = [];
invalidGroups: IdentityGroupModel[] = [];
searchGroups$: Observable<IdentityGroupModel[]>;
_subscriptAnimationState = 'enter';
clientId: string;
searchedValue = '';
isFocused: boolean;
isDisabled: boolean;
private onDestroy$ = new Subject<boolean>();
currentTimeout: any;
invalidGroups: IdentityGroupModel[] = [];
validateGroupsMessage: string;
searchedValue = '';
isLoading = false;
constructor(
private identityGroupService: IdentityGroupService,
private logService: LogService
) { }
private logService: LogService) {}
ngOnInit() {
if (this.searchGroupsSubject === undefined) {
this.searchGroupsSubject = new BehaviorSubject<IdentityGroupModel[]>(this.searchGroups);
this.searchGroups$ = this.searchGroupsSubject.asObservable();
}
this.loadClientId();
this.initSearch();
}
ngOnChanges(changes: SimpleChanges) {
if (this.isPreselectedGroupsChanged(changes)) {
if (this.isValidationEnabled()) {
if (this.hasPreselectedGroupsChanged(changes) || this.hasModeChanged(changes) || this.isValidationChanged(changes)) {
if (this.hasPreSelectGroups()) {
this.loadPreSelectGroups();
} else {
this.loadNoValidationPreselectGroups();
} else if (this.hasPreselectedGroupsCleared(changes)) {
this.selectedGroups = [];
this.invalidGroups = [];
}
if (!this.isValidationEnabled()) {
this.invalidGroups = [];
}
}
if (this.isAppNameChanged(changes.appName)) {
this.disableSearch();
if (changes.appName && this.isAppNameChanged(changes.appName)) {
this.loadClientId();
} else {
this.enableSearch();
this.initSearch();
}
}
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;
}
@@ -175,23 +178,23 @@ export class GroupCloudComponent implements OnInit, OnChanges, OnDestroy {
private async loadClientId() {
this.clientId = await this.identityGroupService.getClientIdByApplicationName(this.appName).toPromise();
if (this.clientId) {
this.enableSearch();
this.searchGroupsControl.enable();
}
}
initSearch() {
this.searchGroupsControl.valueChanges.pipe(
debounceTime(500),
distinctUntilChanged(),
filter((value) => {
return typeof value === 'string';
}),
tap((value) => {
this.searchedValue = value;
if (value) {
this.setError();
this.setTypingError();
}
}),
debounceTime(500),
distinctUntilChanged(),
tap(() => {
this.resetSearchGroups();
}),
@@ -221,7 +224,7 @@ export class GroupCloudComponent implements OnInit, OnChanges, OnDestroy {
takeUntil(this.onDestroy$)
).subscribe((searchedGroup: any) => {
this.searchGroups.push(searchedGroup);
this.searchGroups$.next(this.searchGroups);
this.searchGroupsSubject.next(this.searchGroups);
});
}
@@ -236,7 +239,7 @@ export class GroupCloudComponent implements OnInit, OnChanges, OnDestroy {
private isGroupAlreadySelected(group: IdentityGroupModel): boolean {
if (this.selectedGroups && this.selectedGroups.length > 0 && this.isMultipleMode()) {
const result = this.selectedGroups.find((selectedGroup: IdentityGroupModel) => {
return selectedGroup.id === group.id;
return selectedGroup.name === group.name;
});
return !!result;
@@ -244,115 +247,74 @@ export class GroupCloudComponent implements OnInit, OnChanges, OnDestroy {
return false;
}
async searchGroup(groupName: any): Promise<IdentityGroupModel> {
return (await this.identityGroupService.findGroupsByName(this.createSearchParam(groupName)).toPromise())[0];
async searchGroup(groupName: string): Promise<IdentityGroupModel> {
return (await this.identityGroupService.findGroupsByName({ name: groupName }).toPromise())[0];
}
async filterPreselectGroups() {
const promiseBatch = this.preSelectGroups.map(async (group: IdentityGroupModel) => {
let result: any;
async validatePreselectGroups(): Promise<any> {
this.invalidGroups = [];
let preselectedGroupsToValidate: IdentityGroupModel[] = [];
if (this.isSingleMode()) {
preselectedGroupsToValidate = [this.preSelectGroups[0]];
} else {
preselectedGroupsToValidate = this.removeDuplicatedGroups(this.preSelectGroups);
}
await Promise.all(preselectedGroupsToValidate.map(async (group: IdentityGroupModel) => {
try {
result = await this.searchGroup(group.name);
const validationResult = await this.searchGroup(group.name);
if (!this.hasGroupIdOrName(validationResult)) {
this.invalidGroups.push(group);
}
} catch (error) {
result = [];
this.invalidGroups.push(group);
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();
}
}));
this.checkPreselectValidationErrors();
this.isLoading = false;
}
public checkPreselectValidationErrors() {
this.invalidGroups = this.removeDuplicatedGroups(this.invalidGroups);
if (this.invalidGroups.length > 0) {
this.warning.emit({
message: 'INVALID_PRESELECTED_GROUPS',
groups: this.invalidGroups
});
this.generateInvalidGroupsMessage();
}
this.warning.emit({
message: 'INVALID_PRESELECTED_GROUPS',
groups: this.invalidGroups
});
}
private loadPreSelectGroups() {
if (!this.isMultipleMode()) {
this.loadSinglePreselectGroup();
} else {
this.loadMultiplePreselectGroups();
}
}
generateInvalidGroupsMessage() {
this.validateGroupsMessage = '';
loadNoValidationPreselectGroups() {
this.selectedGroups = [...this.removeDuplicatedGroups([...this.preSelectGroups])];
if (this.isMultipleMode()) {
this.selectedGroups$.next(this.selectedGroups);
} else {
if (this.currentTimeout) {
clearTimeout(this.currentTimeout);
this.invalidGroups.forEach((invalidGroup: IdentityGroupModel, index) => {
if (index === this.invalidGroups.length - 1) {
this.validateGroupsMessage += `${invalidGroup.name} `;
} else {
this.validateGroupsMessage += `${invalidGroup.name}, `;
}
});
}
this.currentTimeout = setTimeout(() => {
this.searchGroupsControl.setValue(this.selectedGroups[0]);
this.onSelect(this.selectedGroups[0]);
}, 0);
private async loadPreSelectGroups() {
this.selectedGroups = [];
if (this.isSingleMode()) {
this.selectedGroups = [this.preSelectGroups[0]];
} else {
this.selectedGroups = this.removeDuplicatedGroups(this.preSelectGroups);
}
if (this.isValidationEnabled()) {
this.isLoading = true;
await this.validatePreselectGroups();
}
}
@@ -368,33 +330,67 @@ export class GroupCloudComponent implements OnInit, OnChanges, OnDestroy {
if (this.isMultipleMode()) {
if (!this.isGroupAlreadySelected(group)) {
this.selectedGroups.push(group);
this.selectedGroups$.next(this.selectedGroups);
this.searchGroups$.next([]);
}
this.groupInput.nativeElement.value = '';
this.searchGroupsControl.setValue('');
} else {
this.invalidGroups = [];
this.selectedGroups = [group];
}
this.changedGroups.emit(this.selectedGroups);
this.clearError();
this.groupInput.nativeElement.value = '';
this.searchGroupsControl.setValue('');
this.changedGroups.emit(this.selectedGroups);
this.resetSearchGroups();
}
onRemove(removedGroup: IdentityGroupModel) {
this.removeGroup.emit(removedGroup);
onRemove(groupToRemove: IdentityGroupModel) {
this.removeGroup.emit(groupToRemove);
const indexToRemove = this.selectedGroups.findIndex((group: IdentityGroupModel) => {
return group.id === removedGroup.id;
return group.id === groupToRemove.id;
});
this.selectedGroups.splice(indexToRemove, 1);
this.selectedGroups$.next(this.selectedGroups);
this.changedGroups.emit(this.selectedGroups);
this.searchGroupsControl.markAsDirty();
if (this.isValidationEnabled()) {
this.removeGroupFromValidation(groupToRemove.name);
this.checkPreselectValidationErrors();
}
}
private removeGroupFromValidation(groupName: string) {
const indexToRemove = this.invalidGroups.findIndex((invalidGroup) => {
return invalidGroup.name === groupName;
});
if (indexToRemove !== -1) {
this.invalidGroups.splice(indexToRemove, 1);
}
}
private resetSearchGroups() {
this.searchGroups = [];
this.searchGroups$.next([]);
this.searchGroupsSubject.next(this.searchGroups);
}
hasGroupIdOrName(group: IdentityGroupModel): boolean {
return group && (group.id !== undefined || group.name !== undefined);
}
isSingleMode(): boolean {
return this.mode === GroupCloudComponent.MODE_SINGLE;
}
private isSingleSelectionReadonly(): boolean {
return this.isSingleMode() && this.selectedGroups.length === 1 && this.selectedGroups[0].readonly === true;
}
hasPreselectError(): boolean {
return this.invalidGroups && this.invalidGroups.length > 0;
}
isReadonly(): boolean {
return this.readOnly || this.isSingleSelectionReadonly();
}
isMultipleMode(): boolean {
@@ -405,58 +401,76 @@ export class GroupCloudComponent implements OnInit, OnChanges, OnDestroy {
return group ? group.name : '';
}
private removeDuplicatedGroups(groups: IdentityGroupModel[]): IdentityGroupModel[] {
removeDuplicatedGroups(groups: IdentityGroupModel[]): IdentityGroupModel[] {
return groups.filter((group, index, self) =>
index === self.findIndex((auxGroup) => {
return group.id === auxGroup.id && group.name === auxGroup.name;
}));
index === self.findIndex((auxGroup) => {
return group.id === auxGroup.id && group.name === auxGroup.name;
}));
}
private hasPreSelectGroups(): boolean {
return this.preSelectGroups && this.preSelectGroups.length > 0;
}
private hasModeChanged(changes): boolean {
return changes && changes.mode && changes.mode.currentValue !== changes.mode.previousValue;
}
private isValidationChanged(changes): boolean {
return changes && changes.validate && changes.validate.currentValue !== changes.validate.previousValue;
}
private hasPreselectedGroupsChanged(changes): boolean {
return changes && changes.preSelectGroups && changes.preSelectGroups.currentValue !== changes.preSelectGroups.previousValue;
}
private hasPreselectedGroupsCleared(changes): boolean {
return changes && changes.preSelectGroups && changes.preSelectGroups.currentValue.length === 0;
}
private createSearchParam(value: string): IdentityGroupSearchParam {
const queryParams: IdentityGroupSearchParam = { name: value };
return queryParams;
}
getSelectedGroups(): IdentityGroupModel[] {
return this.selectedGroups;
}
private hasRoles(): boolean {
return this.roles && this.roles.length > 0;
}
private disableSearch() {
this.searchGroupsControl.disable();
this.isDisabled = true;
}
private enableSearch() {
this.searchGroupsControl.enable();
this.isDisabled = false;
}
private setError() {
this.searchGroupsControl.setErrors({ invalid: true });
}
private clearError() {
this.searchGroupsControl.setErrors(null);
private setTypingError() {
this.searchGroupsControl.setErrors({ searchTypingError: true, ...this.searchGroupsControl.errors });
}
hasError(): boolean {
return this.searchGroupsControl && this.searchGroupsControl.errors && (this.searchGroupsControl.errors.invalid || this.searchGroupsControl.errors.required);
return !!this.searchGroupsControl.errors;
}
isValidationLoading(): boolean {
return this.isValidationEnabled() && this.isLoading;
}
setFocus(isFocused: boolean) {
this.isFocused = isFocused;
}
isValidationEnabled() {
isValidationEnabled(): boolean {
return this.validate === true;
}
hasErrorMessage(): boolean {
return !this.isFocused && this.hasError();
getValidationPattern(): string {
return this.searchGroupsControl.errors.pattern.requiredPattern;
}
getValidationMaxLength(): string {
return this.searchGroupsControl.errors.maxlength.requiredLength;
}
getValidationMinLength(): string {
return this.searchGroupsControl.errors.minlength.requiredLength;
}
ngOnDestroy() {

View File

@@ -181,6 +181,20 @@
"NOT_FOUND": "No group found with the name {{groupName}}"
}
},
"ADF_CLOUD_USERS": {
"ERROR": {
"NOT_FOUND": "No user found with the name {{userName}}"
}
},
"ADF_CLOUD_PEOPLE_GROUPS": {
"ERROR": {
"INVALID_PATTERN": "Not mathcing pattern {{pattern}}",
"INVALID_MIN_LENGTH": "Minimum length {{requiredLength}}",
"INVALID_MAX_LENGTH": "Maximum length {{requiredLength}}",
"REQUIRED": "Field is required"
}
},
"ADF_CLOUD_TASK_HEADER": {
"BUTTON": {
"CLAIM": "Claim",

View File

@@ -1,47 +1,35 @@
<form>
<mat-form-field class="adf-people-cloud">
<mat-label id="adf-people-cloud-title-id">{{ title | translate }}</mat-label>
<mat-chip-list #userChipList *ngIf="isMultipleMode(); else singleSelection">
<mat-chip
*ngFor="let user of selectedUsers$ | async"
[removable]="!(user.readonly)"
[attr.data-automation-id]="'adf-people-cloud-chip-' + user.username"
matTooltip="{{ (user.readonly ? 'ADF_CLOUD_GROUPS.MANDATORY' : '') | translate }}"
(removed)="onRemove(user)">
{{user | fullName}}
<mat-icon
matChipRemove
*ngIf="!(user.readonly || readOnly)"
[attr.data-automation-id]="'adf-people-cloud-chip-remove-icon-' + user.username">
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 #userMultipleChipList [disabled]="isReadonly() || isValidationLoading()" data-automation-id="adf-cloud-people-chip-list">
<mat-chip
*ngFor="let user of selectedUsers"
[removable]="!(user.readonly)"
[attr.data-automation-id]="'adf-people-cloud-chip-' + user.username"
(removed)="onRemove(user)"
matTooltip="{{ (user.readonly ? 'ADF_CLOUD_GROUPS.MANDATORY' : '') | translate }}">
{{user | fullName}}
<mat-icon
matChipRemove
*ngIf="!(user.readonly || readOnly)"
[attr.data-automation-id]="'adf-people-cloud-chip-remove-icon-' + user.username">
cancel
</mat-icon>
</mat-chip>
<input matInput
[formControl]="searchUserCtrl"
[matAutocomplete]="auto"
[matChipInputFor]="userMultipleChipList"
(focus)="setFocus(true)"
(blur)="setFocus(false)"
class="adf-cloud-input"
data-automation-id="adf-people-cloud-search-input" #userInput>
</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]="searchUserCtrl"
[matAutocomplete]="auto">
</ng-template>
<mat-autocomplete autoActiveFirstOption class="adf-people-cloud-list"
#auto="matAutocomplete"
(optionSelected)="onSelect($event.option.value)"
[displayWith]="getDisplayName">
#auto="matAutocomplete"
(optionSelected)="onSelect($event.option.value)"
[displayWith]="getDisplayName">
<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>
@@ -50,10 +38,28 @@
</mat-option>
</mat-autocomplete>
</mat-form-field>
<div class="adf-start-task-cloud-error">
<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>
</div>
<mat-progress-bar
*ngIf="isLoading"
mode="indeterminate">
</mat-progress-bar>
<mat-error *ngIf="hasPreselectError() && !isValidationLoading()">
<mat-icon class="adf-start-task-cloud-error-icon">warning</mat-icon>
{{ 'ADF_CLOUD_USERS.ERROR.NOT_FOUND' | translate : { userName : validateUsersMessage } }}</mat-error>
<mat-error *ngIf="searchUserCtrl.hasError('pattern')">
<mat-icon class="adf-start-task-cloud-error-icon">warning</mat-icon>
{{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.INVALID_PATTERN' | translate: { pattern: getValidationPattern() } }}</mat-error>
<mat-error *ngIf="searchUserCtrl.hasError('maxlength')">
<mat-icon class="adf-start-task-cloud-error-icon">warning</mat-icon>
{{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.INVALID_MAX_LENGTH' | translate: { requiredLength: getValidationMaxLength() } }}
</mat-error>
<mat-error *ngIf="searchUserCtrl.hasError('minlength')">
<mat-icon class="adf-start-task-cloud-error-icon">warning</mat-icon>
{{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.INVALID_MIN_LENGTH' | translate: { requiredLength: getValidationMinLength() } }}</mat-error>
<mat-error *ngIf="searchUserCtrl.hasError('required')">
<mat-icon class="adf-start-task-cloud-error-icon">warning</mat-icon>
{{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.REQUIRED' | translate }} </mat-error>
<mat-error *ngIf="searchUserCtrl.hasError('searchTypingError') && !this.isFocused" data-automation-id="invalid-users-typing-error">
<mat-icon class="adf-start-task-cloud-error-icon">warning</mat-icon>
{{ 'ADF_CLOUD_USERS.ERROR.NOT_FOUND' | translate : { userName : searchedValue } }}</mat-error>
</form>

View File

@@ -17,7 +17,12 @@
import { PeopleCloudComponent } from './people-cloud.component';
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { IdentityUserService, AlfrescoApiService, CoreModule, setupTestBed } from '@alfresco/adf-core';
import {
IdentityUserService,
AlfrescoApiService,
CoreModule,
setupTestBed
} from '@alfresco/adf-core';
import { ProcessServiceCloudTestingModule } from '../../testing/process-service-cloud.testing.module';
import { of } from 'rxjs';
import { mockUsers } from '../mock/user-cloud.mock';
@@ -32,7 +37,6 @@ describe('PeopleCloudComponent', () => {
let identityService: IdentityUserService;
let alfrescoApiService: AlfrescoApiService;
let findUsersByNameSpy: jasmine.Spy;
let findUserByUsernameSpy: jasmine.Spy;
const mock = {
oauth2Auth: {
@@ -62,7 +66,6 @@ describe('PeopleCloudComponent', () => {
identityService = TestBed.get(IdentityUserService);
alfrescoApiService = TestBed.get(AlfrescoApiService);
spyOn(alfrescoApiService, 'getInstance').and.returnValue(mock);
findUserByUsernameSpy = spyOn(identityService, 'findUserByUsername').and.returnValue(Promise.resolve([]));
});
it('should create PeopleCloudComponent', () => {
@@ -73,7 +76,7 @@ describe('PeopleCloudComponent', () => {
component.title = 'TITLE_KEY';
fixture.detectChanges();
const matLabel: HTMLInputElement = <HTMLInputElement> fixture.nativeElement.querySelector('#adf-people-cloud-title-id');
fixture.whenStable().then( () => {
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(matLabel.textContent).toEqual('TITLE_KEY');
});
@@ -83,7 +86,7 @@ describe('PeopleCloudComponent', () => {
fixture.detectChanges();
const matLabel: HTMLInputElement = <HTMLInputElement> fixture.nativeElement.querySelector('#adf-people-cloud-title-id');
fixture.detectChanges();
fixture.whenStable().then( () => {
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(matLabel.textContent).toEqual('');
});
@@ -97,14 +100,14 @@ describe('PeopleCloudComponent', () => {
findUsersByNameSpy = spyOn(identityService, 'findUsersByName').and.returnValue(of(mockUsers));
}));
it('should list the users if the typed result match', (done) => {
findUsersByNameSpy.and.returnValue(of(mockUsers));
it('should list the users as dropdown options if the search term has results', (done) => {
const inputHTMLElement: HTMLInputElement = <HTMLInputElement> element.querySelector('input');
inputHTMLElement.focus();
inputHTMLElement.value = 'first';
inputHTMLElement.dispatchEvent(new Event('keyup'));
inputHTMLElement.dispatchEvent(new Event('input'));
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(fixture.debugElement.queryAll(By.css('mat-option')).length).toEqual(3);
@@ -127,13 +130,19 @@ describe('PeopleCloudComponent', () => {
});
});
it('should emit selectedUser if option is valid', (done) => {
it('should selectedUser and changedUsers emit, update selected users when a user is selected', (done) => {
const user = { username: 'username' };
fixture.detectChanges();
spyOn(component, 'hasUserDetails').and.returnValue(true);
const selectEmitSpy = spyOn(component.selectUser, 'emit');
component.onSelect({ username: 'username' });
const changedUsersSpy = spyOn(component.changedUsers, 'emit');
component.onSelect(user);
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(selectEmitSpy).toHaveBeenCalled();
expect(selectEmitSpy).toHaveBeenCalledWith(user);
expect(changedUsersSpy).toHaveBeenCalledWith([user]);
expect(component.getSelectedUsers()).toEqual([user]);
done();
});
});
@@ -150,9 +159,9 @@ describe('PeopleCloudComponent', () => {
fixture.whenStable().then(() => {
inputHTMLElement.blur();
fixture.detectChanges();
const errorMessage = element.querySelector('.adf-start-task-cloud-error-message');
const errorMessage = element.querySelector('[data-automation-id="invalid-users-typing-error"]');
expect(errorMessage).not.toBeNull();
expect(errorMessage.textContent).toContain('ADF_CLOUD_START_TASK.ERROR.MESSAGE');
expect(errorMessage.textContent).toContain('ADF_CLOUD_USERS.ERROR.NOT_FOUND');
done();
});
});
@@ -173,6 +182,18 @@ describe('PeopleCloudComponent', () => {
element = fixture.nativeElement;
}));
it('should fetch the client ID if appName specified', async (() => {
const getClientIdByApplicationNameSpy = spyOn(identityService, 'getClientIdByApplicationName').and.callThrough();
component.appName = 'mock-app-name';
const change = new SimpleChange(null, 'mock-app-name', false);
component.ngOnChanges({ 'appName': change });
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(getClientIdByApplicationNameSpy).toHaveBeenCalled();
});
}));
it('should list users who have access to the app when appName is specified', (done) => {
const inputHTMLElement: HTMLInputElement = <HTMLInputElement> element.querySelector('input');
inputHTMLElement.focus();
@@ -293,14 +314,34 @@ describe('PeopleCloudComponent', () => {
fixture.whenStable().then(() => {
inputHTMLElement.blur();
fixture.detectChanges();
const errorMessage = element.querySelector('.adf-start-task-cloud-error-message');
const errorMessage = element.querySelector('[data-automation-id="invalid-users-typing-error"]');
expect(errorMessage).not.toBeNull();
expect(errorMessage.textContent).toContain('ADF_CLOUD_START_TASK.ERROR.MESSAGE');
expect(errorMessage.textContent).toContain('ADF_CLOUD_USERS.ERROR.NOT_FOUND');
done();
});
});
});
describe('No preselected users', () => {
beforeEach(async () => {
fixture.detectChanges();
});
it('should not pre-select any user when preSelectUsers is empty - single mode', () => {
component.mode = 'single';
fixture.detectChanges();
const chips = fixture.debugElement.queryAll(By.css('mat-chip'));
expect(chips.length).toEqual(0);
});
it('should not pre-select any users when preSelectUsers is empty - multiple mode', () => {
component.mode = 'multiple';
fixture.detectChanges();
const chips = fixture.debugElement.queryAll(By.css('mat-chip'));
expect(chips.length).toEqual(0);
});
});
describe('When roles defined', () => {
let checkUserHasRoleSpy: jasmine.Spy;
@@ -360,311 +401,170 @@ describe('PeopleCloudComponent', () => {
});
});
describe('Single Mode and Pre-selected users with no validate flag', () => {
describe('Single Mode with Pre-selected users', () => {
const changes = new SimpleChange(null, mockPreselectedUsers, false);
beforeEach(async(() => {
component.mode = 'single';
component.preSelectUsers = <any> mockPreselectedUsers;
component.ngOnChanges({ 'preSelectUsers': changes });
fixture.detectChanges();
element = fixture.nativeElement;
}));
it('should not show chip list when mode=single', (done) => {
fixture.detectChanges();
fixture.whenStable().then(() => {
const chip = element.querySelector('mat-chip-list');
expect(chip).toBeNull();
done();
});
});
it('should not pre-select any user when preSelectUsers is empty and mode=single', (done) => {
component.preSelectUsers = [];
fixture.detectChanges();
fixture.whenStable().then(() => {
const selectedUser = component.searchUserCtrl.value;
expect(selectedUser).toBeNull();
done();
});
});
});
describe('Single Mode and Pre-selected users with validate flag', () => {
beforeEach(async(() => {
component.mode = 'single';
component.validate = true;
component.preSelectUsers = <any> mockPreselectedUsers;
fixture.detectChanges();
element = fixture.nativeElement;
}));
it('should not show chip list when mode=single', (done) => {
fixture.detectChanges();
fixture.whenStable().then(() => {
const chip = element.querySelector('mat-chip-list');
expect(chip).toBeNull();
done();
});
});
});
describe('Multiple Mode and Pre-selected users with no validate flag', () => {
const change = new SimpleChange(null, mockPreselectedUsers, false);
beforeEach(async(() => {
component.mode = 'multiple';
component.preSelectUsers = <any> mockPreselectedUsers;
fixture.detectChanges();
element = fixture.nativeElement;
alfrescoApiService = TestBed.get(AlfrescoApiService);
}));
it('should show chip list when mode=multiple', (done) => {
fixture.detectChanges();
fixture.whenStable().then(() => {
const chip = element.querySelector('mat-chip-list');
expect(chip).toBeDefined();
done();
});
});
it('should pre-select all preSelectUsers when mode=multiple validation disabled', (done) => {
component.mode = 'multiple';
spyOn(component, 'filterPreselectUsers').and.returnValue(Promise.resolve(mockPreselectedUsers));
component.ngOnChanges({ 'preSelectUsers': change });
fixture.detectChanges();
it('should show only one mat chip with the first preSelectedUser', (done) => {
fixture.whenStable().then(() => {
fixture.detectChanges();
component.selectedUsers$.subscribe((selectedUsers) => {
expect(selectedUsers).toBeDefined();
expect(selectedUsers.length).toEqual(2);
expect(selectedUsers[0].id).toEqual('fake-id-2');
done();
});
const chips = fixture.debugElement.queryAll(By.css('mat-chip'));
expect(chips.length).toEqual(1);
expect(chips[0].attributes['data-automation-id']).toEqual(`adf-people-cloud-chip-${mockPreselectedUsers[0].username}`);
done();
});
});
});
describe('Multiple Mode with read-only mode', () => {
describe('Multiple Mode with Pre-selected Users', () => {
it('Should not show remove icon for pre-selected users if readonly property set to true', (done) => {
component.mode = 'multiple';
const removeUserSpy = spyOn(component.removeUser, 'emit');
fixture.detectChanges();
component.preSelectUsers = [
{ id: mockUsers[0].id, username: mockUsers[0].username, readonly: true },
{ id: mockUsers[1].id, username: mockUsers[1].username, readonly: true }
];
fixture.detectChanges();
const chipList = fixture.nativeElement.querySelectorAll('mat-chip-list mat-chip');
const removeIcon = <HTMLElement> fixture.nativeElement.querySelector('[data-automation-id="adf-people-cloud-chip-remove-icon-first-name-1 last-name-1"]');
expect(chipList.length).toBe(2);
expect(component.preSelectUsers[0].readonly).toBeTruthy();
expect(component.preSelectUsers[1].readonly).toBeTruthy();
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(removeIcon).toBeNull();
fixture.detectChanges();
expect(removeUserSpy).not.toHaveBeenCalled();
expect(component.preSelectUsers.length).toBe(2);
expect(component.preSelectUsers[0].readonly).toBe(true, 'Not removable');
expect(component.preSelectUsers[1].readonly).toBe(true, 'not removable');
done();
});
});
});
describe('Multiple Mode and Pre-selected users with validate flag', () => {
const change = new SimpleChange(null, mockPreselectedUsers, false);
beforeEach(async(() => {
const change = new SimpleChange(null, component.preSelectUsers, false);
component.mode = 'multiple';
component.validate = true;
component.preSelectUsers = <any> mockPreselectedUsers;
element = fixture.nativeElement;
alfrescoApiService = TestBed.get(AlfrescoApiService);
fixture.detectChanges();
}));
it('should show chip list when mode=multiple', (done) => {
fixture.detectChanges();
fixture.whenStable().then(() => {
const chip = element.querySelector('mat-chip-list');
expect(chip).toBeDefined();
done();
});
});
it('should pre-select all preSelectUsers when mode=multiple', (done) => {
spyOn(component, 'searchUser').and.returnValue(Promise.resolve(mockPreselectedUsers));
component.mode = 'multiple';
fixture.detectChanges();
component.ngOnChanges({ 'preSelectUsers': change });
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
const chipList = fixture.nativeElement.querySelectorAll('mat-chip-list mat-chip');
const removeIcon = <HTMLElement> fixture.nativeElement.querySelector('[data-automation-id="adf-people-cloud-chip-remove-icon-first-name-1 last-name-1"]');
expect(chipList.length).toBe(2);
expect(component.preSelectUsers[0].readonly).toBeTruthy();
expect(component.preSelectUsers[1].readonly).toBeTruthy();
expect(removeIcon).toBeNull();
done();
});
});
it('Should be able to remove preselected users if readonly property set to false', (done) => {
fixture.detectChanges();
component.preSelectUsers = [
{ id: mockUsers[0].id, username: mockUsers[0].username, readonly: false },
{ id: mockUsers[1].id, username: mockUsers[1].username, readonly: false }
];
const change = new SimpleChange(null, component.preSelectUsers, false);
component.mode = 'multiple';
component.ngOnChanges({ 'preSelectUsers': change });
const removeUserSpy = spyOn(component.removeUser, 'emit');
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
const chips = fixture.debugElement.queryAll(By.css('mat-chip'));
const removeIcon = <HTMLElement> fixture.nativeElement.querySelector(`[data-automation-id="adf-people-cloud-chip-remove-icon-${mockPreselectedUsers[0].username}"]`);
expect(chips.length).toBe(2);
expect(component.preSelectUsers[0].readonly).toBe(false, 'Removable');
expect(component.preSelectUsers[1].readonly).toBe(false, 'Removable');
removeIcon.click();
fixture.detectChanges();
expect(removeUserSpy).toHaveBeenCalled();
expect(fixture.nativeElement.querySelectorAll('mat-chip-list mat-chip').length).toBe(1);
done();
});
});
it('should emit removeUser when a selected user is removed if mode=multiple', (done) => {
spyOn(component.removeUser, 'emit');
component.mode = 'multiple';
fixture.detectChanges();
fixture.whenStable().then(() => {
describe('Component readonly mode', () => {
const change = new SimpleChange(null, mockPreselectedUsers, false);
it('should chip list be disabled and show one single chip - single mode', () => {
component.mode = 'single';
component.readOnly = true;
component.preSelectUsers = <any> mockPreselectedUsers;
component.ngOnChanges({ 'preSelectUsers': change });
fixture.detectChanges();
const removeIcon = fixture.debugElement.query(By.css('mat-chip mat-icon'));
removeIcon.nativeElement.click();
const chips = fixture.debugElement.queryAll(By.css('mat-chip'));
const chipList = fixture.nativeElement.querySelector('mat-chip-list');
expect(chips).toBeDefined();
expect(chipList).toBeDefined();
expect(chips.length).toBe(1);
expect(chipList.attributes['ng-reflect-disabled'].value).toEqual('true');
});
it('should chip list be disabled and show mat chips for all the preselected users - multiple mode', () => {
component.mode = 'multiple';
component.readOnly = true;
component.preSelectUsers = <any> mockPreselectedUsers;
component.ngOnChanges({ 'preSelectUsers': change });
fixture.detectChanges();
expect(component.removeUser.emit).toHaveBeenCalled();
done();
});
});
it('should emit warning if are invalid users', (done) => {
findUserByUsernameSpy.and.returnValue(Promise.resolve([]));
const warnMessage = { message: 'INVALID_PRESELECTED_USERS', users: [{ username: 'invalidUsername' }] };
component.validate = true;
component.preSelectUsers = <any> [{ username: 'invalidUsername' }];
fixture.detectChanges();
component.loadSinglePreselectUser();
component.warning.subscribe((response) => {
expect(response).toEqual(warnMessage);
expect(response.message).toEqual(warnMessage.message);
expect(response.users).toEqual(warnMessage.users);
expect(response.users[0].username).toEqual('invalidUsername');
done();
});
});
it('should filter user by id if validate true', async(() => {
const findByIdSpy = spyOn(identityService, 'findUserById').and.returnValue(of(mockUsers[0]));
component.mode = 'multiple';
component.validate = true;
fixture.detectChanges();
component.preSelectUsers = <any> [{ id: mockUsers[0].id }, { id: mockUsers[1].id }];
component.ngOnChanges({ 'preSelectUsers': change });
fixture.detectChanges();
component.filterPreselectUsers().then((result: any) => {
fixture.detectChanges();
expect(findByIdSpy).toHaveBeenCalled();
expect(component.userExists(result[0])).toEqual(true);
expect(result[1].id).toBe(mockUsers[0].id);
});
}));
it('should filter user by username if validate true', (done) => {
findUserByUsernameSpy.and.returnValue(of(mockUsers));
component.mode = 'multiple';
component.validate = true;
component.preSelectUsers = <any> [{ username: mockUsers[1].username }, { username: mockUsers[2].username }];
fixture.detectChanges();
fixture.whenStable().then(() => {
component.filterPreselectUsers().then((result) => {
expect(findUserByUsernameSpy).toHaveBeenCalled();
expect(component.userExists(result[0])).toEqual(true);
expect(component.userExists(result[1])).toEqual(true);
done();
});
});
});
it('should filter user by email if validate true', (done) => {
const findUserByEmailSpy = spyOn(identityService, 'findUserByEmail').and.returnValue(of(mockUsers));
fixture.detectChanges();
component.mode = 'multiple';
component.validate = true;
component.preSelectUsers = <any> [{ email: mockUsers[1].email }, { email: mockUsers[2].email }];
fixture.detectChanges();
fixture.whenStable().then(() => {
component.filterPreselectUsers().then((result) => {
expect(findUserByEmailSpy).toHaveBeenCalled();
expect(component.userExists(result[0])).toEqual(true);
expect(component.userExists(result[1])).toEqual(true);
done();
});
});
});
it('should search user by id on single selection mode', (done) => {
const findUserByIdSpy = spyOn(identityService, 'findUserById').and.returnValue(of(mockUsers[0]));
component.mode = 'single';
component.validate = true;
fixture.detectChanges();
component.preSelectUsers = <any> [{ id: mockUsers[0].id }];
fixture.detectChanges();
fixture.whenStable().then(() => {
component.validatePreselectUsers().then((result) => {
expect(findUserByIdSpy).toHaveBeenCalled();
expect(result.length).toEqual(1);
done();
});
});
});
it('should not preselect any user if email is invalid and validation enable', (done) => {
const findUserByEmailSpy = spyOn(identityService, 'findUserByEmail').and.returnValue(of([]));
component.mode = 'single';
component.validate = true;
component.preSelectUsers = <any> [{ email: 'invalid email' }];
fixture.detectChanges();
fixture.whenStable().then(() => {
component.validatePreselectUsers().then((result) => {
expect(findUserByEmailSpy).toHaveBeenCalled();
expect(result.length).toEqual(0);
done();
});
});
});
it('should not preselect any user if id is invalid and validation enable', (done) => {
const findUserByIdSpy = spyOn(identityService, 'findUserById').and.returnValue(of([]));
component.mode = 'single';
component.validate = true;
component.preSelectUsers = <any> [{ id: 'invalid id' }];
fixture.detectChanges();
fixture.whenStable().then(() => {
component.validatePreselectUsers().then((result) => {
expect(findUserByIdSpy).toHaveBeenCalled();
expect(result.length).toEqual(0);
done();
});
});
});
it('should not preselect any user if username is invalid and validation enable', (done) => {
findUserByUsernameSpy.and.returnValue(of([]));
component.mode = 'single';
component.validate = true;
component.preSelectUsers = <any> [{ username: 'invalid user' }];
fixture.detectChanges();
fixture.whenStable().then(() => {
component.validatePreselectUsers().then((result) => {
fixture.detectChanges();
expect(findUserByUsernameSpy).toHaveBeenCalled();
expect(result.length).toEqual(0);
done();
});
});
});
it('should remove duplicated preselcted users when a user is duplicated', () => {
spyOn(identityService, 'findUserById').and.returnValue(of(mockUsers[0]));
component.mode = 'multiple';
component.validate = true;
component.preSelectUsers = <any> [{ id: mockUsers[0].id }, { id: mockUsers[0].id }];
component.ngOnChanges({ 'preSelectUsers': change });
fixture.detectChanges();
fixture.whenStable().then(() => {
component.validatePreselectUsers().then((result) => {
expect(result.length).toEqual(1);
});
const chips = fixture.debugElement.queryAll(By.css('mat-chip'));
const chipList = fixture.nativeElement.querySelector('mat-chip-list');
expect(chips).toBeDefined();
expect(chipList).toBeDefined();
expect(chips.length).toBe(2);
expect(chipList.attributes['ng-reflect-disabled'].value).toEqual('true');
});
});
});
describe('Preselected users and validation enabled', () => {
it('should check validation only for the first user and emit warning when user is invalid - single mode', (done) => {
spyOn(identityService, 'findUserById').and.returnValue(Promise.resolve([]));
spyOn(component, 'hasUserDetails').and.returnValue(false);
const expectedWarning = {
message: 'INVALID_PRESELECTED_USERS',
users: [{
id: mockPreselectedUsers[0].id,
username: mockPreselectedUsers[0].username
}]
};
component.warning.subscribe(warning => {
expect(warning).toEqual(expectedWarning);
done();
});
component.mode = 'single';
component.validate = true;
component.preSelectUsers = <any> [mockPreselectedUsers[0], mockPreselectedUsers[1]];
component.ngOnChanges({ 'preSelectUsers': new SimpleChange(null, [mockPreselectedUsers[0], mockPreselectedUsers[1]], false) });
});
it('should check validation for all the users and emit warning - multiple mode', (done) => {
spyOn(identityService, 'findUserById').and.returnValue(Promise.resolve(undefined));
const expectedWarning = {
message: 'INVALID_PRESELECTED_USERS',
users: [
{
id: mockPreselectedUsers[0].id,
username: mockPreselectedUsers[0].username
},
{
id: mockPreselectedUsers[1].id,
username: mockPreselectedUsers[1].username
}]
};
component.warning.subscribe(warning => {
expect(warning).toEqual(expectedWarning);
done();
});
component.mode = 'multiple';
component.validate = true;
component.preSelectUsers = <any> [mockPreselectedUsers[0], mockPreselectedUsers[1]];
component.ngOnChanges({ 'preSelectUsers': new SimpleChange(null, [mockPreselectedUsers[0], mockPreselectedUsers[1]], false) });
});
});
it('should removeDuplicateUsers return only unique users', () => {
const duplicatedUsers = [{ id: mockUsers[0].id }, { id: mockUsers[0].id }];
expect(component.removeDuplicatedUsers(duplicatedUsers)).toEqual([{ id: mockUsers[0].id }]);
});
});

View File

@@ -16,10 +16,27 @@
*/
import { FormControl } from '@angular/forms';
import { Component, OnInit, Output, EventEmitter, ViewEncapsulation, Input, ViewChild, ElementRef, SimpleChanges, OnChanges, OnDestroy, ChangeDetectionStrategy } from '@angular/core';
import {
Component,
OnInit,
Output,
EventEmitter,
ViewEncapsulation,
Input,
SimpleChanges,
OnChanges,
OnDestroy,
ChangeDetectionStrategy,
ViewChild, ElementRef
} from '@angular/core';
import { Observable, of, BehaviorSubject, Subject } from 'rxjs';
import { switchMap, debounceTime, distinctUntilChanged, mergeMap, tap, filter, map, takeUntil } from 'rxjs/operators';
import { FullNamePipe, IdentityUserModel, IdentityUserService, LogService } from '@alfresco/adf-core';
import {
FullNamePipe,
IdentityUserModel,
IdentityUserService,
LogService
} from '@alfresco/adf-core';
import { trigger, state, style, transition, animate } from '@angular/animations';
@Component({
@@ -79,7 +96,7 @@ export class PeopleCloudComponent implements OnInit, OnChanges, OnDestroy {
/** FormControl to search the user */
@Input()
searchUserCtrl: FormControl = new FormControl();
searchUserCtrl: FormControl = new FormControl({ value: '', disabled: false });
/** Placeholder translation key
*/
@@ -106,166 +123,63 @@ export class PeopleCloudComponent implements OnInit, OnChanges, OnDestroy {
private userInput: ElementRef<HTMLInputElement>;
private _searchUsers: IdentityUserModel[] = [];
private selectedUsersSubject: BehaviorSubject<IdentityUserModel[]>;
private searchUsersSubject: BehaviorSubject<IdentityUserModel[]>;
private onDestroy$ = new Subject<boolean>();
selectedUsers: IdentityUserModel[] = [];
selectedUsers$: Observable<IdentityUserModel[]>;
invalidUsers: IdentityUserModel[] = [];
searchUsers$: Observable<IdentityUserModel[]>;
_subscriptAnimationState: string = 'enter';
clientId: string;
isFocused: boolean;
invalidUsers: IdentityUserModel[] = [];
currentTimeout: any;
validateUsersMessage: string;
searchedValue = '';
constructor(private identityUserService: IdentityUserService, private logService: LogService) {
}
isLoading = false;
constructor(
private identityUserService: IdentityUserService,
private logService: LogService) {}
ngOnInit() {
if (this.hasPreSelectUsers()) {
this.selectedUsers = [...this.preSelectUsers];
}
this.initSubjects();
this.initSearch();
if (this.appName) {
this.disableSearch();
this.loadClientId();
}
}
ngOnChanges(changes: SimpleChanges) {
this.initSubjects();
if (this.isPreselectedUserChanged(changes)) {
if (this.isValidationEnabled()) {
this.loadPreSelectUsers();
} else {
this.loadNoValidationPreselectUsers();
}
}
if (changes.appName && this.isAppNameChanged(changes.appName)) {
this.disableSearch();
this.loadClientId();
} else {
this.enableSearch();
}
}
ngOnDestroy() {
clearTimeout(this.currentTimeout);
this.onDestroy$.next(true);
this.onDestroy$.complete();
}
initSubjects() {
if (this.selectedUsersSubject === undefined) {
this.selectedUsersSubject = new BehaviorSubject<IdentityUserModel[]>(this.preSelectUsers);
this.selectedUsers$ = this.selectedUsersSubject.asObservable();
}
if (this.searchUsersSubject === undefined) {
this.searchUsersSubject = new BehaviorSubject<IdentityUserModel[]>(this._searchUsers);
this.searchUsers$ = this.searchUsersSubject.asObservable();
}
this.loadClientId();
this.initSearch();
}
private isAppNameChanged(change) {
return change.previousValue !== change.currentValue && this.appName && this.appName.length > 0;
}
ngOnChanges(changes: SimpleChanges) {
isPreselectedUserChanged(changes: SimpleChanges) {
return changes.preSelectUsers
&& changes.preSelectUsers.previousValue !== changes.preSelectUsers.currentValue
&& this.hasPreSelectUsers();
}
isValidationEnabled() {
return this.validate === true;
}
async validatePreselectUsers(): Promise<any> {
let filteredPreselectUsers: IdentityUserModel[];
let validUsers: IdentityUserModel[] = [];
try {
filteredPreselectUsers = await this.filterPreselectUsers();
} catch (error) {
validUsers = [];
this.logService.error(error);
}
await this.preSelectUsers.map((user: IdentityUserModel) => {
const validUser = this.isValidUser(filteredPreselectUsers, user);
if (validUser) {
validUsers.push(validUser);
} else {
this.invalidUsers.push(user);
if (this.hasPreselectedUsersChanged(changes) || this.hasModeChanged(changes) || this.isValidationChanged(changes)) {
if (this.hasPreSelectUsers()) {
this.loadPreSelectUsers();
} else if (this.hasPreselectedUsersCleared(changes)) {
this.selectedUsers = [];
this.invalidUsers = [];
}
});
validUsers = this.removeDuplicatedUsers(validUsers);
return validUsers;
}
private removeDuplicatedUsers(users: IdentityUserModel[]): IdentityUserModel[] {
return users.filter((user, index, self) =>
index === self.findIndex((auxUser) => {
return user.id === auxUser.id && user.username === auxUser.username;
}));
}
async filterPreselectUsers() {
const promiseBatch = this.preSelectUsers.map(async (user: IdentityUserModel) => {
let result: any;
try {
result = await this.searchUser(user);
} catch (error) {
result = [];
this.logService.error(error);
if (!this.isValidationEnabled()) {
this.invalidUsers = [];
}
const isUserValid: boolean = this.userExists(result);
return isUserValid ? result : null;
});
return Promise.all(promiseBatch);
}
async searchUser(user: IdentityUserModel) {
let key: string = '';
if (user.id) {
key = 'id';
} else if (user.email) {
key = 'email';
} else if (user.username) {
key = 'username';
}
switch (key) {
case 'id': return this.identityUserService.findUserById(user[key]).toPromise();
case 'username': return (await this.identityUserService.findUserByUsername(user[key]).toPromise())[0];
case 'email': return (await this.identityUserService.findUserByEmail(user[key]).toPromise())[0];
default: return of([]);
if (changes.appName && this.isAppNameChanged(changes.appName)) {
this.loadClientId();
this.initSearch();
}
}
private isValidUser(filteredUsers: IdentityUserModel[], user: IdentityUserModel) {
return filteredUsers.find((filteredUser: IdentityUserModel) => {
return filteredUser &&
(filteredUser.id === user.id ||
filteredUser.username === user.username ||
filteredUser.email === user.email);
});
}
public userExists(result: IdentityUserModel): boolean {
return result
&& (result.id !== undefined
|| result.username !== undefined
|| result.email !== undefined);
private async loadClientId() {
this.clientId = await this.identityUserService.getClientIdByApplicationName(this.appName).toPromise();
if (this.clientId) {
this.searchUserCtrl.enable();
}
}
private initSearch() {
@@ -276,12 +190,9 @@ export class PeopleCloudComponent implements OnInit, OnChanges, OnDestroy {
return typeof value === 'string';
}),
tap((value) => {
this.searchedValue = value;
if (value) {
this.setError();
} else {
if (!this.isMultipleMode()) {
this.removeUser.emit();
}
this.setTypingError();
}
}),
tap(() => {
@@ -296,7 +207,6 @@ export class PeopleCloudComponent implements OnInit, OnChanges, OnDestroy {
}),
mergeMap((user: any) => {
if (this.appName) {
return this.checkUserHasAccess(user.id).pipe(
mergeMap((hasRole) => {
return hasRole ? of(user) : of();
@@ -315,6 +225,20 @@ export class PeopleCloudComponent implements OnInit, OnChanges, OnDestroy {
});
}
ngOnDestroy() {
clearTimeout(this.currentTimeout);
this.onDestroy$.next(true);
this.onDestroy$.complete();
}
private isAppNameChanged(change): boolean {
return change && change.previousValue !== change.currentValue && this.appName && this.appName.length > 0;
}
isValidationEnabled(): boolean {
return this.validate === true;
}
private checkUserHasAccess(userId: string): Observable<boolean> {
if (this.hasRoles()) {
return this.identityUserService.checkUserHasAnyClientAppRole(userId, this.clientId, this.roles);
@@ -345,79 +269,105 @@ export class PeopleCloudComponent implements OnInit, OnChanges, OnDestroy {
return false;
}
private loadPreSelectUsers() {
if (!this.isMultipleMode()) {
this.loadSinglePreselectUser();
private async loadPreSelectUsers() {
this.selectedUsers = [];
if (this.isSingleMode()) {
this.selectedUsers = [this.preSelectUsers[0]];
} else {
this.loadMultiplePreselectUsers();
this.selectedUsers = this.removeDuplicatedUsers(this.preSelectUsers);
}
if (this.isValidationEnabled()) {
this.isLoading = true;
await this.validatePreselectUsers();
}
}
async loadNoValidationPreselectUsers() {
this.selectedUsers = [...this.removeDuplicatedUsers(this.preSelectUsers)];
async validatePreselectUsers(): Promise<any> {
this.invalidUsers = [];
const validUsers: IdentityUserModel[] = [];
if (this.isMultipleMode()) {
this.selectedUsersSubject.next(this.selectedUsers);
let preselectedUsersToValidate: IdentityUserModel[] = [];
if (this.isSingleMode()) {
preselectedUsersToValidate = [this.preSelectUsers[0]];
} else {
if (this.currentTimeout) {
clearTimeout(this.currentTimeout);
}
this.currentTimeout = setTimeout(() => {
this.searchUserCtrl.setValue(this.selectedUsers[0]);
this.onSelect(this.selectedUsers[0]);
}, 0);
preselectedUsersToValidate = this.removeDuplicatedUsers(this.preSelectUsers);
}
}
public async loadSinglePreselectUser() {
const users = await this.validatePreselectUsers();
if (users && users.length > 0) {
this.checkPreselectValidationErrors();
this.searchUserCtrl.setValue(users[0]);
} else {
this.checkPreselectValidationErrors();
}
}
public async loadMultiplePreselectUsers() {
const users = await this.validatePreselectUsers();
if (users && users.length > 0) {
this.checkPreselectValidationErrors();
this.selectedUsers = [...this.alignPreselectedReadonlyUsersAfterValidation(users)];
this.selectedUsersSubject.next(this.selectedUsers);
} else {
this.checkPreselectValidationErrors();
}
}
private alignPreselectedReadonlyUsersAfterValidation(users: IdentityUserModel[]) {
this.preSelectUsers.forEach((preSelectedUser, index) => {
if (users[index]) {
if ((preSelectedUser.id === users[index].id) || (preSelectedUser.username === users[index].username)) {
users[index].readonly = preSelectedUser.readonly;
await Promise.all(preselectedUsersToValidate.map(async (user: IdentityUserModel) => {
try {
const validationResult = await this.searchUser(user);
if (!this.hasUserDetails(validationResult)) {
this.invalidUsers.push(user);
} else {
validUsers.push(validationResult);
}
} catch (error) {
this.invalidUsers.push(user);
this.logService.error(error);
}
}));
this.checkPreselectValidationErrors();
this.alignUsersAfterValidation(validUsers);
this.isLoading = false;
}
private alignUsersAfterValidation(validatedUsers: IdentityUserModel[]) {
this.selectedUsers.forEach((selectedUser, index) => {
validatedUsers.forEach(validatedUser => {
if ((selectedUser.id === validatedUser.id) || (selectedUser.username === validatedUser.username)
|| (selectedUser.email === validatedUser.email)) {
validatedUser.readonly = selectedUser.readonly;
this.selectedUsers[index] = validatedUser;
}
});
});
return users;
}
async searchUser(user: IdentityUserModel) {
let key: string = '';
if (user.id) {
key = 'id';
} else if (user.email) {
key = 'email';
} else if (user.username) {
key = 'username';
}
switch (key) {
case 'id':
return this.identityUserService.findUserById(user[key]).toPromise();
case 'username':
return (await this.identityUserService.findUserByUsername(user[key]).toPromise())[0];
case 'email':
return (await this.identityUserService.findUserByEmail(user[key]).toPromise())[0];
default:
return of([]);
}
}
removeDuplicatedUsers(users: IdentityUserModel[]): IdentityUserModel[] {
return users.filter((user, index, self) =>
index === self.findIndex((auxUser) => {
return user.id === auxUser.id && user.username === auxUser.username && user.email === auxUser.email;
}));
}
public checkPreselectValidationErrors() {
this.invalidUsers = this.removeDuplicatedUsers(this.invalidUsers);
if (this.invalidUsers.length > 0) {
this.warning.emit({
message: 'INVALID_PRESELECTED_USERS',
users: this.invalidUsers
});
this.generateInvalidUsersMessage();
}
}
private async loadClientId() {
this.clientId = await this.identityUserService.getClientIdByApplicationName(this.appName).toPromise();
if (this.clientId) {
this.enableSearch();
}
this.warning.emit({
message: 'INVALID_PRESELECTED_USERS',
users: this.invalidUsers
});
}
onSelect(user: IdentityUserModel) {
@@ -425,26 +375,66 @@ export class PeopleCloudComponent implements OnInit, OnChanges, OnDestroy {
if (this.isMultipleMode()) {
if (!this.isUserAlreadySelected(user)) {
this.selectedUsers.push(user);
this.selectedUsersSubject.next(this.selectedUsers);
}
this.userInput.nativeElement.value = '';
this.searchUserCtrl.setValue('');
} else {
this.invalidUsers = [];
this.selectedUsers = [user];
}
this.userInput.nativeElement.value = '';
this.searchUserCtrl.setValue('');
this.changedUsers.emit(this.selectedUsers);
this.clearError();
this.resetSearchUsers();
}
onRemove(user: IdentityUserModel) {
this.removeUser.emit(user);
const indexToRemove = this.selectedUsers.findIndex((selectedUser) => { return selectedUser.id === user.id; });
onRemove(userToRemove: IdentityUserModel) {
this.removeUser.emit(userToRemove);
const indexToRemove = this.selectedUsers.findIndex((selectedUser: IdentityUserModel) => {
return selectedUser.id === userToRemove.id;
});
this.selectedUsers.splice(indexToRemove, 1);
this.selectedUsersSubject.next(this.selectedUsers);
this.changedUsers.emit(this.selectedUsers);
this.searchUserCtrl.markAsDirty();
if (this.isValidationEnabled()) {
this.removeUserFromValidation(userToRemove.username);
this.checkPreselectValidationErrors();
}
}
private removeUserFromValidation(username: string) {
const indexToRemove = this.invalidUsers.findIndex((invalidUser) => {
return invalidUser.username === username;
});
if (indexToRemove !== -1) {
this.invalidUsers.splice(indexToRemove, 1);
}
}
hasUserDetails(user: IdentityUserModel): boolean {
return user && (user.id !== undefined || user.username !== undefined || user.email !== undefined);
}
generateInvalidUsersMessage() {
this.validateUsersMessage = '';
this.invalidUsers.forEach((invalidUser: IdentityUserModel, index) => {
if (index === this.invalidUsers.length - 1) {
this.validateUsersMessage += `${invalidUser.username} `;
} else {
this.validateUsersMessage += `${invalidUser.username}, `;
}
});
}
setTypingError() {
this.searchUserCtrl.setErrors({ searchTypingError: true, ...this.searchUserCtrl.errors });
}
hasPreselectError(): boolean {
return this.invalidUsers && this.invalidUsers.length > 0;
}
getDisplayName(user): string {
@@ -455,21 +445,49 @@ export class PeopleCloudComponent implements OnInit, OnChanges, OnDestroy {
return this.mode === PeopleCloudComponent.MODE_MULTIPLE;
}
isSingleMode(): boolean {
return this.mode === PeopleCloudComponent.MODE_SINGLE;
}
private isSingleSelectionReadonly(): boolean {
return this.isSingleMode() && this.selectedUsers.length === 1 && this.selectedUsers[0].readonly === true;
}
private hasPreSelectUsers(): boolean {
return this.preSelectUsers && this.preSelectUsers.length > 0;
}
private hasModeChanged(changes): boolean {
return changes && changes.mode && changes.mode.currentValue !== changes.mode.previousValue;
}
private isValidationChanged(changes): boolean {
return changes && changes.validate && changes.validate.currentValue !== changes.validate.previousValue;
}
private hasPreselectedUsersChanged(changes): boolean {
return changes && changes.preSelectUsers && changes.preSelectUsers.currentValue !== changes.preSelectUsers.previousValue;
}
private hasPreselectedUsersCleared(changes): boolean {
return changes && changes.preSelectUsers && changes.preSelectUsers.currentValue.length === 0;
}
private resetSearchUsers() {
this._searchUsers = [];
this.searchUsersSubject.next(this._searchUsers);
}
private setError() {
this.searchUserCtrl.setErrors({ invalid: true });
getSelectedUsers(): IdentityUserModel[] {
return this.selectedUsers;
}
private clearError() {
this.searchUserCtrl.setErrors(null);
isReadonly(): boolean {
return this.readOnly || this.isSingleSelectionReadonly();
}
isValidationLoading(): boolean {
return this.isValidationEnabled() && this.isLoading;
}
setFocus(isFocused: boolean) {
@@ -480,16 +498,15 @@ export class PeopleCloudComponent implements OnInit, OnChanges, OnDestroy {
return !!this.searchUserCtrl.errors;
}
hasErrorMessage(): boolean {
return !this.isFocused && this.hasError();
getValidationPattern(): string {
return this.searchUserCtrl.errors.pattern.requiredPattern;
}
private disableSearch() {
this.searchUserCtrl.disable();
getValidationMaxLength(): string {
return this.searchUserCtrl.errors.maxlength.requiredLength;
}
private enableSearch() {
this.searchUserCtrl.enable();
getValidationMinLength(): string {
return this.searchUserCtrl.errors.minlength.requiredLength;
}
}