diff --git a/demo-shell/resources/i18n/en.json b/demo-shell/resources/i18n/en.json index 059f18ce4e..e26bd15f52 100644 --- a/demo-shell/resources/i18n/en.json +++ b/demo-shell/resources/i18n/en.json @@ -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", diff --git a/demo-shell/src/app/components/cloud/people-groups-cloud-demo.component.html b/demo-shell/src/app/components/cloud/people-groups-cloud-demo.component.html index b42ad00475..0f04e5f167 100644 --- a/demo-shell/src/app/components/cloud/people-groups-cloud-demo.component.html +++ b/demo-shell/src/app/components/cloud/people-groups-cloud-demo.component.html @@ -32,21 +32,23 @@ {{ 'PEOPLE_GROUPS_CLOUD.PRESELECT_VALIDATION' | translate }} + {{ + 'PEOPLE_GROUPS_CLOUD.READONLY_MODE' | translate }}
-

{{ 'PEOPLE_GROUPS_CLOUD.ALL_SELECTED_USERS' | translate }}

+

{{ 'PEOPLE_GROUPS_CLOUD.ALL_PRESELECTED_USERS' | translate }}

person @@ -99,23 +101,24 @@ Preselect: {{ DEFAULT_GROUP_PLACEHOLDER }} - {{ + {{ 'PEOPLE_GROUPS_CLOUD.PRESELECT_VALIDATION' | translate }} + {{ + 'PEOPLE_GROUPS_CLOUD.READONLY_MODE' | translate }}
-

{{ 'PEOPLE_GROUPS_CLOUD.ALL_SELECTED_GROUPS' | translate }}

+

{{ 'PEOPLE_GROUPS_CLOUD.ALL_PRESELECTED_GROUPS' | translate }}

group diff --git a/demo-shell/src/app/components/cloud/people-groups-cloud-demo.component.ts b/demo-shell/src/app/components/cloud/people-groups-cloud-demo.component.ts index 6dd41084b9..31084620c2 100644 --- a/demo-shell/src/app/components/cloud/people-groups-cloud-demo.component.ts +++ b/demo-shell/src/app/components/cloud/people-groups-cloud-demo.component.ts @@ -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; } diff --git a/docs/process-services-cloud/components/group-cloud.component.md b/docs/process-services-cloud/components/group-cloud.component.md index d2cb38e0e1..9131edfba8 100644 --- a/docs/process-services-cloud/components/group-cloud.component.md +++ b/docs/process-services-cloud/components/group-cloud.component.md @@ -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 + + +``` + +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: diff --git a/docs/process-services-cloud/components/people-cloud.component.md b/docs/process-services-cloud/components/people-cloud.component.md index 42062794e4..3a3a1272e7 100644 --- a/docs/process-services-cloud/components/people-cloud.component.md +++ b/docs/process-services-cloud/components/people-cloud.component.md @@ -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 + + +``` + +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 ``` diff --git a/e2e/process-services-cloud/people-group-cloud-component.e2e.ts b/e2e/process-services-cloud/people-group-cloud-component.e2e.ts index a0ba972012..1a959ad183 100644 --- a/e2e/process-services-cloud/people-group-cloud-component.e2e.ts +++ b/e2e/process-services-cloud/people-group-cloud-component.e2e.ts @@ -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(''); - }); - }); }); diff --git a/e2e/process-services-cloud/people-group-cloud-filter-component.e2e.ts b/e2e/process-services-cloud/people-group-cloud-filter-component.e2e.ts index 54d1767e5a..bc79bacd89 100644 --- a/e2e/process-services-cloud/people-group-cloud-filter-component.e2e.ts +++ b/e2e/process-services-cloud/people-group-cloud-filter-component.e2e.ts @@ -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 () => { diff --git a/e2e/process-services-cloud/start-task/start-task-custom-app-cloud.e2e.ts b/e2e/process-services-cloud/start-task/start-task-custom-app-cloud.e2e.ts index cf3566a3eb..25c951cbf8 100644 --- a/e2e/process-services-cloud/start-task/start-task-custom-app-cloud.e2e.ts +++ b/e2e/process-services-cloud/start-task/start-task-custom-app-cloud.e2e.ts @@ -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}`); diff --git a/lib/process-services-cloud/src/lib/group/components/group-cloud.component.html b/lib/process-services-cloud/src/lib/group/components/group-cloud.component.html index 155e0d0425..91b995f1d2 100644 --- a/lib/process-services-cloud/src/lib/group/components/group-cloud.component.html +++ b/lib/process-services-cloud/src/lib/group/components/group-cloud.component.html @@ -1,61 +1,71 @@ -
- - {{ (title || 'ADF_CLOUD_GROUPS.SEARCH-GROUP') | translate }} - - - {{group.name}} - - cancel - - - - +
+ + {{ (title || 'ADF_CLOUD_GROUPS.SEARCH-GROUP') | translate }} + + + {{group.name}} + + cancel + + + + - + + +
+ + {{group.name}} +
+
+
+
+ + - - -
- - {{group.name}} -
-
-
- -
-
-
- {{ 'ADF_CLOUD_GROUPS.ERROR.NOT_FOUND' | translate : { groupName : searchedValue } }} -
- warning -
-
-
+ + warning + {{ 'ADF_CLOUD_GROUPS.ERROR.NOT_FOUND' | translate : { groupName : validateGroupsMessage } }} + + warning + {{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.INVALID_PATTERN' | translate: { pattern: getValidationPattern() } }} + + warning + {{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.INVALID_MAX_LENGTH' | translate: { requiredLength: getValidationMaxLength() } }} + + + warning + {{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.INVALID_MIN_LENGTH' | translate: { requiredLength: getValidationMinLength() } }} + + warning + {{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.REQUIRED' | translate }} + + warning + {{ 'ADF_CLOUD_GROUPS.ERROR.NOT_FOUND' | translate : { groupName : searchedValue } }} + diff --git a/lib/process-services-cloud/src/lib/group/components/group-cloud.component.scss b/lib/process-services-cloud/src/lib/group/components/group-cloud.component.scss index 983eb02543..8308ac7463 100644 --- a/lib/process-services-cloud/src/lib/group/components/group-cloud.component.scss +++ b/lib/process-services-cloud/src/lib/group/components/group-cloud.component.scss @@ -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%; } diff --git a/lib/process-services-cloud/src/lib/group/components/group-cloud.component.spec.ts b/lib/process-services-cloud/src/lib/group/components/group-cloud.component.spec.ts index b1df775c7b..bf6c888c98 100644 --- a/lib/process-services-cloud/src/lib/group/components/group-cloud.component.spec.ts +++ b/lib/process-services-cloud/src/lib/group/components/group-cloud.component.spec.ts @@ -72,7 +72,7 @@ describe('GroupCloudComponent', () => { component.title = 'TITLE_KEY'; fixture.detectChanges(); const matLabel: 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = [{ 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 = 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 = [{ 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 = [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 = [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 = [{ 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 }]); }); }); }); diff --git a/lib/process-services-cloud/src/lib/group/components/group-cloud.component.ts b/lib/process-services-cloud/src/lib/group/components/group-cloud.component.ts index 4b537ce123..90289a5cf5 100644 --- a/lib/process-services-cloud/src/lib/group/components/group-cloud.component.ts +++ b/lib/process-services-cloud/src/lib/group/components/group-cloud.component.ts @@ -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; - private selectedGroups: IdentityGroupModel[] = []; - private searchGroups: IdentityGroupModel[] = []; + private searchGroupsSubject: BehaviorSubject; + private onDestroy$ = new Subject(); - searchGroups$ = new BehaviorSubject([]); - - selectedGroups$ = new BehaviorSubject([]); + selectedGroups: IdentityGroupModel[] = []; + invalidGroups: IdentityGroupModel[] = []; + searchGroups$: Observable; _subscriptAnimationState = 'enter'; - clientId: string; - - searchedValue = ''; - isFocused: boolean; - isDisabled: boolean; - - private onDestroy$ = new Subject(); 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(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 { - return (await this.identityGroupService.findGroupsByName(this.createSearchParam(groupName)).toPromise())[0]; + async searchGroup(groupName: string): Promise { + 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 { + 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 { - 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() { diff --git a/lib/process-services-cloud/src/lib/i18n/en.json b/lib/process-services-cloud/src/lib/i18n/en.json index 7544ec47a4..c1e399370d 100644 --- a/lib/process-services-cloud/src/lib/i18n/en.json +++ b/lib/process-services-cloud/src/lib/i18n/en.json @@ -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", diff --git a/lib/process-services-cloud/src/lib/people/components/people-cloud.component.html b/lib/process-services-cloud/src/lib/people/components/people-cloud.component.html index b282ae016f..89174f1969 100644 --- a/lib/process-services-cloud/src/lib/people/components/people-cloud.component.html +++ b/lib/process-services-cloud/src/lib/people/components/people-cloud.component.html @@ -1,47 +1,35 @@
{{ title | translate }} - - - {{user | fullName}} - - cancel - - - + + + {{user | fullName}} + + cancel + + + - - - + #auto="matAutocomplete" + (optionSelected)="onSelect($event.option.value)" + [displayWith]="getDisplayName">
@@ -50,10 +38,28 @@ -
-
-
{{ 'ADF_CLOUD_START_TASK.ERROR.MESSAGE' | translate }}
- warning -
-
+ + + + + warning + {{ 'ADF_CLOUD_USERS.ERROR.NOT_FOUND' | translate : { userName : validateUsersMessage } }} + + warning + {{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.INVALID_PATTERN' | translate: { pattern: getValidationPattern() } }} + + warning + {{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.INVALID_MAX_LENGTH' | translate: { requiredLength: getValidationMaxLength() } }} + + + warning + {{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.INVALID_MIN_LENGTH' | translate: { requiredLength: getValidationMinLength() } }} + + warning + {{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.REQUIRED' | translate }} + + warning + {{ 'ADF_CLOUD_USERS.ERROR.NOT_FOUND' | translate : { userName : searchedValue } }} diff --git a/lib/process-services-cloud/src/lib/people/components/people-cloud.component.spec.ts b/lib/process-services-cloud/src/lib/people/components/people-cloud.component.spec.ts index 2140cb5e6f..a2218f6ae1 100644 --- a/lib/process-services-cloud/src/lib/people/components/people-cloud.component.spec.ts +++ b/lib/process-services-cloud/src/lib/people/components/people-cloud.component.spec.ts @@ -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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = [{ 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 = [{ 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 = [{ 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 = [{ 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 = [{ 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 = [{ 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 = [{ 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 = [{ 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 = [{ 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 = [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 = [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 }]); + }); + }); diff --git a/lib/process-services-cloud/src/lib/people/components/people-cloud.component.ts b/lib/process-services-cloud/src/lib/people/components/people-cloud.component.ts index c38e6bd8f2..8ce3bb74f0 100644 --- a/lib/process-services-cloud/src/lib/people/components/people-cloud.component.ts +++ b/lib/process-services-cloud/src/lib/people/components/people-cloud.component.ts @@ -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; private _searchUsers: IdentityUserModel[] = []; - private selectedUsersSubject: BehaviorSubject; private searchUsersSubject: BehaviorSubject; private onDestroy$ = new Subject(); selectedUsers: IdentityUserModel[] = []; - selectedUsers$: Observable; + invalidUsers: IdentityUserModel[] = []; + searchUsers$: Observable; _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(this.preSelectUsers); - this.selectedUsers$ = this.selectedUsersSubject.asObservable(); - } - if (this.searchUsersSubject === undefined) { this.searchUsersSubject = new BehaviorSubject(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 { - 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 { 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 { + 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; } - } diff --git a/lib/testing/src/lib/process-services-cloud/pages/people-cloud-component.page.ts b/lib/testing/src/lib/process-services-cloud/pages/people-cloud-component.page.ts index 718abb924d..f5d945ba85 100644 --- a/lib/testing/src/lib/process-services-cloud/pages/people-cloud-component.page.ts +++ b/lib/testing/src/lib/process-services-cloud/pages/people-cloud-component.page.ts @@ -29,6 +29,11 @@ export class PeopleCloudComponentPage { await this.peopleCloudSearch.sendKeys(protractor.Key.BACK_SPACE); } + async clearAssigneeFromChip(username: string): Promise { + const assigneeChipRemoveIcon = element(by.css(`[data-automation-id="adf-people-cloud-chip-remove-icon-${username}"]`)); + await assigneeChipRemoveIcon.click(); + } + async searchAssigneeAndSelect(name: string): Promise { await BrowserActions.clearSendKeys(this.peopleCloudSearch, name); await this.selectAssigneeFromList(name);