[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

@ -309,10 +309,11 @@
"APP_FILTER_MODE": "Filter by application name",
"ROLE_FILTER_MODE": "Filter by role",
"PRESELECT_VALIDATION": "Preselect validation",
"ALL_SELECTED_USERS": "All Selected Users",
"ALL_SELECTED_GROUPS": "All Selected Groups",
"ALL_PRESELECTED_USERS": "All Pre-selected Users",
"ALL_PRESELECTED_GROUPS": "All Pre-selected Groups",
"INVALID_USERS": "Invalid Users",
"INVALID_GROUPS": "Invalid Groups"
"INVALID_GROUPS": "Invalid Groups",
"READONLY_MODE": "Readonly Mode"
},
"SETTINGS_CLOUD": {
"MULTISELECTION": "Multiselection",

View File

@ -32,21 +32,23 @@
</mat-form-field>
<mat-checkbox class="app-preselect-value" (change)="onChangePeopleValidation($event)">{{
'PEOPLE_GROUPS_CLOUD.PRESELECT_VALIDATION' | translate }}</mat-checkbox>
<mat-checkbox data-automation-id="app-people-readonly" value="{{ peopleReadonly }}" (change)="onChangePeopleReadonly($event)">{{
'PEOPLE_GROUPS_CLOUD.READONLY_MODE' | translate }}</mat-checkbox>
</div>
<div>
<adf-cloud-people
[preSelectUsers]="preSelectUsers"
[readOnly]="peopleReadonly"
[validate]="peoplePreselectValidation"
[appName]="peopleAppName"
[roles]="peopleRoles"
[title]="'ADF_TASK_LIST.START_TASK.FORM.LABEL.ASSIGNEE'"
[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>
<h4>{{ 'PEOPLE_GROUPS_CLOUD.ALL_PRESELECTED_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>
@ -99,23 +101,24 @@
<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)">{{
<mat-checkbox class="app-preselect-value" (change)="onChangeGroupValidation($event)">{{
'PEOPLE_GROUPS_CLOUD.PRESELECT_VALIDATION' | translate }}</mat-checkbox>
<mat-checkbox data-automation-id="app-group-readonly" value="{{ groupReadonly }}" (change)="onChangeGroupReadonly($event)">{{
'PEOPLE_GROUPS_CLOUD.READONLY_MODE' | translate }}</mat-checkbox>
</div>
<div>
<adf-cloud-group
[mode]="groupMode"
[readOnly]="groupReadonly"
[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>
<h4>{{ 'PEOPLE_GROUPS_CLOUD.ALL_PRESELECTED_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>

View File

@ -34,17 +34,18 @@ export class PeopleGroupCloudDemoComponent {
peopleMode: string = PeopleCloudComponent.MODE_SINGLE;
preSelectUsers: IdentityUserModel[] = [];
invalidUsers: IdentityGroupModel[] = [];
invalidUsers: IdentityUserModel[] = [];
peopleRoles: string[] = [];
peopleAppName: string;
peopleFilterMode: string = this.DEFAULT_FILTER_MODE;
peoplePreselectValidation: Boolean = false;
groupPreselectValidation = false;
peopleReadonly = false;
groupReadonly = false;
groupMode: string = GroupCloudComponent.MODE_SINGLE;
preSelectGroup: IdentityGroupModel[] = [];
invalidGroups: IdentityGroupModel[] = [];
selectedGroupList: IdentityGroupModel[] = [];
groupRoles: string[];
groupAppName: string;
groupFilterMode: string = this.DEFAULT_FILTER_MODE;
@ -75,12 +76,18 @@ export class PeopleGroupCloudDemoComponent {
onChangePeopleMode(event: MatRadioChange) {
this.peopleMode = event.value;
this.preSelectUsers = [...this.preSelectUsers];
}
onChangePeopleReadonly(event: MatCheckboxChange) {
this.peopleReadonly = event.checked;
}
onChangeGroupReadonly(event: MatCheckboxChange) {
this.groupReadonly = event.checked;
}
onChangeGroupsMode(event: MatRadioChange) {
this.groupMode = event.value;
this.preSelectGroup = [...this.preSelectGroup];
}
onChangePeopleFilterMode(event: MatRadioChange) {
@ -119,18 +126,10 @@ 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) {
@ -167,22 +166,6 @@ export class PeopleGroupCloudDemoComponent {
return this.groupMode === GroupCloudComponent.MODE_MULTIPLE;
}
onRemoveGroup(group: IdentityGroupModel) {
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);
}
}
get peopleSingleMode() {
return PeopleCloudComponent.MODE_SINGLE;
}

View File

@ -26,9 +26,10 @@ Searches Groups.
| Name | Type | Default value | Description |
| ---- | ---- | ------------- | ----------- |
| appName | `string` | | Name of the application. If specified this shows the users who have access to the app. |
| appName | `string` | | Name of the application. If specified this shows the groups who have access to the app. |
| readOnly | `boolean` | false | readOnly mode (true/false). |
| mode | `string` | | User selection mode (single/multiple). |
| preSelectGroups | [`IdentityGroupModel`](../../../lib/core/models/identity-group.model.ts)`[]` | \[] | 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. |
| preSelectGroups | [`IdentityGroupModel`](../../../lib/core/models/identity-group.model.ts)`[]` | \[] | 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. |
| roles | `string[]` | \[] | Role names of the groups to be listed. |
| searchGroupsControl | `FormControl` | new FormControl() | FormControl to search the group |
| title | `string` | | Title of the field |
@ -95,6 +96,20 @@ export class MyComponent {
### Read-only
You can use `readonly` property to set the component in `readonly` mode. Readonly mode will disable any interaction with the component.
```html
<adf-cloud-group
[appName]="'simple-app'"
[mode]="'multiple'"
[preSelectGroups]="groups"
[readOnly]="true">
</adf-cloud-group>
```
If you want to manage each group seperately you can set their readonly property at your preference.
You need to have component's readonly property set to false. Component's readonly mode overwrites groups level.
You can use `readonly` property to make preselected groups read-only in `multiple` mode.
Usage example:

View File

@ -25,6 +25,7 @@ Allows one or more users to be selected (with auto-suggestion) based on the inpu
| Name | Type | Default value | Description |
| ---- | ---- | ------------- | ----------- |
| appName | `string` | | Name of the application. If specified, this shows the users who have access to the app. |
| readOnly | `boolean` | false | readOnly mode (true/false). |
| mode | `string` | | User selection mode (single/multiple). |
| preSelectUsers | [`IdentityUserModel`](../../../lib/core/models/identity-user.model.ts)`[]` | | Array of users to be pre-selected. All users in the array are pre-selected in multi selection mode, but only the first user is pre-selected in single selection mode. Mandatory properties are: id, email, username |
| roles | `string[]` | | Role names of the users to be listed. |
@ -44,7 +45,19 @@ Allows one or more users to be selected (with auto-suggestion) based on the inpu
### Read-only
You can use `readonly` property to make preselected users read-only in `multiple` mode.
You can use `readonly` property to set the component in `readonly` mode. Readonly mode will disable any interaction with the component.
```html
<adf-cloud-people
[appName]="'simple-app'"
[mode]="'multiple'"
[preSelectUsers]="preSelectUsers"
[readOnly]="true">
</adf-cloud-people>
```
If you want to manage each user seperately you can set their readonly property at your preference.
You need to have component's readonly property set to false. Component's readonly mode overwrites users level.
```ts
const preSelectUsers = [
@ -55,7 +68,7 @@ const preSelectUsers = [
```
```html
<adf-cloud-people
[mode]="'multiple'",
[mode]="'multiple'"
[preSelectUsers]="preSelectUsers">
</adf-cloud-people>
```

View File

@ -193,21 +193,22 @@ describe('People Groups Cloud Component', () => {
await peopleGroupCloudComponentPage.clickPeopleCloudSingleSelection();
await peopleGroupCloudComponentPage.checkPeopleCloudSingleSelectionIsSelected();
await peopleGroupCloudComponentPage.enterPeoplePreselect('[{"id":"12345","username":"someUsername","email":"someEmail"}]');
await expect(await peopleCloudComponent.checkSelectedPeople('someUsername'));
await peopleGroupCloudComponentPage.clickPreselectValidation();
await expect(await peopleGroupCloudComponentPage.getPreselectValidationStatus()).toBe('true');
await peopleGroupCloudComponentPage.enterPeoplePreselect('[{"id":"12345","username":"someUsername","email":"someEmail"}]');
await expect(await peopleCloudComponent.getAssigneeFieldContent()).toBe('');
await expect(await peopleGroupCloudComponentPage.getPreselectValidationStatus()).toBe('true');
await peopleGroupCloudComponentPage.enterPeoplePreselect(`[{"id":"${noRoleUser.idIdentityService}"}]`);
await expect(await peopleCloudComponent.getAssigneeFieldContent()).toBe(`${noRoleUser.firstName} ${noRoleUser.lastName}`);
await expect(await peopleCloudComponent.checkSelectedPeople(`${noRoleUser.firstName} ${noRoleUser.lastName}`));
await peopleGroupCloudComponentPage.enterPeoplePreselect(`[{"email":"${apsUser.email}"}]`);
await expect(await peopleCloudComponent.getAssigneeFieldContent()).toBe(`${apsUser.firstName} ${apsUser.lastName}`);
await expect(await peopleCloudComponent.checkSelectedPeople(`${apsUser.firstName} ${apsUser.lastName}`));
await peopleGroupCloudComponentPage.enterPeoplePreselect(`[{"username":"${testUser.username}"}]`);
await expect(await peopleCloudComponent.getAssigneeFieldContent()).toBe(`${testUser.firstName} ${testUser.lastName}`);
await expect(await peopleCloudComponent.checkSelectedPeople(`${testUser.firstName} ${testUser.lastName}`));
});
it('[C309676] Should fetch the preselect users based on the Validate flag set to True in Multiple mode selection', async () => {
@ -254,18 +255,6 @@ describe('People Groups Cloud Component', () => {
});
it('[C309678] Should not fetch the preselect users when mandatory parameters Id, Email and username are missing', async () => {
await peopleGroupCloudComponentPage.clickPeopleCloudMultipleSelection();
await peopleGroupCloudComponentPage.checkPeopleCloudMultipleSelectionIsSelected();
await peopleGroupCloudComponentPage.clickPreselectValidation();
await expect(await peopleGroupCloudComponentPage.getPreselectValidationStatus()).toBe('true');
await peopleGroupCloudComponentPage.enterPeoplePreselect(`[{"firstName":"${apsUser.firstName}","lastName":"${apsUser.lastName},"` +
`{"firstName":"${testUser.firstName}","lastName":"${testUser.lastName}",{"firstName":"${noRoleUser.firstName}","lastName":"${noRoleUser.lastName}"]`);
await browser.sleep(200);
await expect(await peopleCloudComponent.getAssigneeFieldContent()).toBe('');
});
});
});

View File

@ -90,7 +90,7 @@ describe('People Groups Cloud Component', () => {
await peopleCloudComponent.checkUserIsDisplayed(`${testUser.firstName} ${testUser.lastName}`);
await peopleCloudComponent.selectAssigneeFromList(`${testUser.firstName} ${testUser.lastName}`);
await browser.sleep(100);
await expect(await peopleCloudComponent.getAssigneeFieldContent()).toBe(`${testUser.firstName} ${testUser.lastName}`);
await expect(await peopleCloudComponent.checkSelectedPeople(`${testUser.firstName} ${testUser.lastName}`));
});
it('[C305041] Should filter the People Multiple Selection with the Application name filter', async () => {
@ -118,7 +118,7 @@ describe('People Groups Cloud Component', () => {
await groupCloudComponentPage.searchGroups(hrGroup.name);
await groupCloudComponentPage.checkGroupIsDisplayed(hrGroup.name);
await groupCloudComponentPage.selectGroupFromList(hrGroup.name);
await expect(await groupCloudComponentPage.getGroupsFieldContent()).toBe(hrGroup.name);
await expect(await groupCloudComponentPage.checkSelectedGroup(hrGroup.name));
});
it('[C305041] Should filter the Groups Multiple Selection with the Application name filter', async () => {

View File

@ -106,7 +106,7 @@ describe('Start Task', () => {
it('[C297675] Should create a task unassigned when assignee field is empty in Start Task form', async () => {
await tasksCloudDemoPage.openNewTaskForm();
await startTask.checkFormIsDisplayed();
await peopleCloudComponent.clearAssignee();
await peopleCloudComponent.clearAssigneeFromChip(testUser.username);
await startTask.addName(unassignedTaskName);
await startTask.clickStartButton();
await tasksCloudDemoPage.editTaskFilterCloudComponent();
@ -125,7 +125,7 @@ describe('Start Task', () => {
it('[C291956] Should be able to create a new standalone task without assignee', async () => {
await tasksCloudDemoPage.openNewTaskForm();
await startTask.checkFormIsDisplayed();
await peopleCloudComponent.clearAssignee();
await peopleCloudComponent.clearAssigneeFromChip(testUser.username);
await startTask.addName(unassignedTaskName);
await startTask.checkStartButtonIsEnabled();
await startTask.clickStartButton();
@ -203,7 +203,7 @@ describe('Start Task', () => {
it('[C291953] Assignee field should display the logged user as default', async () => {
await tasksCloudDemoPage.openNewTaskForm();
await startTask.checkFormIsDisplayed();
await expect(await peopleCloudComponent.getAssignee()).toContain(testUser.firstName, 'does not contain Admin');
await expect(await peopleCloudComponent.checkSelectedPeople(testUser.firstName));
await startTask.clickCancelButton();
});
@ -212,7 +212,7 @@ describe('Start Task', () => {
await startTask.checkFormIsDisplayed();
await startTask.addName(reassignTaskName);
await expect(await peopleCloudComponent.getAssignee()).toBe(`${testUser.firstName} ${testUser.lastName}`);
await peopleCloudComponent.checkSelectedPeople(`${testUser.firstName} ${testUser.lastName}`);
await peopleCloudComponent.searchAssignee(apsUser.username);
await peopleCloudComponent.checkUserIsDisplayed(`${apsUser.firstName} ${apsUser.lastName}`);
await peopleCloudComponent.selectAssigneeFromList(`${apsUser.firstName} ${apsUser.lastName}`);

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;
}
}

View File

@ -29,6 +29,11 @@ export class PeopleCloudComponentPage {
await this.peopleCloudSearch.sendKeys(protractor.Key.BACK_SPACE);
}
async clearAssigneeFromChip(username: string): Promise<void> {
const assigneeChipRemoveIcon = element(by.css(`[data-automation-id="adf-people-cloud-chip-remove-icon-${username}"]`));
await assigneeChipRemoveIcon.click();
}
async searchAssigneeAndSelect(name: string): Promise<void> {
await BrowserActions.clearSendKeys(this.peopleCloudSearch, name);
await this.selectAssigneeFromList(name);