[AAE-9019] - People/Group cloud with HxP (#7658)

* Cover the use cases by mocking them

* Replace the mock with real stream and remove useless code

* Provide new service to fetch groups

* Fix group tests

* Use the interface and token injection

* [AAE-8870] add unit test and mock for new service

* Improve roles condifion

* initialize the stream as part of NgOnInit to be sure it relies on the correct FormControl instance(input)

* Rollback tmp change for roles

* [AAE-8641] people abstraction mock

* [AAE-8641] revert angular.json changes

* [AAE-8641] few conditions and code improvements

* [AAE-8641] revert change input controls name

* [AAE-8641] initialize the stream as part of ngOnInit

* [AAE-8641] people abstraction improvements

* [AAE-8870] cherry pick people abstraction

* [AAE-8641] fix people-group e2es

* fix lint

* remove hardcoded identityHost

* Use the identityhost api in case of cloud

* Solve issue with returnType array string

* Rebase and use GroupModel from cloud

* Rebase and use GroupModel from cloud

* Use the bpmHost instead of identityFor

* Add identity ingress for user access service

* Rename test

* Fix linting issues

* Fix playwright storybook e2e for people and group

* Trigger travis

* Fix people group e2e

* improved formatting

* Remove not needed travis var override

* Remove unused import after rebase

* Make roles in filter optional + remove comments

Co-authored-by: Tomasz <tomasz.gnyp@hyland.com>
Co-authored-by: arditdomi <ardit.domi@hyland.com>
This commit is contained in:
Maurizio Vitale
2022-06-28 16:21:59 +01:00
committed by GitHub
parent 93c5619e23
commit e27833d770
72 changed files with 2117 additions and 1937 deletions

View File

@@ -65,13 +65,13 @@ describe('GroupCloudWidgetComponent', () => {
expect(asterisk.textContent).toEqual('*');
});
it('should be invalid if no option is selected after interaction', async () => {
it('should be invalid after user interaction without typing', async () => {
fixture.detectChanges();
await fixture.whenStable();
expect(element.querySelector('.adf-invalid')).toBeFalsy();
const cloudGroupInput = element.querySelector('adf-cloud-group');
const cloudGroupInput = element.querySelector('[data-automation-id="adf-cloud-group-search-input"]');
cloudGroupInput.dispatchEvent(new Event('blur'));
fixture.detectChanges();

View File

@@ -16,11 +16,12 @@
*/
import { Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
import { WidgetComponent, IdentityGroupModel, FormService } from '@alfresco/adf-core';
import { WidgetComponent, FormService } from '@alfresco/adf-core';
import { FormControl } from '@angular/forms';
import { filter, takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
import { ComponentSelectionMode } from '../../../../types';
import { IdentityGroupModel } from '../../../../group/models/identity-group.model';
/* eslint-disable @angular-eslint/component-selector */

View File

@@ -15,20 +15,20 @@
* limitations under the License.
*/
import { FormFieldModel, FormFieldTypes, FormModel, IdentityUserModel, IdentityUserService, setupTestBed } from '@alfresco/adf-core';
import { FormFieldModel, FormFieldTypes, FormModel, IdentityUserModel, setupTestBed } from '@alfresco/adf-core';
import { TranslateModule } from '@ngx-translate/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { PeopleCloudWidgetComponent } from './people-cloud.widget';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { ProcessServiceCloudTestingModule } from '../../../../testing/process-service-cloud.testing.module';
import { IdentityUserService } from '../../../../people/services/identity-user.service';
import { mockShepherdsPie, mockYorkshirePudding } from '../../../../people/mock/people-cloud.mock';
describe('PeopleCloudWidgetComponent', () => {
let fixture: ComponentFixture<PeopleCloudWidgetComponent>;
let widget: PeopleCloudWidgetComponent;
let element: HTMLElement;
let identityUserService: IdentityUserService;
const currentUser = { id: 'id', username: 'user' };
const fakeUser = { id: 'fake-id', username: 'fake' };
setupTestBed({
imports: [
@@ -48,20 +48,20 @@ describe('PeopleCloudWidgetComponent', () => {
fixture = TestBed.createComponent(PeopleCloudWidgetComponent);
widget = fixture.componentInstance;
element = fixture.nativeElement;
spyOn(identityUserService, 'getCurrentUserInfo').and.returnValue(fakeUser);
spyOn(identityUserService, 'getCurrentUserInfo').and.returnValue(mockShepherdsPie);
});
it('should preselect the current user', () => {
widget.field = new FormFieldModel(new FormModel(), { value: null, selectLoggedUser: true });
fixture.detectChanges();
expect(widget.preSelectUsers).toEqual([fakeUser]);
expect(widget.preSelectUsers).toEqual([mockShepherdsPie]);
expect(identityUserService.getCurrentUserInfo).toHaveBeenCalled();
});
it('should not preselect the current user if value exist', () => {
widget.field = new FormFieldModel(new FormModel(), { value: [currentUser], selectLoggedUser: true });
widget.field = new FormFieldModel(new FormModel(), { value: [mockYorkshirePudding], selectLoggedUser: true });
fixture.detectChanges();
expect(widget.preSelectUsers).toEqual([currentUser]);
expect(widget.preSelectUsers).toEqual([mockYorkshirePudding]);
expect(identityUserService.getCurrentUserInfo).not.toHaveBeenCalled();
});
@@ -84,13 +84,13 @@ describe('PeopleCloudWidgetComponent', () => {
expect(asterisk.textContent).toEqual('*');
});
it('should be invalid if no option is selected after interaction', async () => {
it('should be invalid after user interaction without typing', async () => {
fixture.detectChanges();
await fixture.whenStable();
expect(element.querySelector('.adf-invalid')).toBeFalsy();
const cloudPeopleInput = element.querySelector('adf-cloud-people');
const cloudPeopleInput = element.querySelector('[data-automation-id="adf-people-cloud-search-input"]');
cloudPeopleInput.dispatchEvent(new Event('blur'));
fixture.detectChanges();

View File

@@ -16,11 +16,13 @@
*/
import { Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
import { WidgetComponent, IdentityUserModel, FormService, IdentityUserService } from '@alfresco/adf-core';
import { WidgetComponent, FormService } from '@alfresco/adf-core';
import { FormControl } from '@angular/forms';
import { filter, takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
import { ComponentSelectionMode } from '../../../../types';
import { IdentityUserModel } from '../../../../people/models/identity-user.model';
import { IdentityUserService } from '../../../../people/services/identity-user.service';
/* eslint-disable @angular-eslint/component-selector */

View File

@@ -24,35 +24,54 @@ import { GroupCloudModule } from '../group-cloud.module';
import { GroupCloudComponent } from './group-cloud.component';
import {
setupTestBed,
IdentityGroupService,
mockIdentityGroups,
AlfrescoApiService,
CoreTestingModule
} from '@alfresco/adf-core';
import { SimpleChange } from '@angular/core';
import { TranslateModule } from '@ngx-translate/core';
import { IdentityGroupService } from '../services/identity-group.service';
import { mockFoodGroups, mockMeatChicken, mockVegetableAubergine } from '../mock/group-cloud.mock';
describe('GroupCloudComponent', () => {
let component: GroupCloudComponent;
let fixture: ComponentFixture<GroupCloudComponent>;
let element: HTMLElement;
let identityGroupService: IdentityGroupService;
let alfrescoApiService: AlfrescoApiService;
let findGroupsByNameSpy: jasmine.Spy;
const mock: any = {
oauth2Auth: {
callCustomApi: () => Promise.resolve(mockIdentityGroups)
},
isEcmLoggedIn: () => false,
reply: jasmine.createSpy('reply')
};
// eslint-disable-next-line prefer-arrow/prefer-arrow-functions
function getElement<T = HTMLElement>(selector: string): T {
return fixture.nativeElement.querySelector(selector);
}
async function searchGroup(value: string) {
const input = getElement<HTMLInputElement>('input');
input.focus();
input.value = value;
input.dispatchEvent(new Event('keyup'));
input.dispatchEvent(new Event('input'));
await fixture.whenStable();
fixture.detectChanges();
}
async function searchGroupsAndBlur(value: string) {
const input = getElement<HTMLInputElement>('input');
input.focus();
input.value = value;
input.dispatchEvent(new Event('keyup'));
input.dispatchEvent(new Event('input'));
await fixture.whenStable();
fixture.detectChanges();
input.blur();
fixture.detectChanges();
}
function getGroupListUI() {
return fixture.debugElement.queryAll(By.css('[data-automation-id="adf-cloud-group-row"]'));
}
setupTestBed({
imports: [
TranslateModule.forRoot(),
@@ -68,16 +87,12 @@ describe('GroupCloudComponent', () => {
element = fixture.nativeElement;
identityGroupService = TestBed.inject(IdentityGroupService);
alfrescoApiService = TestBed.inject(AlfrescoApiService);
spyOn(alfrescoApiService, 'getInstance').and.returnValue(mock);
});
it('should populate placeholder when title is present', async () => {
it('should populate placeholder when title is present', () => {
component.title = 'TITLE_KEY';
fixture.detectChanges();
await fixture.whenStable();
const matLabel = element.querySelector<HTMLInputElement>('#adf-group-cloud-title-id');
expect(matLabel.textContent).toEqual('TITLE_KEY');
@@ -87,330 +102,88 @@ describe('GroupCloudComponent', () => {
beforeEach(() => {
fixture.detectChanges();
findGroupsByNameSpy = spyOn(identityGroupService, 'findGroupsByName').and.returnValue(of(mockIdentityGroups));
findGroupsByNameSpy = spyOn(identityGroupService, 'search').and.returnValue(of(mockFoodGroups));
});
it('should list the groups as dropdown options if the search term has results', (done) => {
const input = getElement<HTMLInputElement>('input');
input.focus();
input.value = 'Mock';
input.dispatchEvent(new Event('keyup'));
input.dispatchEvent(new Event('input'));
it('should list the groups as dropdown options if the search term has results', async () => {
await searchGroup('All');
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(fixture.debugElement.queryAll(By.css('[data-automation-id="adf-cloud-group-row"]')).length).toEqual(5);
expect(findGroupsByNameSpy).toHaveBeenCalled();
done();
});
const groupList = getGroupListUI();
expect(groupList.length).toEqual(2);
});
it('should not be able to search for a group that its name matches one of the preselected groups name', (done) => {
component.preSelectGroups = [{ name: mockIdentityGroups[0].name }];
const changes = new SimpleChange(null, [{ name: mockIdentityGroups[0].name }], false);
it('should not be able to search for a group that its name matches one of the preselected groups name', async () => {
component.preSelectGroups = [{ name: mockVegetableAubergine.name }];
const changes = new SimpleChange(null, [{ name: mockVegetableAubergine.name }], false);
component.ngOnChanges({ preSelectGroups: changes });
fixture.detectChanges();
const inputHTMLElement = element.querySelector<HTMLInputElement>('input');
inputHTMLElement.focus();
inputHTMLElement.value = 'mock-group';
inputHTMLElement.dispatchEvent(new Event('keyup'));
inputHTMLElement.dispatchEvent(new Event('input'));
fixture.detectChanges();
await searchGroup('Aubergine');
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(fixture.debugElement.queryAll(By.css('[data-automation-id="adf-cloud-group-row"]')).length).toEqual(4);
done();
});
const groupList = getGroupListUI();
expect(groupList.length).toEqual(1);
});
it('should hide result list if input is empty', (done) => {
fixture.detectChanges();
it('should hide result list if input is empty', async () => {
await searchGroup('');
const input = getElement<HTMLInputElement>('input');
input.focus();
input.value = '';
input.dispatchEvent(new Event('keyup'));
input.dispatchEvent(new Event('input'));
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(element.querySelector('[data-automation-id="adf-cloud-group-row"]')).toBeNull();
done();
});
expect(element.querySelector('[data-automation-id="adf-cloud-group-row"]')).toBeNull();
});
it('should update selected groups when a group is selected', (done) => {
fixture.detectChanges();
it('should update selected groups when a group is selected', async () => {
const selectEmitSpy = spyOn(component.selectGroup, 'emit');
const changedGroupsSpy = spyOn(component.changedGroups, 'emit');
const group = { name: 'groupname' };
component.onSelect(group);
component.onSelect(mockMeatChicken);
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(selectEmitSpy).toHaveBeenCalledWith(group);
expect(changedGroupsSpy).toHaveBeenCalledWith([group]);
expect(component.selectedGroups).toEqual([group]);
done();
});
await fixture.whenStable();
expect(selectEmitSpy).toHaveBeenCalledWith(mockMeatChicken);
expect(changedGroupsSpy).toHaveBeenCalledWith([mockMeatChicken]);
expect(component.selectedGroups).toEqual([mockMeatChicken]);
});
it('should replace the group in single-selection mode', () => {
component.mode = 'single';
const group1 = { name: 'group1' };
const group2 = { name: 'group2' };
component.onSelect(mockVegetableAubergine);
expect(component.selectedGroups).toEqual([mockVegetableAubergine]);
component.onSelect(group1);
expect(component.selectedGroups).toEqual([group1]);
component.onSelect(group2);
expect(component.selectedGroups).toEqual([group2]);
component.onSelect(mockMeatChicken);
expect(component.selectedGroups).toEqual([mockMeatChicken]);
});
it('should allow multiple groups in multi-selection mode', () => {
component.mode = 'multiple';
const group1 = { name: 'group1' };
const group2 = { name: 'group2' };
component.onSelect(mockVegetableAubergine);
component.onSelect(mockMeatChicken);
component.onSelect(group1);
component.onSelect(group2);
expect(component.selectedGroups).toEqual([group1, group2]);
expect(component.selectedGroups).toEqual([mockVegetableAubergine, mockMeatChicken]);
});
it('should allow only unique groups in multi-selection mode', () => {
component.mode = 'multiple';
const group1 = { name: 'group1' };
const group2 = { name: 'group2' };
component.onSelect(mockVegetableAubergine);
component.onSelect(mockMeatChicken);
component.onSelect(mockMeatChicken);
component.onSelect(mockVegetableAubergine);
component.onSelect(group1);
component.onSelect(group2);
component.onSelect(group1);
component.onSelect(group2);
expect(component.selectedGroups).toEqual([group1, group2]);
expect(component.selectedGroups).toEqual([mockVegetableAubergine, mockMeatChicken]);
});
it('should show an error message if the search result empty', (done) => {
it('should show an error message and icon if the search result empty', async () => {
findGroupsByNameSpy.and.returnValue(of([]));
fixture.detectChanges();
const input = getElement<HTMLInputElement>('input');
input.focus();
input.value = 'ZZZ';
input.dispatchEvent(new Event('keyup'));
input.dispatchEvent(new Event('input'));
await searchGroupsAndBlur('INCORRECTVALUE');
fixture.detectChanges();
fixture.whenStable().then(() => {
input.blur();
fixture.detectChanges();
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');
done();
});
});
it('should display proper error icon', (done) => {
findGroupsByNameSpy.and.returnValue(of([]));
fixture.detectChanges();
const input = getElement<HTMLInputElement>('input');
input.focus();
input.value = 'ZZZ';
input.dispatchEvent(new Event('keyup'));
input.dispatchEvent(new Event('input'));
fixture.detectChanges();
fixture.whenStable().then(() => {
input.blur();
fixture.detectChanges();
const errorIcon = element.querySelector('.adf-error-icon').textContent;
expect(errorIcon).toEqual('error_outline');
done();
});
});
});
describe('when application name defined', () => {
let checkGroupHasAnyClientAppRoleSpy: jasmine.Spy;
let checkGroupHasClientAppSpy: jasmine.Spy;
beforeEach(() => {
findGroupsByNameSpy = spyOn(identityGroupService, 'findGroupsByName').and.returnValue(of(mockIdentityGroups));
checkGroupHasAnyClientAppRoleSpy = spyOn(identityGroupService, 'checkGroupHasAnyClientAppRole').and.returnValue(of(true));
checkGroupHasClientAppSpy = spyOn(identityGroupService, 'checkGroupHasClientApp').and.returnValue(of(true));
component.preSelectGroups = [];
component.appName = 'mock-app-name';
fixture.detectChanges();
});
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();
await fixture.whenStable();
expect(getClientIdByApplicationNameSpy).toHaveBeenCalled();
});
it('should list groups who have access to the app when appName is specified', (done) => {
const input = getElement<HTMLInputElement>('input');
input.focus();
input.value = 'M';
input.dispatchEvent(new Event('keyup'));
input.dispatchEvent(new Event('input'));
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(fixture.debugElement.queryAll(By.css('[data-automation-id="adf-cloud-group-row"]')).length).toEqual(5);
done();
});
});
it('should not list groups who do not have access to the app when appName is specified', (done) => {
checkGroupHasClientAppSpy.and.returnValue(of(false));
fixture.detectChanges();
const input = getElement<HTMLInputElement>('input');
input.focus();
input.value = 'M';
input.dispatchEvent(new Event('keyup'));
input.dispatchEvent(new Event('input'));
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(fixture.debugElement.queryAll(By.css('[data-automation-id="adf-cloud-group-row"]')).length).toEqual(0);
expect(fixture.debugElement.queryAll(By.css('[data-automation-id="adf-cloud-group-no-results"]')).length).toEqual(1);
done();
});
});
it('should list groups if given roles mapped with client roles', (done) => {
component.roles = ['MOCK_ROLE_1', 'MOCK_ROLE_1'];
const input = getElement<HTMLInputElement>('input');
input.focus();
input.value = 'M';
input.dispatchEvent(new Event('keyup'));
input.dispatchEvent(new Event('input'));
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(fixture.debugElement.queryAll(By.css('[data-automation-id="adf-cloud-group-row"]')).length).toEqual(5);
expect(checkGroupHasAnyClientAppRoleSpy).toHaveBeenCalled();
done();
});
});
it('should not list groups if roles are not mapping with client roles', (done) => {
checkGroupHasAnyClientAppRoleSpy.and.returnValue(of(false));
component.roles = ['MOCK_ROLE_1', 'MOCK_ROLE_1'];
const input = getElement<HTMLInputElement>('input');
input.focus();
input.value = 'M';
input.dispatchEvent(new Event('keyup'));
input.dispatchEvent(new Event('input'));
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(fixture.debugElement.queryAll(By.css('[data-automation-id="adf-cloud-group-row"]')).length).toEqual(0);
expect(fixture.debugElement.queryAll(By.css('[data-automation-id="adf-cloud-group-no-results"]')).length).toEqual(1);
expect(checkGroupHasAnyClientAppRoleSpy).toHaveBeenCalled();
done();
});
});
it('should not call client role mapping sevice if roles not specified', (done) => {
const input = getElement<HTMLInputElement>('input');
input.focus();
input.value = 'M';
input.dispatchEvent(new Event('keyup'));
input.dispatchEvent(new Event('input'));
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(checkGroupHasAnyClientAppRoleSpy).not.toHaveBeenCalled();
done();
});
});
it('should validate access to the app when appName is specified', (done) => {
const input = getElement<HTMLInputElement>('input');
input.focus();
input.value = 'M';
input.dispatchEvent(new Event('keyup'));
input.dispatchEvent(new Event('input'));
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(checkGroupHasClientAppSpy).toHaveBeenCalledTimes(5);
done();
});
});
it('should not validate access to the app when appName is not specified', (done) => {
component.appName = '';
fixture.detectChanges();
const input = getElement<HTMLInputElement>('input');
input.focus();
input.value = 'M';
input.dispatchEvent(new Event('keyup'));
input.dispatchEvent(new Event('input'));
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(checkGroupHasClientAppSpy).not.toHaveBeenCalled();
done();
});
});
it('should show an error message if the group does not have access', (done) => {
checkGroupHasClientAppSpy.and.returnValue(of(false));
findGroupsByNameSpy.and.returnValue(of([]));
fixture.detectChanges();
const input = getElement<HTMLInputElement>('input');
input.focus();
input.value = 'ZZZ';
input.dispatchEvent(new Event('keyup'));
input.dispatchEvent(new Event('input'));
fixture.detectChanges();
fixture.whenStable().then(() => {
input.blur();
fixture.detectChanges();
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');
done();
});
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');
const errorIcon = element.querySelector('.adf-error-icon').textContent;
expect(errorIcon).toEqual('error_outline');
});
});
@@ -436,76 +209,12 @@ describe('GroupCloudComponent', () => {
});
});
describe('When roles defined', () => {
let checkGroupHasRoleSpy: jasmine.Spy;
beforeEach(() => {
component.roles = ['mock-role-1', 'mock-role-2'];
spyOn(identityGroupService, 'findGroupsByName').and.returnValue(of(mockIdentityGroups));
checkGroupHasRoleSpy = spyOn(identityGroupService, 'checkGroupHasRole').and.returnValue(of(true));
fixture.detectChanges();
});
it('should filter if groups has any specified role', (done) => {
fixture.detectChanges();
const input = getElement<HTMLInputElement>('input');
input.focus();
input.value = 'M';
input.dispatchEvent(new Event('input'));
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(fixture.debugElement.queryAll(By.css('[data-automation-id="adf-cloud-group-row"]')).length).toEqual(5);
expect(checkGroupHasRoleSpy).toHaveBeenCalledTimes(5);
done();
});
});
it('should not filter groups if group does not have any specified role', (done) => {
fixture.detectChanges();
checkGroupHasRoleSpy.and.returnValue(of(false));
const input = getElement<HTMLInputElement>('input');
input.focus();
input.value = 'M';
input.dispatchEvent(new Event('input'));
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(fixture.debugElement.queryAll(By.css('[data-automation-id="adf-cloud-group-row"]')).length).toEqual(0);
expect(fixture.debugElement.queryAll(By.css('[data-automation-id="adf-cloud-group-no-results"]')).length).toEqual(1);
expect(checkGroupHasRoleSpy).toHaveBeenCalled();
done();
});
});
it('should not call checkGroupHasRole service when roles are not specified', (done) => {
component.roles = [];
fixture.detectChanges();
const input = getElement<HTMLInputElement>('input');
input.focus();
input.value = 'M';
input.dispatchEvent(new Event('input'));
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(checkGroupHasRoleSpy).not.toHaveBeenCalled();
done();
});
});
});
describe('Single Mode with pre-selected groups', () => {
const changes = new SimpleChange(null, mockIdentityGroups, false);
const changes = new SimpleChange(null, mockFoodGroups, false);
beforeEach(() => {
component.mode = 'single';
component.preSelectGroups = mockIdentityGroups;
component.preSelectGroups = mockFoodGroups;
component.ngOnChanges({ preSelectGroups: changes });
fixture.detectChanges();
});
@@ -513,16 +222,16 @@ describe('GroupCloudComponent', () => {
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}`);
expect(chips[0].attributes['data-automation-id']).toEqual(`adf-cloud-group-chip-${mockVegetableAubergine.name}`);
});
});
describe('Multiple Mode with pre-selected groups', () => {
const change = new SimpleChange(null, mockIdentityGroups, false);
const change = new SimpleChange(null, mockFoodGroups, false);
beforeEach(() => {
component.mode = 'multiple';
component.preSelectGroups = mockIdentityGroups;
component.preSelectGroups = mockFoodGroups;
component.ngOnChanges({ preSelectGroups: change });
fixture.detectChanges();
});
@@ -533,196 +242,145 @@ describe('GroupCloudComponent', () => {
component.ngOnChanges({ preSelectGroups: change });
fixture.detectChanges();
const chips = fixture.debugElement.queryAll(By.css('mat-chip'));
expect(chips.length).toBe(5);
expect(chips.length).toBe(2);
});
it('should removeGroup and changedGroups emit when a selected group is removed', (done) => {
it('should removeGroup and changedGroups emit when a selected group is removed', async () => {
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.selectedGroups.indexOf({
id: groupToRemove.id,
name: groupToRemove.name,
path: groupToRemove.path
})).toEqual(-1);
done();
});
await fixture.whenStable();
expect(removeGroupEmitterSpy).toHaveBeenCalledWith(mockVegetableAubergine);
expect(changedGroupsEmitterSpy).toHaveBeenCalledWith([mockMeatChicken]);
expect(component.selectedGroups.indexOf({
id: mockMeatChicken.id,
name: mockMeatChicken.name
})).toEqual(-1);
});
});
});
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();
component.preSelectGroups = [
{ id: mockIdentityGroups[0].id, name: mockIdentityGroups[0].name, readonly: true },
{ id: mockIdentityGroups[1].id, name: mockIdentityGroups[1].name, readonly: true }
];
const change = new SimpleChange(null, component.preSelectGroups, false);
it('Should not show remove icon for pre-selected groups if readonly property set to true', async () => {
component.mode = 'multiple';
component.ngOnChanges({ preSelectGroups: change });
component.preSelectGroups = [
{ id: mockVegetableAubergine.id, name: mockVegetableAubergine.name, readonly: true },
mockMeatChicken
];
const changes = new SimpleChange(null, [{ name: mockVegetableAubergine.name }], false);
component.ngOnChanges({ preSelectGroups: changes });
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
const chipList = fixture.nativeElement.querySelectorAll('mat-chip-list mat-chip');
const removeIcon = getElement('[data-automation-id="adf-cloud-group-chip-remove-icon-Mock Group 1"]');
await fixture.whenStable();
expect(chipList.length).toBe(2);
expect(component.preSelectGroups[0].readonly).toBeTruthy();
expect(component.preSelectGroups[1].readonly).toBeTruthy();
expect(removeIcon).toBeNull();
const chipList = fixture.nativeElement.querySelectorAll('mat-chip-list mat-chip');
expect(chipList.length).toBe(2);
const removeIconAubergine = getElement(`[data-automation-id="adf-cloud-group-chip-remove-icon-${mockVegetableAubergine.name}"]`);
expect(removeIconAubergine).toBeNull();
const removeIconPepper = getElement(`[data-automation-id="adf-cloud-group-chip-remove-icon-${mockMeatChicken.name}"]`);
expect(removeIconPepper).not.toBeNull();
done();
});
});
it('Should be able to remove preselected groups if readonly property set to false', (done) => {
fixture.detectChanges();
component.preSelectGroups = [
{ id: mockIdentityGroups[0].id, name: mockIdentityGroups[0].name, readonly: false },
{ id: mockIdentityGroups[1].id, name: mockIdentityGroups[1].name, readonly: false }
];
it('Should be able to remove preselected groups if readonly property set to false', async () => {
component.mode = 'multiple';
component.preSelectGroups = mockFoodGroups;
const change = new SimpleChange(null, component.preSelectGroups, false);
component.mode = 'multiple';
component.ngOnChanges({ preSelectGroups: change });
const removeGroupSpy = spyOn(component.removeGroup, 'emit');
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
fixture.whenStable();
fixture.detectChanges();
const chips = fixture.debugElement.queryAll(By.css('mat-chip'));
const removeIcon = getElement('[data-automation-id="adf-cloud-group-chip-remove-icon-Mock Group 1"]');
const chipList = fixture.nativeElement.querySelectorAll('mat-chip-list mat-chip');
expect(chipList.length).toBe(2);
expect(chips.length).toBe(2);
expect(component.preSelectGroups[0].readonly).toBe(false, 'Removable');
expect(component.preSelectGroups[1].readonly).toBe(false, 'Removable');
const removeIcon = getElement(`[data-automation-id="adf-cloud-group-chip-remove-icon-${mockMeatChicken.name}"]`);
removeIcon.click();
fixture.detectChanges();
removeIcon.click();
fixture.detectChanges();
expect(removeGroupSpy).toHaveBeenCalled();
expect(fixture.nativeElement.querySelectorAll('mat-chip-list mat-chip').length).toBe(1);
done();
});
});
describe('Component readonly mode', () => {
const change = new SimpleChange(null, mockIdentityGroups, false);
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 });
fixture.detectChanges();
const chips = fixture.debugElement.queryAll(By.css('mat-chip'));
const chipList = getElement('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 = getElement('mat-chip-list');
expect(chips).toBeDefined();
expect(chipList).toBeDefined();
expect(chips.length).toBe(5);
expect(chipList.attributes['ng-reflect-disabled'].value).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(of([]));
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(of(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)
});
});
expect(removeGroupSpy).toHaveBeenCalled();
expect(fixture.nativeElement.querySelectorAll('mat-chip-list mat-chip').length).toBe(1);
});
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 }]);
const duplicatedGroups = [ mockMeatChicken, mockMeatChicken];
expect(component.removeDuplicatedGroups(duplicatedGroups)).toEqual([mockMeatChicken]);
});
});
describe('Preselected groups and validation enabled', () => {
beforeEach(() => {
spyOn(identityGroupService, 'search').and.throwError('Invalid group');
component.validate = true;
component.preSelectGroups = mockFoodGroups;
});
it('should check validation only for the first group and emit warning when group is invalid - single mode', async () => {
component.mode = 'single';
component.ngOnChanges({ preSelectGroups: new SimpleChange(null, [mockVegetableAubergine, mockMeatChicken], false) });
fixture.detectChanges();
await fixture.whenStable();
expect(component.invalidGroups.length).toEqual(1);
});
it('should check validation for all the groups and emit warning - multiple mode', async () => {
component.mode = 'multiple';
component.ngOnChanges({ preSelectGroups: new SimpleChange(null, [mockVegetableAubergine, mockMeatChicken], false) });
fixture.detectChanges();
await fixture.whenStable();
expect(component.invalidGroups.length).toEqual(2);
});
});
describe('Component readonly mode', () => {
const change = new SimpleChange(null, mockFoodGroups, false);
it('should chip list be disabled and show one single chip - single mode', () => {
component.mode = 'single';
component.readOnly = true;
component.preSelectGroups = mockFoodGroups;
component.ngOnChanges({ preSelectGroups: change });
fixture.detectChanges();
const chips = fixture.debugElement.queryAll(By.css('mat-chip'));
const chipList = getElement('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 = mockFoodGroups;
component.ngOnChanges({ preSelectGroups: change });
fixture.detectChanges();
const chips = fixture.debugElement.queryAll(By.css('mat-chip'));
const chipList = getElement('mat-chip-list');
expect(chips).toBeDefined();
expect(chipList).toBeDefined();
expect(chips.length).toBe(2);
expect(chipList.attributes['ng-reflect-disabled']?.value).toEqual('true');
});
});
});

View File

@@ -16,10 +16,16 @@
*/
import { Meta, moduleMetadata, Story } from '@storybook/angular';
import { IdentityGroupService, mockIdentityGroups, IdentityGroupServiceMock } from '@alfresco/adf-core';
import { GroupCloudModule } from '../group-cloud.module';
import { GroupCloudComponent } from './group-cloud.component';
import { ProcessServicesCloudStoryModule } from '../../testing/process-services-cloud-story.module';
import { IdentityGroupService } from '../services/identity-group.service';
import {
IdentityGroupServiceMock,
mockFoodGroups,
mockMeatChicken,
mockVegetableAubergine
} from '../mock/group-cloud.mock';
export default {
component: GroupCloudComponent,
@@ -37,15 +43,6 @@ export default {
mode: {
options: ['single', 'multiple'],
control: 'radio'
},
roles: {
options: ['empty', 'user', 'admin'],
control: 'radio',
mapping: {
empty: [],
user: ['MOCK-USER-ROLE'],
admin: ['MOCK-ADMIN-ROLE']
}
}
}
} as Meta;
@@ -60,7 +57,6 @@ primary.args = {
mode: 'single',
preSelectGroups: [],
readOnly: false,
roles: [],
title: 'Groups',
validate: false
};
@@ -70,7 +66,7 @@ validPreselectedGroups.args = {
...primary.args,
validate: true,
mode: 'multiple',
preSelectGroups: mockIdentityGroups
preSelectGroups: mockFoodGroups
};
export const mandatoryPreselectedGroups = template.bind({});
@@ -78,9 +74,7 @@ mandatoryPreselectedGroups.args = {
...primary.args,
validate: true,
mode: 'multiple',
preSelectGroups: [{ id: 'mock-group-id-1', name: 'Mock Group 1', path: '/mock', subGroups: [], readonly: true },
{ id: 'mock-group-id-2', name: 'Mock Group 2', path: '', subGroups: [] },
{ id: 'mock-group-id-3', name: 'Mock Group 3', path: '', subGroups: [], readonly: true }]
preSelectGroups: [mockVegetableAubergine, { ...mockMeatChicken, readonly: true }]
};
export const invalidPreselectedGroups = template.bind({});
@@ -88,13 +82,7 @@ invalidPreselectedGroups.args = {
...primary.args,
validate: true,
mode: 'multiple',
preSelectGroups: [{ id: 'invalid-group', name: 'invalid groups' }]
};
export const adminRoleGroups = template.bind({});
adminRoleGroups.args = {
...primary.args,
roles: 'admin'
preSelectGroups: [{ id: 'invalid-group', name: 'Invalid Group' }]
};
export const invalidOrEmptyAppName = template.bind({});

View File

@@ -27,14 +27,17 @@ import {
SimpleChanges,
OnChanges,
OnDestroy,
SimpleChange
Inject
} from '@angular/core';
import { FormControl } from '@angular/forms';
import { trigger, state, style, transition, animate } from '@angular/animations';
import { Observable, of, BehaviorSubject, Subject } from 'rxjs';
import { distinctUntilChanged, switchMap, mergeMap, filter, tap, map, takeUntil, debounceTime } from 'rxjs/operators';
import { IdentityGroupModel, IdentityGroupService, LogService } from '@alfresco/adf-core';
import { BehaviorSubject, Observable, Subject } from 'rxjs';
import { distinctUntilChanged, switchMap, mergeMap, filter, tap, takeUntil, debounceTime } from 'rxjs/operators';
import { LogService } from '@alfresco/adf-core';
import { ComponentSelectionMode } from '../../types';
import { IdentityGroupModel } from '../models/identity-group.model';
import { IdentityGroupServiceInterface } from '../services/identity-group.service.interface';
import { IDENTITY_GROUP_SERVICE_TOKEN } from '../services/identity-group-service.token';
@Component({
selector: 'adf-cloud-group',
@@ -125,7 +128,6 @@ export class GroupCloudComponent implements OnInit, OnChanges, OnDestroy {
searchGroups$ = new BehaviorSubject<IdentityGroupModel[]>(this.searchGroups);
subscriptAnimationState: string = 'enter';
clientId: string;
isFocused: boolean;
touched: boolean = false;
@@ -135,12 +137,14 @@ export class GroupCloudComponent implements OnInit, OnChanges, OnDestroy {
validationLoading = false;
searchLoading = false;
typingUniqueValueNotEmpty$: Observable<any>;
constructor(
private identityGroupService: IdentityGroupService,
@Inject(IDENTITY_GROUP_SERVICE_TOKEN)
private identityGroupService: IdentityGroupServiceInterface,
private logService: LogService) {}
ngOnInit(): void {
this.loadClientId();
this.initSearch();
}
@@ -157,41 +161,52 @@ export class GroupCloudComponent implements OnInit, OnChanges, OnDestroy {
this.invalidGroups = [];
}
}
if (changes.appName && this.isAppNameChanged(changes.appName)) {
this.loadClientId();
}
}
private isAppNameChanged(change: SimpleChange): boolean {
return change
&& change.previousValue !== change.currentValue
&& this.appName
&& this.appName.length > 0;
private initSearch(): void {
this.initializeStream();
this.typingUniqueValueNotEmpty$.pipe(
switchMap((name: string) =>
this.identityGroupService.search(name, { roles: this.roles, withinApplication: this.appName })
),
mergeMap((groups: IdentityGroupModel[]) => {
this.resetSearchGroups();
this.searchLoading = false;
return groups;
}),
filter(group => !this.isGroupAlreadySelected(group)),
takeUntil(this.onDestroy$)
).subscribe((searchedGroup: IdentityGroupModel) => {
this.searchGroups.push(searchedGroup);
this.searchGroups$.next(this.searchGroups);
});
}
private async loadClientId(): Promise<void> {
this.clientId = await this.identityGroupService.getClientIdByApplicationName(this.appName).toPromise();
private initializeStream() {
const typingValueFromControl$ = this.searchGroupsControl.valueChanges;
if (this.clientId) {
this.searchGroupsControl.enable();
}
}
initSearch(): void {
this.searchGroupsControl.valueChanges.pipe(
filter((value) => {
const typingValueTypeSting$ = typingValueFromControl$.pipe(
filter(value => {
this.searchLoading = true;
return typeof value === 'string';
}),
})
);
const typingValueHandleErrorMessage$ = typingValueTypeSting$.pipe(
tap((value: string) => {
if (value) {
this.setTypingError();
}
}),
})
);
const typingValueDebouncedUnique$ = typingValueHandleErrorMessage$.pipe(
debounceTime(500),
distinctUntilChanged(),
tap((value) => {
distinctUntilChanged()
);
this.typingUniqueValueNotEmpty$ = typingValueDebouncedUnique$.pipe(
tap((value: string) => {
if (value.trim()) {
this.searchedValue = value;
} else {
@@ -199,42 +214,8 @@ export class GroupCloudComponent implements OnInit, OnChanges, OnDestroy {
this.searchGroupsControl.markAsUntouched();
}
}),
tap(() => this.resetSearchGroups()),
switchMap((name: string) =>
this.identityGroupService.findGroupsByName({ name: name.trim() })
),
mergeMap((groups) => {
this.resetSearchGroups();
this.searchLoading = false;
return groups;
}),
filter(group => !this.isGroupAlreadySelected(group)),
mergeMap(group => {
if (this.appName) {
return this.checkGroupHasAccess(group.id).pipe(
mergeMap(
hasRole => hasRole ? of(group) : of()
)
);
} else if (this.hasRoles()) {
return this.filterGroupsByRoles(group);
} else {
return of(group);
}
}),
takeUntil(this.onDestroy$)
).subscribe(searchedGroup => {
this.searchGroups.push(searchedGroup);
this.searchGroups$.next(this.searchGroups);
});
}
checkGroupHasAccess(groupId: string): Observable<boolean> {
if (this.hasRoles()) {
return this.identityGroupService.checkGroupHasAnyClientAppRole(groupId, this.clientId, this.roles);
} else {
return this.identityGroupService.checkGroupHasClientApp(groupId, this.clientId);
}
tap(() => this.resetSearchGroups())
);
}
private isGroupAlreadySelected(group: IdentityGroupModel): boolean {
@@ -246,8 +227,8 @@ export class GroupCloudComponent implements OnInit, OnChanges, OnDestroy {
return false;
}
async searchGroup(name: string): Promise<IdentityGroupModel> {
return (await this.identityGroupService.findGroupsByName({ name }).toPromise())[0];
private async searchGroup(name: string): Promise<IdentityGroupModel> {
return (await this.identityGroupService.search(name).toPromise())[0];
}
private getPreselectedGroups(): IdentityGroupModel[] {
@@ -258,7 +239,7 @@ export class GroupCloudComponent implements OnInit, OnChanges, OnDestroy {
}
}
async validatePreselectGroups(): Promise<any> {
private async validatePreselectGroups(): Promise<any> {
this.invalidGroups = [];
for (const group of this.getPreselectedGroups()) {
@@ -276,7 +257,7 @@ export class GroupCloudComponent implements OnInit, OnChanges, OnDestroy {
this.checkPreselectValidationErrors();
}
checkPreselectValidationErrors(): void {
private checkPreselectValidationErrors(): void {
this.invalidGroups = this.removeDuplicatedGroups(this.invalidGroups);
if (this.invalidGroups.length > 0) {
@@ -289,7 +270,7 @@ export class GroupCloudComponent implements OnInit, OnChanges, OnDestroy {
});
}
generateInvalidGroupsMessage(): void {
private generateInvalidGroupsMessage(): void {
this.validateGroupsMessage = '';
this.invalidGroups.forEach((invalidGroup: IdentityGroupModel, index) => {
@@ -317,13 +298,6 @@ export class GroupCloudComponent implements OnInit, OnChanges, OnDestroy {
}
}
filterGroupsByRoles(group: IdentityGroupModel): Observable<IdentityGroupModel> {
return this.identityGroupService.checkGroupHasRole(group.id, this.roles).pipe(
map((hasRole: boolean) => ({ hasRole, group })),
filter((filteredGroup: { hasRole: boolean; group: IdentityGroupModel }) => filteredGroup.hasRole),
map((filteredGroup: { hasRole: boolean; group: IdentityGroupModel }) => filteredGroup.group));
}
onSelect(group: IdentityGroupModel): void {
if (group) {
this.selectGroup.emit(group);
@@ -365,6 +339,19 @@ export class GroupCloudComponent implements OnInit, OnChanges, OnDestroy {
}
}
private isPreselectedGroupInvalid(preselectedGroup: IdentityGroupModel, validatedGroup: IdentityGroupModel): boolean {
if (validatedGroup && validatedGroup.name !== undefined) {
return preselectedGroup.name !== validatedGroup.name;
} else {
return true;
}
}
removeDuplicatedGroups(groups: IdentityGroupModel[]): IdentityGroupModel[] {
return groups.filter((group, index, self) =>
index === self.findIndex((auxGroup) => group.id === auxGroup.id && group.name === auxGroup.name));
}
private groupChipsCtrlValue(value: string) {
this.groupChipsCtrl.setValue(value);
this.groupChipsCtrl.markAsDirty();
@@ -392,43 +379,18 @@ export class GroupCloudComponent implements OnInit, OnChanges, OnDestroy {
this.searchGroups$.next(this.searchGroups);
}
isPreselectedGroupInvalid(preselectedGroup: IdentityGroupModel, validatedGroup: IdentityGroupModel): boolean {
if (validatedGroup && validatedGroup.name !== undefined) {
return preselectedGroup.name !== validatedGroup.name;
} else {
return true;
}
}
isSingleMode(): boolean {
return this.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;
private isSingleMode(): boolean {
return this.mode === 'single';
}
isReadonly(): boolean {
return this.readOnly || this.isSingleSelectionReadonly();
}
isMultipleMode(): boolean {
private isMultipleMode(): boolean {
return this.mode === 'multiple';
}
getDisplayName(group: IdentityGroupModel): string {
return group ? group.name : '';
}
removeDuplicatedGroups(groups: IdentityGroupModel[]): IdentityGroupModel[] {
return groups.filter((group, index, self) =>
index === self.findIndex((auxGroup) => group.id === auxGroup.id && group.name === auxGroup.name));
}
private hasPreSelectGroups(): boolean {
return this.preSelectGroups && this.preSelectGroups.length > 0;
}
@@ -457,10 +419,6 @@ export class GroupCloudComponent implements OnInit, OnChanges, OnDestroy {
&& changes.preSelectGroups.currentValue.length === 0;
}
private hasRoles(): boolean {
return this.roles && this.roles.length > 0;
}
private setTypingError(): void {
this.searchGroupsControl.setErrors({
searchTypingError: true,
@@ -468,6 +426,18 @@ export class GroupCloudComponent implements OnInit, OnChanges, OnDestroy {
});
}
hasPreselectError(): boolean {
return this.invalidGroups && this.invalidGroups.length > 0;
}
isReadonly(): boolean {
return this.readOnly || this.isSingleSelectionReadonly();
}
getDisplayName(group: IdentityGroupModel): string {
return group ? group.name : '';
}
hasError(): boolean {
return !!this.searchGroupsControl.errors;
}

View File

@@ -24,6 +24,8 @@ import { CoreModule } from '@alfresco/adf-core';
import { MaterialModule } from '../material.module';
import { GroupCloudComponent } from './components/group-cloud.component';
import { InitialGroupNamePipe } from './pipe/group-initial.pipe';
import { IDENTITY_GROUP_SERVICE_TOKEN } from './services/identity-group-service.token';
import { IdentityGroupService } from './services/identity-group.service';
@NgModule({
imports: [
@@ -35,6 +37,9 @@ import { InitialGroupNamePipe } from './pipe/group-initial.pipe';
CoreModule
],
declarations: [GroupCloudComponent, InitialGroupNamePipe],
providers: [
{ provide: IDENTITY_GROUP_SERVICE_TOKEN, useExisting: IdentityGroupService }
],
exports: [GroupCloudComponent, InitialGroupNamePipe]
})
export class GroupCloudModule { }

View File

@@ -0,0 +1,47 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Injectable } from '@angular/core';
import { Observable, EMPTY, of } from 'rxjs';
import { IdentityGroupModel } from '../models/identity-group.model';
import { IdentityGroupFilterInterface } from '../services/identity-group-filter.interface';
import { IdentityGroupServiceInterface } from '../services/identity-group.service.interface';
export const mockVegetableAubergine: IdentityGroupModel = { id: 'aubergine', name: 'Vegetable Aubergine'};
export const mockMeatChicken: IdentityGroupModel = { id: 'chicken', name: 'Meat Chicken'};
export const mockFoodGroups = [ mockVegetableAubergine, mockMeatChicken ];
export const mockSearchGroupEmptyFilters: IdentityGroupFilterInterface = {
roles: [],
withinApplication: ''
};
@Injectable({
providedIn: 'root'
})
export class IdentityGroupServiceMock implements IdentityGroupServiceInterface {
search(name: string, _filters?: IdentityGroupFilterInterface): Observable<IdentityGroupModel[]> {
if (name.trim() === '') {
return EMPTY;
}
return of(mockFoodGroups.filter(group =>
group.name.toUpperCase().includes(name.toUpperCase())
));
}
}

View File

@@ -0,0 +1,57 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { HttpErrorResponse } from '@angular/common/http';
import { throwError } from 'rxjs';
import { IdentityGroupModel } from '../models/identity-group.model';
import { IdentityGroupFilterInterface } from '../services/identity-group-filter.interface';
export const mockSearchGroupByRoles: IdentityGroupFilterInterface = {
roles: ['fake-role-1', 'fake-role-2'],
withinApplication: ''
};
export const mockSearchGroupByRolesAndApp: IdentityGroupFilterInterface = {
roles: ['fake-role-1', 'fake-role-2'],
withinApplication: 'fake-app-name'
};
export const mockSearchGroupByApp: IdentityGroupFilterInterface = {
roles: [],
withinApplication: 'fake-app-name'
};
export function oAuthMockApiWithIdentityGroups(groups: IdentityGroupModel[]) {
return {
oauth2Auth: {
callCustomApi: () => Promise.resolve(groups)
},
reply: jasmine.createSpy('reply')
};
}
const errorResponse = new HttpErrorResponse({
error: 'Mock Error',
status: 404, statusText: 'Not Found'
});
export const oAuthMockApiWithError = {
oauth2Auth: {
callCustomApi: () => throwError(errorResponse)
},
reply: jasmine.createSpy('reply')
};

View File

@@ -0,0 +1,22 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export interface IdentityGroupModel {
id?: string;
name: string;
readonly?: boolean;
}

View File

@@ -15,8 +15,8 @@
* limitations under the License.
*/
import { IdentityGroupModel } from '../models/identity-group.model';
import { InitialGroupNamePipe } from './group-initial.pipe';
import { IdentityGroupModel } from '@alfresco/adf-core';
describe('InitialGroupNamePipe', () => {

View File

@@ -16,7 +16,7 @@
*/
import { Pipe, PipeTransform } from '@angular/core';
import { IdentityGroupModel } from '@alfresco/adf-core';
import { IdentityGroupModel } from '../models/identity-group.model';
@Pipe({
name: 'groupNameInitial'

View File

@@ -17,4 +17,7 @@
export * from './components/group-cloud.component';
export * from './pipe/group-initial.pipe';
export * from './models/identity-group.model';
export * from './group-cloud.module';
export * from './services/identity-group.service';
export * from './services/identity-group-service.token';

View File

@@ -0,0 +1,21 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export interface IdentityGroupFilterInterface {
roles?: string[];
withinApplication?: string;
}

View File

@@ -0,0 +1,21 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { InjectionToken } from '@angular/core';
import { IdentityGroupServiceInterface } from './identity-group.service.interface';
export const IDENTITY_GROUP_SERVICE_TOKEN = new InjectionToken<IdentityGroupServiceInterface>('IdentityGroup');

View File

@@ -0,0 +1,24 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Observable } from 'rxjs';
import { IdentityGroupModel } from '../models/identity-group.model';
import { IdentityGroupFilterInterface } from './identity-group-filter.interface';
export interface IdentityGroupServiceInterface {
search(name: string, filters?: IdentityGroupFilterInterface): Observable<IdentityGroupModel[]>;
}

View File

@@ -0,0 +1,181 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { TestBed } from '@angular/core/testing';
import { TranslateModule } from '@ngx-translate/core';
import { AlfrescoApiService, setupTestBed } from '@alfresco/adf-core';
import { ProcessServiceCloudTestingModule } from '../../testing/process-service-cloud.testing.module';
import { IdentityGroupService } from './identity-group.service';
import {
mockSearchGroupByApp,
mockSearchGroupByRoles,
mockSearchGroupByRolesAndApp,
oAuthMockApiWithError,
oAuthMockApiWithIdentityGroups
} from '../mock/identity-group.service.mock';
import { mockFoodGroups } from '../mock/group-cloud.mock';
describe('IdentityGroupService', () => {
let service: IdentityGroupService;
let alfrescoApiService: AlfrescoApiService;
setupTestBed({
imports: [
TranslateModule.forRoot(),
ProcessServiceCloudTestingModule
]
});
beforeEach(() => {
service = TestBed.inject(IdentityGroupService);
alfrescoApiService = TestBed.inject(AlfrescoApiService);
});
describe('Search', () => {
it('should fetch groups', (done) => {
spyOn(alfrescoApiService, 'getInstance').and.returnValue(oAuthMockApiWithIdentityGroups(mockFoodGroups));
const searchSpy = spyOn(service, 'search').and.callThrough();
service.search('fake').subscribe(
res => {
expect(res).toBeDefined();
expect(searchSpy).toHaveBeenCalled();
expect(service.queryParams).toEqual({
search: 'fake'
});
done();
}
);
});
it('should not fetch groups if error occurred', (done) => {
spyOn(alfrescoApiService, 'getInstance').and.returnValue(oAuthMockApiWithError);
const searchSpy = spyOn(service, 'search').and.callThrough();
service.search('fake')
.subscribe(
() => {
fail('expected an error, not groups');
},
(error) => {
expect(searchSpy).toHaveBeenCalled();
expect(error.status).toEqual(404);
expect(error.statusText).toEqual('Not Found');
expect(error.error).toEqual('Mock Error');
done();
}
);
});
it('should fetch groups by roles', (done) => {
spyOn(alfrescoApiService, 'getInstance').and.returnValue(oAuthMockApiWithIdentityGroups(mockFoodGroups));
const searchSpy = spyOn(service, 'search').and.callThrough();
service.search('fake', mockSearchGroupByRoles).subscribe(
res => {
expect(res).toBeDefined();
expect(searchSpy).toHaveBeenCalled();
expect(service.queryParams).toEqual({
search: 'fake',
role: 'fake-role-1,fake-role-2'
});
done();
}
);
});
it('should not fetch groups by roles if error occurred', (done) => {
spyOn(alfrescoApiService, 'getInstance').and.returnValue(oAuthMockApiWithError);
const searchSpy = spyOn(service, 'search').and.callThrough();
service.search('fake', mockSearchGroupByRoles)
.subscribe(
() => {
fail('expected an error, not groups');
},
(error) => {
expect(searchSpy).toHaveBeenCalled();
expect(service.queryParams).toEqual({
search: 'fake',
role: 'fake-role-1,fake-role-2'
});
expect(error.status).toEqual(404);
expect(error.statusText).toEqual('Not Found');
expect(error.error).toEqual('Mock Error');
done();
}
);
});
it('should fetch groups within app', (done) => {
spyOn(alfrescoApiService, 'getInstance').and.returnValue(oAuthMockApiWithIdentityGroups(mockFoodGroups));
service.search('fake', mockSearchGroupByApp).subscribe(
res => {
expect(res).toBeDefined();
expect(service.queryParams).toEqual({
search: 'fake',
application: 'fake-app-name'
});
done();
}
);
});
it('should fetch groups within app with roles', (done) => {
spyOn(alfrescoApiService, 'getInstance').and.returnValue(oAuthMockApiWithIdentityGroups(mockFoodGroups));
service.search('fake', mockSearchGroupByRolesAndApp).subscribe(
res => {
expect(res).toBeDefined();
expect(service.queryParams).toEqual({
search: 'fake',
application: 'fake-app-name',
role: 'fake-role-1,fake-role-2'
});
done();
}
);
});
it('should not fetch groups within app if error occurred', (done) => {
spyOn(alfrescoApiService, 'getInstance').and.returnValue(oAuthMockApiWithError);
const searchSpy = spyOn(service, 'search').and.callThrough();
service.search('fake', mockSearchGroupByApp)
.subscribe(
() => {
fail('expected an error, not groups');
},
(error) => {
expect(searchSpy).toHaveBeenCalled();
expect(service.queryParams).toEqual({
search: 'fake',
application: 'fake-app-name'
});
expect(error.status).toEqual(404);
expect(error.statusText).toEqual('Not Found');
expect(error.error).toEqual('Mock Error');
done();
}
);
});
});
});

View File

@@ -0,0 +1,111 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Injectable } from '@angular/core';
import { AppConfigService, OAuth2Service } from '@alfresco/adf-core';
import { EMPTY, Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { IdentityGroupServiceInterface } from './identity-group.service.interface';
import { IdentityGroupFilterInterface } from './identity-group-filter.interface';
import { IdentityGroupModel } from '../models/identity-group.model';
const IDENTITY_MICRO_SERVICE_INGRESS = 'modeling-service';
@Injectable({ providedIn: 'root' })
export class IdentityGroupService implements IdentityGroupServiceInterface {
queryParams: { search: string; application?: string; roles?: string [] };
constructor(
private oAuth2Service: OAuth2Service,
private appConfigService: AppConfigService
) {}
public search(name: string, filters?: IdentityGroupFilterInterface): Observable<IdentityGroupModel[]> {
if (name.trim() === '') {
return EMPTY;
} else if (filters?.withinApplication) {
return this.searchGroupsWithinApp(name, filters.withinApplication, filters?.roles);
} else if (filters?.roles?.length > 0) {
return this.searchGroupsWithGlobalRoles(name, filters.roles);
} else {
return this.searchGroupsByName(name);
}
}
private searchGroupsByName(name: string): Observable<IdentityGroupModel[]> {
this.buildQueryParam(name);
return this.invokeIdentityGroupApi().pipe(
catchError((err) => this.handleError(err))
);
}
private searchGroupsWithGlobalRoles(name: string, roles: string []): Observable<IdentityGroupModel[]> {
this.buildQueryParam(name, roles);
return this.invokeIdentityGroupApi().pipe(
catchError((err) => this.handleError(err))
);
}
private searchGroupsWithinApp(name: string, applicationName: string, roles?: string []): Observable<IdentityGroupModel[]> {
this.buildQueryParam(name, roles, applicationName);
return this.invokeIdentityGroupApi().pipe(
catchError((err) => this.handleError(err))
);
}
private invokeIdentityGroupApi(): Observable<IdentityGroupModel[]> {
const url = `${this.identityHost}/${IDENTITY_MICRO_SERVICE_INGRESS}/v1/identity/groups`;
return this.oAuth2Service.get({ url, queryParams: this.queryParams });
}
private buildQueryParam(name: string, roles?: string [], applicationName?: string) {
this.queryParams = { search: name };
this.addOptionalValueToQueryParam('application', applicationName);
this.addOptionalCommaValueToQueryParam('role', roles);
}
private addOptionalCommaValueToQueryParam(key: string, values: string []) {
if (values?.length > 0) {
const valuesNotEmpty = this.filterOutEmptyValue(values);
if (valuesNotEmpty?.length > 0) {
this.queryParams[key] = valuesNotEmpty.join(',');
}
}
}
private addOptionalValueToQueryParam(key: string, value: string) {
if (value?.trim()) {
this.queryParams[key] = value;
}
}
private filterOutEmptyValue(roles: string []): string [] {
return roles.filter( role => role.trim() ? true : false);
}
private handleError(error: any) {
return throwError(error || 'Server error');
}
private get identityHost(): string {
return `${this.appConfigService.get('bpmHost')}`;
}
}

View File

@@ -16,7 +16,8 @@
*/
import { Pagination } from '@alfresco/js-api';
import { IdentityGroupModel, IdentityUserModel } from '@alfresco/adf-core';
import { IdentityGroupModel } from '../group/models/identity-group.model';
import { IdentityUserModel } from '../people/models/identity-user.model';
import { ProcessInstanceVariable } from './process-instance-variable.model';
export class TaskCloudNodePaging {

View File

@@ -16,10 +16,17 @@
*/
import { Meta, moduleMetadata, Story } from '@storybook/angular';
import { IdentityUserService, IdentityUserServiceMock, mockIdentityUsers } from '@alfresco/adf-core';
import { PeopleCloudComponent } from './people-cloud.component';
import { PeopleCloudModule } from '../people-cloud.module';
import { ProcessServicesCloudStoryModule } from '../../testing/process-services-cloud-story.module';
import { IdentityUserService } from '../services/identity-user.service';
import {
IdentityUserServiceMock,
mockFoodUsers,
mockKielbasaSausage,
mockShepherdsPie,
mockYorkshirePudding
} from '../mock/people-cloud.mock';
export default {
component: PeopleCloudComponent,
@@ -28,7 +35,7 @@ export default {
moduleMetadata({
imports: [ProcessServicesCloudStoryModule, PeopleCloudModule],
providers: [
{ provide: IdentityUserService, useClass: IdentityUserServiceMock }
{ provide: IdentityUserService, useClass: IdentityUserServiceMock}
]
})
],
@@ -37,15 +44,6 @@ export default {
mode: {
options: ['single', 'multiple'],
control: 'radio'
},
roles: {
options: ['empty', 'user', 'admin'],
control: 'radio',
mapping: {
empty: [],
user: ['MOCK-USER-ROLE'],
admin: ['MOCK-ADMIN-ROLE']
}
}
}
} as Meta;
@@ -61,7 +59,6 @@ primary.args = {
mode: 'single',
preSelectUsers: [],
readOnly: false,
roles: [],
title: 'Users',
validate: false
};
@@ -71,7 +68,7 @@ validPreselectedUsers.args = {
...primary.args,
validate: true,
mode: 'multiple',
preSelectUsers: mockIdentityUsers
preSelectUsers: mockFoodUsers
};
export const mandatoryPreselectedUsers = template.bind({});
@@ -79,8 +76,7 @@ mandatoryPreselectedUsers.args = {
...primary.args,
validate: true,
mode: 'multiple',
preSelectUsers: [{ id: 'mock-user-id-1', username: 'userName1', firstName: 'first-name-1', lastName: 'last-name-1', email: 'abc@xyz.com', readonly: true },
{ id: 'mock-user-id-2', username: 'userName2', firstName: 'first-name-2', lastName: 'last-name-2', email: 'abcd@xyz.com' }]
preSelectUsers: [{ ...mockKielbasaSausage, readonly: true }, mockShepherdsPie]
};
export const invalidPreselectedUsers = template.bind({});
@@ -88,28 +84,22 @@ invalidPreselectedUsers.args = {
...primary.args,
validate: true,
mode: 'multiple',
preSelectUsers: [{ id: 'invalid-user', username: 'invalid user', firstName: 'invalid', lastName: 'user', email: 'invalid@xyz.com' }]
preSelectUsers: [{ id: 'invalid-user', username: 'Invalid User', firstName: 'Invalid', lastName: 'User', email: 'invalid@xyz.com' }]
};
export const excludedUsers = template.bind({});
excludedUsers.args = {
...primary.args,
excludedUsers: [
{ id: 'mock-user-id-2' },
{ id: 'mock-user-id-3' }
mockKielbasaSausage,
mockYorkshirePudding
]
};
export const adminRoleUser = template.bind({});
adminRoleUser.args = {
...primary.args,
roles: 'admin'
};
export const noUsers = template.bind({});
noUsers.args = {
...primary.args,
excludedUsers: mockIdentityUsers
excludedUsers: mockFoodUsers
};
export const invalidOrEmptyAppName = template.bind({});

View File

@@ -26,18 +26,21 @@ import {
SimpleChanges,
OnChanges,
OnDestroy,
ViewChild, ElementRef, SimpleChange
ViewChild,
ElementRef,
Inject
} from '@angular/core';
import { Observable, of, BehaviorSubject, Subject } from 'rxjs';
import { switchMap, debounceTime, distinctUntilChanged, mergeMap, tap, filter, map, takeUntil } from 'rxjs/operators';
import { BehaviorSubject, Observable, Subject } from 'rxjs';
import { switchMap, debounceTime, distinctUntilChanged, mergeMap, tap, filter, takeUntil } from 'rxjs/operators';
import {
FullNamePipe,
IdentityUserModel,
IdentityUserService,
LogService
} from '@alfresco/adf-core';
import { trigger, state, style, transition, animate } from '@angular/animations';
import { ComponentSelectionMode } from '../../types';
import { IdentityUserModel } from '../models/identity-user.model';
import { IdentityUserServiceInterface } from '../services/identity-user.service.interface';
import { IDENTITY_USER_SERVICE_TOKEN } from '../services/identity-user-service.token';
@Component({
selector: 'adf-cloud-people',
@@ -139,15 +142,14 @@ export class PeopleCloudComponent implements OnInit, OnChanges, OnDestroy {
@ViewChild('userInput')
private userInput: ElementRef<HTMLInputElement>;
private _searchUsers: IdentityUserModel[] = [];
private searchUsers: IdentityUserModel[] = [];
private onDestroy$ = new Subject<boolean>();
selectedUsers: IdentityUserModel[] = [];
invalidUsers: IdentityUserModel[] = [];
searchUsers$ = new BehaviorSubject<IdentityUserModel[]>(this._searchUsers);
searchUsers$ = new BehaviorSubject<IdentityUserModel[]>(this.searchUsers);
subscriptAnimationState: string = 'enter';
clientId: string;
isFocused: boolean;
touched: boolean = false;
@@ -157,20 +159,19 @@ export class PeopleCloudComponent implements OnInit, OnChanges, OnDestroy {
validationLoading = false;
searchLoading = false;
typingUniqueValueNotEmpty$: Observable<string>;
constructor(
private identityUserService: IdentityUserService,
@Inject(IDENTITY_USER_SERVICE_TOKEN)
private identityUserService: IdentityUserServiceInterface,
private logService: LogService) {}
ngOnInit(): void {
this.loadClientId();
this.initSearch();
}
ngOnChanges(changes: SimpleChanges): void {
if (this.valueChanged(changes.preSelectUsers)
|| this.valueChanged(changes.mode)
|| this.valueChanged(changes.validate)
) {
if (this.hasPreselectedUsersChanged(changes) || this.hasModeChanged(changes) || this.isValidationChanged(changes)) {
if (this.hasPreSelectUsers()) {
this.loadPreSelectUsers();
} else if (this.hasPreselectedUsersCleared(changes)) {
@@ -182,32 +183,51 @@ export class PeopleCloudComponent implements OnInit, OnChanges, OnDestroy {
this.invalidUsers = [];
}
}
if (changes.appName && this.isAppNameChanged(changes.appName)) {
this.loadClientId();
}
}
private async loadClientId(): Promise<void> {
this.clientId = await this.identityUserService.getClientIdByApplicationName(this.appName).toPromise();
if (this.clientId) {
this.searchUserCtrl.enable();
}
}
private initSearch(): void {
this.searchUserCtrl.valueChanges.pipe(
filter((value) => {
this.initializeStream();
this.typingUniqueValueNotEmpty$.pipe(
switchMap((name: string) =>
this.identityUserService.search(name, { roles: this.roles, withinApplication: this.appName, groups: this.groupsRestriction })
),
mergeMap((users: IdentityUserModel[]) => {
this.resetSearchUsers();
this.searchLoading = false;
return users;
}),
filter(user => !this.isUserAlreadySelected(user) && !this.isExcludedUser(user)),
takeUntil(this.onDestroy$)
).subscribe((user: IdentityUserModel) => {
this.searchUsers.push(user);
this.searchUsers$.next(this.searchUsers);
});
}
private initializeStream() {
const typingValueFromControl$ = this.searchUserCtrl.valueChanges;
const typingValueTypeSting$ = typingValueFromControl$.pipe(
filter(value => {
this.searchLoading = true;
return typeof value === 'string';
}),
})
);
const typingValueHandleErrorMessage$ = typingValueTypeSting$.pipe(
tap((value: string) => {
if (value) {
this.setTypingError();
}
}),
})
);
const typingValueDebouncedUnique$ = typingValueHandleErrorMessage$.pipe(
debounceTime(500),
distinctUntilChanged(),
distinctUntilChanged()
);
this.typingUniqueValueNotEmpty$ = typingValueDebouncedUnique$.pipe(
tap((value: string) => {
if (value.trim()) {
this.searchedValue = value;
@@ -216,87 +236,17 @@ export class PeopleCloudComponent implements OnInit, OnChanges, OnDestroy {
this.searchUserCtrl.markAsUntouched();
}
}),
tap(() => {
this.resetSearchUsers();
}),
switchMap((search) =>
this.findUsers(search)
),
mergeMap((users) => {
this.resetSearchUsers();
this.searchLoading = false;
return users;
}),
filter(user => !this.isUserAlreadySelected(user) && !this.isExcludedUser(user)),
mergeMap(user => this.filterUsersByGroupsRestriction(user)),
mergeMap(user => {
if (this.appName) {
return this.checkUserHasAccess(user.id).pipe(
mergeMap(
hasRole => hasRole ? of(user) : of()
)
);
} else if (this.hasRoles()) {
return this.filterUsersByRoles(user);
} else {
return of(user);
}
}),
takeUntil(this.onDestroy$)
).subscribe(user => {
this._searchUsers.push(user);
this.searchUsers$.next(this._searchUsers);
});
tap(() => this.resetSearchUsers())
);
}
ngOnDestroy(): void {
this.onDestroy$.next(true);
this.onDestroy$.complete();
}
private isAppNameChanged(change: SimpleChange): boolean {
return change && change.previousValue !== change.currentValue && this.appName && this.appName.length > 0;
}
isValidationEnabled(): boolean {
private isValidationEnabled(): boolean {
return this.validate === true;
}
private checkUserHasAccess(userId: string): Observable<boolean> {
if (this.hasRoles()) {
return this.identityUserService.checkUserHasAnyClientAppRole(userId, this.clientId, this.roles);
} else {
return this.identityUserService.checkUserHasClientApp(userId, this.clientId);
}
}
private hasRoles(): boolean {
return this.roles && this.roles.length > 0;
}
filterUsersByRoles(user: IdentityUserModel): Observable<IdentityUserModel> {
return this.identityUserService.checkUserHasRole(user.id, this.roles).pipe(
map((hasRole: boolean) => ({ hasRole, user })),
filter((filteredUser: { hasRole: boolean; user: IdentityUserModel }) => filteredUser.hasRole),
map((filteredUser: { hasRole: boolean; user: IdentityUserModel }) => filteredUser.user));
}
private filterUsersByGroupsRestriction(user: IdentityUserModel): Observable<IdentityUserModel> {
if (this.groupsRestriction?.length) {
return this.isUserPartOfAllRestrictedGroups(user).pipe(
mergeMap(isPartOfAllGroups => isPartOfAllGroups ? of(user) : of())
);
}
return of(user);
}
private findUsers(search: string): Observable<IdentityUserModel[]> {
return this.identityUserService.findUsersByName(search.trim());
}
private isUserAlreadySelected(searchUser: IdentityUserModel): boolean {
if (this.selectedUsers && this.selectedUsers.length > 0) {
const result = this.selectedUsers.find((selectedUser) => this.compare(selectedUser, searchUser));
const result = this.selectedUsers.find((selectedUser) => this.equalsUsers(selectedUser, searchUser));
return !!result;
}
@@ -305,7 +255,7 @@ export class PeopleCloudComponent implements OnInit, OnChanges, OnDestroy {
private isExcludedUser(searchUser: IdentityUserModel): boolean {
if (this.excludedUsers?.length > 0) {
return !!this.excludedUsers.find(excludedUser => this.compare(excludedUser, searchUser));
return !!this.excludedUsers.find(excludedUser => this.equalsUsers(excludedUser, searchUser));
}
return false;
}
@@ -334,28 +284,14 @@ export class PeopleCloudComponent implements OnInit, OnChanges, OnDestroy {
}
}
async validatePreselectUsers(): Promise<any> {
private async validatePreselectUsers(): Promise<any> {
this.invalidUsers = [];
const validUsers: IdentityUserModel[] = [];
for (const user of this.getPreselectedUsers()) {
try {
const validationResult = await this.searchUser(user);
const validationResult = (await this.identityUserService.search(user.username, { roles: this.roles, withinApplication: this.appName, groups: this.groupsRestriction }).toPromise())[0];
if (this.compare(user, validationResult)) {
validationResult.readonly = user.readonly;
if (this.groupsRestriction?.length) {
const isUserPartOfAllRestrictedGroups = await this.isUserPartOfAllRestrictedGroups(validationResult).toPromise();
if (isUserPartOfAllRestrictedGroups) {
validUsers.push(user);
} else {
this.invalidUsers.push(user);
}
} else {
validUsers.push(validationResult);
}
} else {
if (!this.equalsUsers(user, validationResult)) {
this.invalidUsers.push(user);
}
} catch (error) {
@@ -365,10 +301,9 @@ export class PeopleCloudComponent implements OnInit, OnChanges, OnDestroy {
}
this.checkPreselectValidationErrors();
this.selectedUsers = validUsers.concat(this.invalidUsers);
}
compare(preselectedUser: IdentityUserModel, identityUser: IdentityUserModel): boolean {
equalsUsers(preselectedUser: IdentityUserModel, identityUser: IdentityUserModel): boolean {
if (preselectedUser && identityUser) {
const uniquePropertyIdentifiers = ['id', 'username', 'email'];
for (const property of Object.keys(preselectedUser)) {
@@ -380,33 +315,6 @@ export class PeopleCloudComponent implements OnInit, OnChanges, OnDestroy {
return false;
}
private getSearchKey(user: IdentityUserModel): string {
if (user.id) {
return 'id';
} else if (user.email) {
return 'email';
} else if (user.username) {
return 'username';
} else {
return null;
}
}
async searchUser(user: IdentityUserModel): Promise<IdentityUserModel> {
const key = this.getSearchKey(user);
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 null;
}
}
removeDuplicatedUsers(users: IdentityUserModel[]): IdentityUserModel[] {
return users.filter((user, index, self) =>
index === self.findIndex(auxUser =>
@@ -414,19 +322,6 @@ export class PeopleCloudComponent implements OnInit, OnChanges, OnDestroy {
));
}
checkPreselectValidationErrors(): void {
this.invalidUsers = this.removeDuplicatedUsers(this.invalidUsers);
if (this.invalidUsers.length > 0) {
this.generateInvalidUsersMessage();
}
this.warning.emit({
message: 'INVALID_PRESELECTED_USERS',
users: this.invalidUsers
});
}
onSelect(user: IdentityUserModel): void {
if (user) {
this.selectUser.emit(user);
@@ -442,7 +337,7 @@ export class PeopleCloudComponent implements OnInit, OnChanges, OnDestroy {
this.userInput.nativeElement.value = '';
this.searchUserCtrl.setValue('');
this.userChipsCtrlValue(this.selectedUsers[0].username);
this.userChipsControlValue(this.selectedUsers[0].username);
this.changedUsers.emit(this.selectedUsers);
this.resetSearchUsers();
@@ -454,10 +349,10 @@ export class PeopleCloudComponent implements OnInit, OnChanges, OnDestroy {
this.removeUserFromSelected(userToRemove);
this.changedUsers.emit(this.selectedUsers);
if (this.selectedUsers.length === 0) {
this.userChipsCtrlValue('');
this.userChipsControlValue('');
} else {
this.userChipsCtrlValue(this.selectedUsers[0].username);
this.userChipsControlValue(this.selectedUsers[0].username);
}
this.searchUserCtrl.markAsDirty();
this.searchUserCtrl.markAsTouched();
@@ -468,10 +363,17 @@ export class PeopleCloudComponent implements OnInit, OnChanges, OnDestroy {
}
}
private userChipsCtrlValue(value: string) {
this.userChipsCtrl.setValue(value);
this.userChipsCtrl.markAsDirty();
this.userChipsCtrl.markAsTouched();
private checkPreselectValidationErrors(): void {
this.invalidUsers = this.removeDuplicatedUsers(this.invalidUsers);
if (this.invalidUsers.length > 0) {
this.generateInvalidUsersMessage();
}
this.warning.emit({
message: 'INVALID_PRESELECTED_USERS',
users: this.invalidUsers
});
}
private removeUserFromSelected({ id, username, email }: IdentityUserModel): void {
@@ -494,7 +396,7 @@ export class PeopleCloudComponent implements OnInit, OnChanges, OnDestroy {
}
}
generateInvalidUsersMessage(): void {
private generateInvalidUsersMessage(): void {
this.validateUsersMessage = '';
this.invalidUsers.forEach((invalidUser, index) => {
@@ -506,13 +408,6 @@ export class PeopleCloudComponent implements OnInit, OnChanges, OnDestroy {
});
}
setTypingError(): void {
this.searchUserCtrl.setErrors({
searchTypingError: true,
...this.searchUserCtrl.errors
});
}
hasPreselectError(): boolean {
return this.invalidUsers
&& this.invalidUsers.length > 0;
@@ -522,11 +417,11 @@ export class PeopleCloudComponent implements OnInit, OnChanges, OnDestroy {
return FullNamePipe.prototype.transform(user);
}
isMultipleMode(): boolean {
private isMultipleMode(): boolean {
return this.mode === 'multiple';
}
isSingleMode(): boolean {
private isSingleMode(): boolean {
return this.mode === 'single';
}
@@ -541,9 +436,22 @@ export class PeopleCloudComponent implements OnInit, OnChanges, OnDestroy {
&& this.preSelectUsers.length > 0;
}
private valueChanged(change: SimpleChange): boolean {
return change
&& change.currentValue !== change.previousValue;
private hasModeChanged(changes: SimpleChanges): boolean {
return changes
&& changes.mode
&& changes.mode.currentValue !== changes.mode.previousValue;
}
private isValidationChanged(changes: SimpleChanges): boolean {
return changes
&& changes.validate
&& changes.validate.currentValue !== changes.validate.previousValue;
}
private hasPreselectedUsersChanged(changes: SimpleChanges): boolean {
return changes
&& changes.preSelectUsers
&& changes.preSelectUsers.currentValue !== changes.preSelectUsers.previousValue;
}
private hasPreselectedUsersCleared(changes: SimpleChanges): boolean {
@@ -554,20 +462,21 @@ export class PeopleCloudComponent implements OnInit, OnChanges, OnDestroy {
}
private resetSearchUsers(): void {
this._searchUsers = [];
this.searchUsers$.next(this._searchUsers);
this.searchUsers = [];
this.searchUsers$.next(this.searchUsers);
}
private isUserPartOfAllRestrictedGroups(user: IdentityUserModel): Observable<boolean> {
return this.getUserGroups(user.id).pipe(
map(userGroups => this.groupsRestriction.every(restricted => userGroups.includes(restricted)))
);
private setTypingError(): void {
this.searchUserCtrl.setErrors({
searchTypingError: true,
...this.searchUserCtrl.errors
});
}
private getUserGroups(userId: string): Observable<string[]> {
return this.identityUserService.getInvolvedGroups(userId).pipe(
map(groups => groups.map((group) => group.name))
);
private userChipsControlValue(value: string) {
this.userChipsCtrl.setValue(value);
this.userChipsCtrl.markAsDirty();
this.userChipsCtrl.markAsTouched();
}
getSelectedUsers(): IdentityUserModel[] {
@@ -617,4 +526,9 @@ export class PeopleCloudComponent implements OnInit, OnChanges, OnDestroy {
getValidationMinLength(): string {
return this.searchUserCtrl.errors.minlength.requiredLength;
}
ngOnDestroy(): void {
this.onDestroy$.next(true);
this.onDestroy$.complete();
}
}

View File

@@ -0,0 +1,90 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { HttpErrorResponse } from '@angular/common/http';
import { throwError } from 'rxjs';
import { IdentityUserModel } from '../models/identity-user.model';
import { IdentityUserFilterInterface } from '../services/identity-user-filter.interface';
export const mockSearchUserEmptyFilters: IdentityUserFilterInterface = {
roles: [],
groups: [],
withinApplication: ''
};
export const mockSearchUserByGroups: IdentityUserFilterInterface = {
roles: [],
groups: ['fake-group-1', 'fake-group-2'],
withinApplication: ''
};
export const mockSearchUserByGroupsAndRoles: IdentityUserFilterInterface = {
roles: ['fake-role-1', 'fake-role-2'],
groups: ['fake-group-1', 'fake-group-2'],
withinApplication: ''
};
export const mockSearchUserByGroupsAndRolesAndApp: IdentityUserFilterInterface = {
roles: ['fake-role-1', 'fake-role-2'],
groups: ['fake-group-1', 'fake-group-2'],
withinApplication: 'fake-app-name'
};
export const mockSearchUserByRoles: IdentityUserFilterInterface = {
roles: ['fake-role-1', 'fake-role-2'],
groups: [],
withinApplication: ''
};
export const mockSearchUserByRolesAndApp: IdentityUserFilterInterface = {
roles: ['fake-role-1', 'fake-role-2'],
groups: [],
withinApplication: 'fake-app-name'
};
export const mockSearchUserByApp: IdentityUserFilterInterface = {
roles: [],
groups: [],
withinApplication: 'fake-app-name'
};
export const mockSearchUserByAppAndGroups: IdentityUserFilterInterface = {
roles: [],
groups: ['fake-group-1', 'fake-group-2'],
withinApplication: 'fake-app-name'
};
export function oAuthMockApiWithIdentityUsers(users: IdentityUserModel[]) {
return {
oauth2Auth: {
callCustomApi: () => Promise.resolve(users)
},
reply: jasmine.createSpy('reply')
};
}
const errorResponse = new HttpErrorResponse({
error: 'Mock Error',
status: 404, statusText: 'Not Found'
});
export const oAuthMockApiWithError = {
oauth2Auth: {
callCustomApi: () => throwError(errorResponse)
},
reply: jasmine.createSpy('reply')
};

View File

@@ -0,0 +1,55 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Injectable } from '@angular/core';
import { Observable, EMPTY, of } from 'rxjs';
import { IdentityUserModel } from '../models/identity-user.model';
import { IdentityUserFilterInterface } from '../services/identity-user-filter.interface';
import { IdentityUserServiceInterface } from '../services/identity-user.service.interface';
export const mockYorkshirePudding: IdentityUserModel = { id: 'yorkshire', username: 'Yorkshire Pudding', firstName: 'Yorkshire', lastName: 'Pudding', email: 'pudding@food.com' };
export const mockShepherdsPie: IdentityUserModel = { id: 'shepherds', username: 'Shepherds Pie', firstName: 'Shepherds', lastName: 'Pie', email: 'shepherds@food.com'};
export const mockKielbasaSausage: IdentityUserModel = { id: 'kielbasa', username: 'Kielbasa Sausage', firstName: 'Kielbasa', lastName: 'Sausage', email: 'sausage@food.com' };
export const mockFoodUsers: IdentityUserModel[] = [mockYorkshirePudding, mockShepherdsPie, mockKielbasaSausage];
export const mockPreselectedFoodUsers = [
{ ...mockYorkshirePudding, readonly: false },
{ ...mockKielbasaSausage, readonly: false }
];
@Injectable({
providedIn: 'root'
})
export class IdentityUserServiceMock implements IdentityUserServiceInterface {
queryParams: { search: string; application?: string; roles?: string[]; groups?: string[] };
getCurrentUserInfo(): IdentityUserModel {
return mockKielbasaSausage;
}
search(name: string, _filters?: IdentityUserFilterInterface): Observable<IdentityUserModel[]> {
if (name.trim() === '') {
return EMPTY;
}
return of(mockFoodUsers.filter(group =>
group.username.toUpperCase().includes(name.toUpperCase())
));
}
}

View File

@@ -1,52 +0,0 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const mockUsers = [
{ id: 'fake-id-1', username: 'first-name-1 last-name-1', firstName: 'first-name-1', lastName: 'last-name-1', email: 'abc@xyz.com' },
{ id: 'fake-id-2', username: 'first-name-2 last-name-2', firstName: 'first-name-2', lastName: 'last-name-2', email: 'abcd@xyz.com'},
{ id: 'fake-id-3', username: 'first-name-3 last-name-3', firstName: 'first-name-3', lastName: 'last-name-3', email: 'abcde@xyz.com' }
];
export const cloudMockUser = {
id: 'fake-id-1', username: 'AssignedTaskUser', firstName: 'first-name-1', lastName: 'last-name-1', email: 'abc@xyz.com'
};
export const mockRoles = [
{ id: 'id-1', name: 'MOCK-ADMIN-ROLE'},
{ id: 'id-2', name: 'MOCK-USER-ROLE'},
{ id: 'id-3', name: 'MOCK_MODELER-ROLE' },
{ id: 'id-4', name: 'MOCK-ROLE-1' },
{ id: 'id-5', name: 'MOCK-ROLE-2'}
];
export const mockOAuth2: any = {
oauth2Auth: {
callCustomApi: () => Promise.resolve(mockUsers)
},
isEcmLoggedIn: () => false,
reply: jasmine.createSpy('reply')
};
export const mockPreselectedUsers = [
{ id: mockUsers[1].id, username: mockUsers[1].username, readonly: false },
{ id: mockUsers[2].id, username: mockUsers[2].username, readonly: false }
];
export const mockInvolvedGroups = [
{ id: 'mock-group-id-1', name: 'Mock Group 1', path: '/mock', subGroups: [] },
{ id: 'mock-group-id-2', name: 'Mock Group 2', path: '', subGroups: [] }
];

View File

@@ -0,0 +1,25 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export interface IdentityUserModel {
id?: string;
username: string;
firstName?: string;
lastName?: string;
email?: string;
readonly?: boolean;
}

View File

@@ -22,6 +22,8 @@ import { MaterialModule } from '../material.module';
import { CoreModule } from '@alfresco/adf-core';
import { FlexLayoutModule } from '@angular/flex-layout';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { IdentityUserService } from './services/identity-user.service';
import { IDENTITY_USER_SERVICE_TOKEN } from './services/identity-user-service.token';
@NgModule({
imports: [
@@ -35,6 +37,9 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms';
declarations: [PeopleCloudComponent],
exports: [
PeopleCloudComponent
],
providers: [
{ provide: IDENTITY_USER_SERVICE_TOKEN, useExisting: IdentityUserService }
]
})
export class PeopleCloudModule {

View File

@@ -16,5 +16,7 @@
*/
export * from './components/people-cloud.component';
export * from './people-cloud.module';
export * from './models/identity-user.model';
export * from './services/identity-user.service';
export * from './services/identity-user-service.token';

View File

@@ -0,0 +1,22 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export interface IdentityUserFilterInterface {
roles?: string[];
withinApplication?: string;
groups?: string[];
}

View File

@@ -0,0 +1,21 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { InjectionToken } from '@angular/core';
import { IdentityUserServiceInterface } from './identity-user.service.interface';
export const IDENTITY_USER_SERVICE_TOKEN = new InjectionToken<IdentityUserServiceInterface>('identity-user-service-token');

View File

@@ -0,0 +1,25 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Observable } from 'rxjs';
import { IdentityUserModel } from '../models/identity-user.model';
import { IdentityUserFilterInterface } from './identity-user-filter.interface';
export interface IdentityUserServiceInterface {
getCurrentUserInfo(): IdentityUserModel;
search(name: string, filters?: IdentityUserFilterInterface): Observable<IdentityUserModel[]>;
}

View File

@@ -0,0 +1,293 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { TestBed } from '@angular/core/testing';
import { TranslateModule } from '@ngx-translate/core';
import { AlfrescoApiService, JwtHelperService, mockToken, setupTestBed } from '@alfresco/adf-core';
import { IdentityUserService } from './identity-user.service';
import { ProcessServiceCloudTestingModule } from '../../testing/process-service-cloud.testing.module';
import {
mockSearchUserByApp,
mockSearchUserByAppAndGroups,
mockSearchUserByGroups,
mockSearchUserByGroupsAndRoles,
mockSearchUserByGroupsAndRolesAndApp,
mockSearchUserByRoles,
mockSearchUserByRolesAndApp,
oAuthMockApiWithError,
oAuthMockApiWithIdentityUsers
} from '../mock/identity-user.service.mock';
import { mockFoodUsers } from '../mock/people-cloud.mock';
describe('IdentityUserService', () => {
let service: IdentityUserService;
let alfrescoApiService: AlfrescoApiService;
setupTestBed({
imports: [
TranslateModule.forRoot(),
ProcessServiceCloudTestingModule
]
});
beforeEach(() => {
service = TestBed.inject(IdentityUserService);
alfrescoApiService = TestBed.inject(AlfrescoApiService);
});
describe('Current user info (JWT token)', () => {
beforeEach(() => {
const store = {};
spyOn(localStorage, 'getItem').and.callFake((key: string): string => store[key] || null);
spyOn(localStorage, 'setItem').and.callFake((key: string, value: string): string => store[key] = value);
});
it('should fetch identity user info from Jwt id token', () => {
localStorage.setItem(JwtHelperService.USER_ID_TOKEN, mockToken);
const user = service.getCurrentUserInfo();
expect(user).toBeDefined();
expect(user.firstName).toEqual('John');
expect(user.lastName).toEqual('Doe');
expect(user.email).toEqual('johnDoe@gmail.com');
expect(user.username).toEqual('johnDoe1');
});
it('should fallback on Jwt access token for identity user info', () => {
localStorage.setItem(JwtHelperService.USER_ACCESS_TOKEN, mockToken);
const user = service.getCurrentUserInfo();
expect(user).toBeDefined();
expect(user.firstName).toEqual('John');
expect(user.lastName).toEqual('Doe');
expect(user.email).toEqual('johnDoe@gmail.com');
expect(user.username).toEqual('johnDoe1');
});
});
describe('Search', () => {
it('should fetch users', (done) => {
spyOn(alfrescoApiService, 'getInstance').and.returnValue(oAuthMockApiWithIdentityUsers(mockFoodUsers));
const searchSpy = spyOn(service, 'search').and.callThrough();
service.search('fake').subscribe(
res => {
expect(res).toBeDefined();
expect(searchSpy).toHaveBeenCalled();
expect(service.queryParams).toEqual({
search: 'fake'
});
done();
}
);
});
it('should not fetch users if error occurred', (done) => {
spyOn(alfrescoApiService, 'getInstance').and.returnValue(oAuthMockApiWithError);
const searchSpy = spyOn(service, 'search').and.callThrough();
service.search('fake')
.subscribe(
() => {
fail('expected an error, not users');
},
(error) => {
expect(searchSpy).toHaveBeenCalled();
expect(error.status).toEqual(404);
expect(error.statusText).toEqual('Not Found');
expect(error.error).toEqual('Mock Error');
done();
}
);
});
it('should fetch users by roles', (done) => {
spyOn(alfrescoApiService, 'getInstance').and.returnValue(oAuthMockApiWithIdentityUsers(mockFoodUsers));
const searchSpy = spyOn(service, 'search').and.callThrough();
service.search('fake', mockSearchUserByRoles).subscribe(
res => {
expect(res).toBeDefined();
expect(searchSpy).toHaveBeenCalled();
expect(service.queryParams).toEqual({
search: 'fake',
role: 'fake-role-1,fake-role-2'
});
done();
}
);
});
it('should not fetch users by roles if error occurred', (done) => {
spyOn(alfrescoApiService, 'getInstance').and.returnValue(oAuthMockApiWithError);
service.search('fake', mockSearchUserByRoles)
.subscribe(
() => {
fail('expected an error, not users');
},
(error) => {
expect(error.status).toEqual(404);
expect(error.statusText).toEqual('Not Found');
expect(error.error).toEqual('Mock Error');
done();
}
);
});
it('should fetch users by groups', (done) => {
spyOn(alfrescoApiService, 'getInstance').and.returnValue(oAuthMockApiWithIdentityUsers(mockFoodUsers));
const searchSpy = spyOn(service, 'search').and.callThrough();
service.search('fake', mockSearchUserByGroups).subscribe(
res => {
expect(res).toBeDefined();
expect(searchSpy).toHaveBeenCalled();
expect(service.queryParams).toEqual({
search: 'fake',
group: 'fake-group-1,fake-group-2'
});
done();
}
);
});
it('should fetch users by roles with groups', (done) => {
spyOn(alfrescoApiService, 'getInstance').and.returnValue(oAuthMockApiWithIdentityUsers(mockFoodUsers));
const searchSpy = spyOn(service, 'search').and.callThrough();
service.search('fake', mockSearchUserByGroupsAndRoles).subscribe(
res => {
expect(res).toBeDefined();
expect(searchSpy).toHaveBeenCalled();
expect(service.queryParams).toEqual({
search: 'fake',
role: 'fake-role-1,fake-role-2',
group: 'fake-group-1,fake-group-2'
});
done();
}
);
});
it('should fetch users by roles with groups and appName', (done) => {
spyOn(alfrescoApiService, 'getInstance').and.returnValue(oAuthMockApiWithIdentityUsers(mockFoodUsers));
const searchSpy = spyOn(service, 'search').and.callThrough();
service.search('fake', mockSearchUserByGroupsAndRolesAndApp).subscribe(
res => {
expect(res).toBeDefined();
expect(searchSpy).toHaveBeenCalled();
expect(service.queryParams).toEqual({
search: 'fake',
role: 'fake-role-1,fake-role-2',
application: 'fake-app-name',
group: 'fake-group-1,fake-group-2'
});
done();
}
);
});
it('should not fetch users by groups if error occurred', (done) => {
spyOn(alfrescoApiService, 'getInstance').and.returnValue(oAuthMockApiWithError);
service.search('fake', mockSearchUserByGroups)
.subscribe(
() => {
fail('expected an error, not users');
},
(error) => {
expect(error.status).toEqual(404);
expect(error.statusText).toEqual('Not Found');
expect(error.error).toEqual('Mock Error');
done();
}
);
});
it('should fetch users within app', (done) => {
spyOn(alfrescoApiService, 'getInstance').and.returnValue(oAuthMockApiWithIdentityUsers(mockFoodUsers));
service.search('fake', mockSearchUserByApp).subscribe(
res => {
expect(res).toBeDefined();
expect(service.queryParams).toEqual({
search: 'fake',
application: 'fake-app-name'
});
done();
}
);
});
it('should fetch users within app with roles', (done) => {
spyOn(alfrescoApiService, 'getInstance').and.returnValue(oAuthMockApiWithIdentityUsers(mockFoodUsers));
service.search('fake', mockSearchUserByRolesAndApp).subscribe(
res => {
expect(res).toBeDefined();
expect(service.queryParams).toEqual({
search: 'fake',
application: 'fake-app-name',
role: 'fake-role-1,fake-role-2'
});
done();
}
);
});
it('should fetch users within app with groups', (done) => {
spyOn(alfrescoApiService, 'getInstance').and.returnValue(oAuthMockApiWithIdentityUsers(mockFoodUsers));
const searchSpy = spyOn(service, 'search').and.callThrough();
service.search('fake', mockSearchUserByAppAndGroups).subscribe(
res => {
expect(res).toBeDefined();
expect(searchSpy).toHaveBeenCalled();
expect(service.queryParams).toEqual({
search: 'fake',
application: 'fake-app-name',
group: 'fake-group-1,fake-group-2'
});
done();
}
);
});
it('should not fetch users within app if error occurred', (done) => {
spyOn(alfrescoApiService, 'getInstance').and.returnValue(oAuthMockApiWithError);
service.search('fake', mockSearchUserByApp)
.subscribe(
() => {
fail('expected an error, not users');
},
(error) => {
expect(error.status).toEqual(404);
expect(error.statusText).toEqual('Not Found');
expect(error.error).toEqual('Mock Error');
done();
}
);
});
});
});

View File

@@ -0,0 +1,150 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Injectable } from '@angular/core';
import {
AppConfigService,
JwtHelperService,
OAuth2Service
} from '@alfresco/adf-core';
import { EMPTY, Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { IdentityUserServiceInterface } from './identity-user.service.interface';
import { IdentityUserModel } from '../models/identity-user.model';
import { IdentityUserFilterInterface } from './identity-user-filter.interface';
const IDENTITY_MICRO_SERVICE_INGRESS = 'modeling-service';
@Injectable({
providedIn: 'root'
})
export class IdentityUserService implements IdentityUserServiceInterface {
queryParams: { search: string; application?: string; roles?: string[]; groups?: string[] };
constructor(
private jwtHelperService: JwtHelperService,
private oAuth2Service: OAuth2Service,
private appConfigService: AppConfigService) {
}
/**
* Gets the name and other basic details of the current user.
*
* @returns The user's details
*/
public getCurrentUserInfo(): IdentityUserModel {
const familyName = this.jwtHelperService.getValueFromLocalToken<string>(JwtHelperService.FAMILY_NAME);
const givenName = this.jwtHelperService.getValueFromLocalToken<string>(JwtHelperService.GIVEN_NAME);
const email = this.jwtHelperService.getValueFromLocalToken<string>(JwtHelperService.USER_EMAIL);
const username = this.jwtHelperService.getValueFromLocalToken<string>(JwtHelperService.USER_PREFERRED_USERNAME);
return { firstName: givenName, lastName: familyName, email, username };
}
/**
* Search users based on name input and filters.
*
* @param name Search query string
* @param [filters] Search query filters
* @returns List of users
*/
public search(name: string, filters?: IdentityUserFilterInterface): Observable<IdentityUserModel[]> {
if (name.trim() === '') {
return EMPTY;
} else if (filters?.groups?.length > 0) {
return this.searchUsersWithGroups(name, filters);
} else if (filters?.withinApplication) {
return this.searchUsersWithinApp(name, filters.withinApplication, filters?.roles);
} else if (filters?.roles?.length > 0) {
return this.searchUsersWithGlobalRoles(name, filters.roles);
} else {
return this.searchUsersByName(name);
}
}
private searchUsersByName(name: string): Observable<IdentityUserModel[]> {
this.buildQueryParam(name);
return this.invokeIdentityUserApi().pipe(
catchError((err) => this.handleError(err))
);
}
private searchUsersWithGlobalRoles(name: string, roles: string []): Observable<IdentityUserModel[]> {
this.buildQueryParam(name, {roles});
return this.invokeIdentityUserApi().pipe(
catchError((err) => this.handleError(err))
);
}
private searchUsersWithinApp(name: string, withinApplication: string, roles?: string []): Observable<IdentityUserModel[]> {
this.buildQueryParam(name, {roles, withinApplication});
return this.invokeIdentityUserApi().pipe(
catchError((err) => this.handleError(err))
);
}
private searchUsersWithGroups(name: string, filters: IdentityUserFilterInterface): Observable<IdentityUserModel[]> {
this.buildQueryParam(name, filters);
return this.invokeIdentityUserApi().pipe(
catchError((err) => this.handleError(err))
);
}
private invokeIdentityUserApi(): Observable<any> {
const url = `${this.identityHost}/${IDENTITY_MICRO_SERVICE_INGRESS}/v1/identity/users`;
return this.oAuth2Service.get({ url, queryParams: this.queryParams });
}
private buildQueryParam(name: string, filters?: IdentityUserFilterInterface) {
this.queryParams = { search: name };
this.addOptionalValueToQueryParam('application', filters?.withinApplication);
this.addOptionalCommaValueToQueryParam('role', filters?.roles);
this.addOptionalCommaValueToQueryParam('group', filters?.groups);
}
private addOptionalCommaValueToQueryParam(key: string, values: string []) {
if (values?.length > 0) {
const valuesNotEmpty = this.filterOutEmptyValue(values);
if (valuesNotEmpty?.length > 0) {
this.queryParams[key] = valuesNotEmpty.join(',');
}
}
}
private addOptionalValueToQueryParam(key: string, value: string) {
if (value?.trim()) {
this.queryParams[key] = value;
}
}
private filterOutEmptyValue(values: string []): string [] {
return values.filter( value => value.trim() ? true : false);
}
private get identityHost(): string {
return `${this.appConfigService.get('bpmHost')}`;
}
private handleError(error: any) {
return throwError(error || 'Server error');
}
}

View File

@@ -17,11 +17,12 @@
import { Component, ViewChild } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { IdentityUserService, setupTestBed } from '@alfresco/adf-core';
import { setupTestBed } from '@alfresco/adf-core';
import { CancelProcessDirective } from './cancel-process.directive';
import { ProcessServiceCloudTestingModule } from '../../testing/process-service-cloud.testing.module';
import { TranslateModule } from '@ngx-translate/core';
import { ProcessInstanceCloud } from '../start-process/models/process-instance-cloud.model';
import { IdentityUserService } from '../../people/services/identity-user.service';
const processDetailsMockRunning: ProcessInstanceCloud = { initiator: 'usermock', status: 'RUNNING' };
const processDetailsMockCompleted: ProcessInstanceCloud = { initiator: 'usermock', status: 'COMPLETED' };

View File

@@ -15,11 +15,11 @@
* limitations under the License.
*/
import { Directive, HostListener, Output, EventEmitter, OnInit, OnDestroy, ElementRef } from '@angular/core';
import { IdentityUserService } from '@alfresco/adf-core';
import { ProcessCloudService } from '../services/process-cloud.service';
import { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
import { ProcessInstanceCloud } from '../start-process/models/process-instance-cloud.model';
import { IdentityUserService } from '../../people/services/identity-user.service';
@Directive({
// eslint-disable-next-line @angular-eslint/directive-selector

View File

@@ -25,11 +25,12 @@ import moment from 'moment-es6';
import { Moment } from 'moment';
import { AppsProcessCloudService } from '../../../app/services/apps-process-cloud.service';
import { ProcessFilterCloudModel, ProcessFilterProperties, ProcessFilterAction, ProcessFilterOptions, ProcessSortFilterProperty } from '../models/process-filter-cloud.model';
import { IdentityUserModel, TranslationService, UserPreferencesService, UserPreferenceValues } from '@alfresco/adf-core';
import { TranslationService, UserPreferencesService, UserPreferenceValues } from '@alfresco/adf-core';
import { ProcessFilterCloudService } from '../services/process-filter-cloud.service';
import { ProcessFilterDialogCloudComponent } from './process-filter-dialog-cloud.component';
import { ProcessCloudService } from '../../services/process-cloud.service';
import { DateCloudFilterType, DateRangeFilter } from '../../../models/date-cloud-filter.model';
import { IdentityUserModel } from '../../../people/models/identity-user.model';
export const PROCESS_FILTER_ACTION_SAVE = 'save';
export const PROCESS_FILTER_ACTION_SAVE_AS = 'saveAs';

View File

@@ -16,7 +16,7 @@
*/
import { TestBed } from '@angular/core/testing';
import { setupTestBed, IdentityUserService } from '@alfresco/adf-core';
import { setupTestBed } from '@alfresco/adf-core';
import { of } from 'rxjs';
import { ProcessFilterCloudService } from './process-filter-cloud.service';
import { PROCESS_FILTERS_SERVICE_TOKEN } from '../../../services/cloud-token.service';
@@ -25,6 +25,7 @@ import { ProcessServiceCloudTestingModule } from '../../../testing/process-servi
import { TranslateModule } from '@ngx-translate/core';
import { fakeEmptyProcessCloudFilterEntries, fakeProcessCloudFilterEntries, fakeProcessCloudFilters, fakeProcessCloudFilterWithDifferentEntries, fakeProcessFilter } from '../mock/process-filters-cloud.mock';
import { ProcessFilterCloudModel } from '../models/process-filter-cloud.model';
import { IdentityUserService } from '../../../people/services/identity-user.service';
describe('ProcessFilterCloudService', () => {
let service: ProcessFilterCloudService;

View File

@@ -15,13 +15,13 @@
* limitations under the License.
*/
import { IdentityUserService } from '@alfresco/adf-core';
import { Injectable, Inject } from '@angular/core';
import { Observable, of, BehaviorSubject, throwError } from 'rxjs';
import { ProcessFilterCloudModel } from '../models/process-filter-cloud.model';
import { switchMap, map, catchError } from 'rxjs/operators';
import { PROCESS_FILTERS_SERVICE_TOKEN } from '../../../services/cloud-token.service';
import { PreferenceCloudServiceInterface } from '../../../services/preference-cloud.interface';
import { IdentityUserService } from '../../../people/services/identity-user.service';
@Injectable({
providedIn: 'root'
})

View File

@@ -39,8 +39,7 @@ export class BaseCloudService {
path: '',
httpMethod: '',
contentTypes: ['application/json'],
accepts: ['application/json'],
returnType: Object
accepts: ['application/json']
};
constructor(

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
import { Directive, Input, HostListener, Output, EventEmitter, OnInit } from '@angular/core';
import { IdentityUserService } from '@alfresco/adf-core';
import { IdentityUserService } from '../../people/services/identity-user.service';
import { TaskCloudService } from '../services/task-cloud.service';
@Directive({

View File

@@ -18,8 +18,8 @@
import { Injectable } from '@angular/core';
import { AppConfigService, CardViewArrayItem, LogService } from '@alfresco/adf-core';
import { from, Observable, of, Subject, throwError } from 'rxjs';
import { DEFAULT_TASK_PRIORITIES, TaskPriorityOption, TASK_ASSIGNED_STATE, TASK_CREATED_STATE } from '../models/task.model';
import { TaskDetailsCloudModel } from '../start-task/public-api';
import { DEFAULT_TASK_PRIORITIES, TaskPriorityOption } from '../models/task.model';
import { TaskDetailsCloudModel, TASK_ASSIGNED_STATE, TASK_CREATED_STATE } from '../start-task/models/task-details-cloud.model';
import { taskDetailsContainer } from '../task-header/mocks/task-details-cloud.mock';
import { ProcessDefinitionCloud } from '../../models/process-definition-cloud.model';
import { StartTaskCloudRequestModel } from '../start-task/models/start-task-cloud-request.model';

View File

@@ -16,7 +16,7 @@
*/
import { TestBed } from '@angular/core/testing';
import { setupTestBed, IdentityUserService, TranslationService, AlfrescoApiService } from '@alfresco/adf-core';
import { setupTestBed, TranslationService, AlfrescoApiService } from '@alfresco/adf-core';
import { TaskCloudService } from './task-cloud.service';
import { taskCompleteCloudMock } from '../task-header/mocks/fake-complete-task.mock';
import { assignedTaskDetailsCloudMock, createdTaskDetailsCloudMock, emptyOwnerTaskDetailsCloudMock } from '../task-header/mocks/task-details-cloud.mock';
@@ -24,6 +24,7 @@ import { fakeTaskDetailsCloud } from '../task-header/mocks/fake-task-details-res
import { cloudMockUser } from '../start-task/mock/user-cloud.mock';
import { ProcessServiceCloudTestingModule } from '../../testing/process-service-cloud.testing.module';
import { TranslateModule } from '@ngx-translate/core';
import { IdentityUserService } from '../../people/services/identity-user.service';
describe('Task Cloud Service', () => {

View File

@@ -16,9 +16,9 @@
*/
import { Injectable } from '@angular/core';
import { AlfrescoApiService, LogService, AppConfigService, IdentityUserService, CardViewArrayItem, TranslationService } from '@alfresco/adf-core';
import { AlfrescoApiService, LogService, AppConfigService, CardViewArrayItem, TranslationService } from '@alfresco/adf-core';
import { throwError, Observable, of, Subject } from 'rxjs';
import { map } from 'rxjs/operators';
import { catchError, map } from 'rxjs/operators';
import {
TaskDetailsCloudModel,
StartTaskCloudResponseModel,
@@ -35,6 +35,7 @@ import {
TaskPriorityOption
} from '../models/task.model';
import { TaskCloudServiceInterface } from './task-cloud.service.interface';
import { IdentityUserService } from '../../people/services/identity-user.service';
@Injectable({
providedIn: 'root'
@@ -242,7 +243,9 @@ export class TaskCloudService extends BaseCloudService implements TaskCloudServi
getCandidateUsers(appName: string, taskId: string): Observable<string[]> {
if ((appName || appName === '') && taskId) {
const queryUrl = `${this.getBasePath(appName)}/query/v1/tasks/${taskId}/candidate-users`;
return this.get<string[]>(queryUrl);
return this.get<string[]>(queryUrl).pipe(
catchError((err) => this.handleError(err))
);
} else {
this.logService.error('AppName and TaskId are mandatory to get candidate user');
return of([]);
@@ -320,4 +323,9 @@ export class TaskCloudService extends BaseCloudService implements TaskCloudServi
const currentUser = this.identityUserService.getCurrentUserInfo().username;
return assignee === currentUser;
}
private handleError(error?: any) {
this.logService.error(error);
return throwError(error || 'Server error');
}
}

View File

@@ -16,7 +16,7 @@
*/
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { setupTestBed, IdentityUserService, AlfrescoApiService, IdentityUserModel } from '@alfresco/adf-core';
import { setupTestBed, AlfrescoApiService } from '@alfresco/adf-core';
import { StartTaskCloudComponent } from './start-task-cloud.component';
import { of, throwError } from 'rxjs';
import { taskDetailsMock } from '../mock/task-details.mock';
@@ -26,6 +26,8 @@ import { FormDefinitionSelectorCloudService } from '../../../form/services/form-
import { TaskCloudService } from '../../services/task-cloud.service';
import { StartTaskCloudRequestModel } from '../models/start-task-cloud-request.model';
import { TranslateModule } from '@ngx-translate/core';
import { IdentityUserService } from '../../../people/services/identity-user.service';
import { IdentityUserModel } from '../../../people/models/identity-user.model';
describe('StartTaskCloudComponent', () => {

View File

@@ -24,8 +24,6 @@ import {
MOMENT_DATE_FORMATS, MomentDateAdapter,
LogService,
UserPreferencesService,
IdentityUserService,
IdentityUserModel,
UserPreferenceValues
} from '@alfresco/adf-core';
import { PeopleCloudComponent } from '../../../people/components/people-cloud.component';
@@ -34,6 +32,8 @@ import { TaskCloudService } from '../../services/task-cloud.service';
import { StartTaskCloudRequestModel } from '../models/start-task-cloud-request.model';
import { takeUntil } from 'rxjs/operators';
import { TaskPriorityOption } from '../../models/task.model';
import { IdentityUserService } from '../../../people/services/identity-user.service';
import { IdentityUserModel } from '../../../people/models/identity-user.model';
const MAX_NAME_LENGTH = 255;
const DATE_FORMAT: string = 'DD/MM/YYYY';

View File

@@ -25,9 +25,11 @@ import { AbstractControl, FormBuilder, FormGroup } from '@angular/forms';
import { debounceTime, filter, finalize, switchMap, takeUntil } from 'rxjs/operators';
import { Observable, Subject } from 'rxjs';
import { DateAdapter } from '@angular/material/core';
import { IdentityGroupModel, IdentityUserModel, TranslationService, UserPreferencesService, UserPreferenceValues } from '@alfresco/adf-core';
import { TranslationService, UserPreferencesService, UserPreferenceValues } from '@alfresco/adf-core';
import { TaskFilterDialogCloudComponent } from '../task-filter-dialog/task-filter-dialog-cloud.component';
import { MatDialog } from '@angular/material/dialog';
import { IdentityUserModel } from '../../../../people/models/identity-user.model';
import { IdentityGroupModel } from '../../../../group/models/identity-group.model';
/* eslint-disable @typescript-eslint/naming-convention */

View File

@@ -19,7 +19,7 @@ import { ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing';
import { SimpleChange } from '@angular/core';
import { By } from '@angular/platform-browser';
import { AlfrescoApiService, IdentityUserModel, setupTestBed } from '@alfresco/adf-core';
import { AlfrescoApiService, setupTestBed } from '@alfresco/adf-core';
import { MatDialog } from '@angular/material/dialog';
import { of } from 'rxjs';
import { debounceTime } from 'rxjs/operators';
@@ -41,6 +41,7 @@ import { TaskFilterCloudModel } from '../../models/filter-cloud.model';
import { PeopleCloudModule } from '../../../../people/people-cloud.module';
import { ProcessDefinitionCloud } from '../../../../models/process-definition-cloud.model';
import { MatIconTestingModule } from '@angular/material/icon/testing';
import { IdentityUserModel } from '../../../../people/models/identity-user.model';
describe('EditTaskFilterCloudComponent', () => {
let component: EditTaskFilterCloudComponent;
@@ -537,7 +538,10 @@ describe('EditTaskFilterCloudComponent', () => {
const mockUser: IdentityUserModel[] = [{
id: 'id',
username: 'test'
username: 'test',
firstName: 'first-name',
lastName: 'last-name',
email: 'email@fake.com'
}];
const startedDateTypeControl: AbstractControl = component.editTaskFilterForm.get('completedBy');

View File

@@ -16,13 +16,14 @@
*/
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { setupTestBed, IdentityUserService, TranslationService, TranslationMock } from '@alfresco/adf-core';
import { setupTestBed, TranslationService, TranslationMock } from '@alfresco/adf-core';
import { TranslateModule } from '@ngx-translate/core';
import { TaskAssignmentFilterCloudComponent } from './task-assignment-filter.component';
import { GroupCloudModule } from 'process-services-cloud/src/lib/group/public-api';
import { TaskFiltersCloudModule } from '../../task-filters-cloud.module';
import { AssignmentType } from '../../models/filter-cloud.model';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { IdentityUserService } from '../../../../people/services/identity-user.service';
describe('EditTaskFilterCloudComponent', () => {
let component: TaskAssignmentFilterCloudComponent;

View File

@@ -17,8 +17,10 @@
import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';
import { IdentityGroupModel, IdentityUserModel, IdentityUserService } from '@alfresco/adf-core';
import { AssignmentType, TaskFilterProperties } from '../../models/filter-cloud.model';
import { IdentityUserModel } from '../../../../people/models/identity-user.model';
import { IdentityUserService } from '../../../../people/services/identity-user.service';
import { IdentityGroupModel } from '../../../../group/models/identity-group.model';
@Component({
selector: 'adf-cloud-task-assignment-filter',

View File

@@ -22,7 +22,8 @@
import { DateCloudFilterType } from '../../../models/date-cloud-filter.model';
import { DateRangeFilterService } from '../../../common/date-range-filter/date-range-filter.service';
import { ComponentSelectionMode } from '../../../types';
import { IdentityUserModel, IdentityGroupModel } from '@alfresco/adf-core';
import { IdentityGroupModel } from '../../../group/models/identity-group.model';
import { IdentityUserModel } from '../../../people/models/identity-user.model';
export class TaskFilterCloudModel {
id: string;

View File

@@ -15,13 +15,13 @@
* limitations under the License.
*/
import { IdentityUserService } from '@alfresco/adf-core';
import { Injectable, Inject } from '@angular/core';
import { Observable, of, BehaviorSubject } from 'rxjs';
import { ServiceTaskFilterCloudModel } from '../models/filter-cloud.model';
import { switchMap, map } from 'rxjs/operators';
import { PreferenceCloudServiceInterface } from '../../../services/preference-cloud.interface';
import { TASK_FILTERS_SERVICE_TOKEN } from '../../../services/cloud-token.service';
import { IdentityUserService } from '../../../people/services/identity-user.service';
@Injectable({
providedIn: 'root'

View File

@@ -16,7 +16,7 @@
*/
import { TestBed } from '@angular/core/testing';
import { IdentityUserService, setupTestBed } from '@alfresco/adf-core';
import { setupTestBed } from '@alfresco/adf-core';
import { of } from 'rxjs';
import { TASK_FILTERS_SERVICE_TOKEN } from '../../../services/cloud-token.service';
import { LocalPreferenceCloudService } from '../../../services/local-preference-cloud.service';
@@ -36,6 +36,7 @@ import { TaskFilterCloudModel } from '../models/filter-cloud.model';
import { NotificationCloudService } from '../../../services/notification-cloud.service';
import { TaskCloudEngineEvent } from './../../../models/engine-event-cloud.model';
import { ProcessServiceCloudTestingModule } from '../../../testing/process-service-cloud.testing.module';
import { IdentityUserService } from '../../../people/services/identity-user.service';
describe('TaskFilterCloudService', () => {
let service: TaskFilterCloudService;

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
import { AlfrescoApiService, AppConfigService, IdentityUserService } from '@alfresco/adf-core';
import { AlfrescoApiService, AppConfigService } from '@alfresco/adf-core';
import { Injectable, Inject } from '@angular/core';
import { Observable, of, BehaviorSubject, throwError } from 'rxjs';
import { TaskFilterCloudModel } from '../models/filter-cloud.model';
@@ -26,6 +26,7 @@ import { TASK_FILTERS_SERVICE_TOKEN } from '../../../services/cloud-token.servic
import { TaskCloudNodePaging } from '../../../models/task-cloud.model';
import { NotificationCloudService } from '../../../services/notification-cloud.service';
import { TaskCloudEngineEvent } from '../../../models/engine-event-cloud.model';
import { IdentityUserService } from '../../../people/services/identity-user.service';
const TASK_EVENT_SUBSCRIPTION_QUERY = `
subscription {

View File

@@ -19,7 +19,7 @@ import { DebugElement, SimpleChange } from '@angular/core';
import { By } from '@angular/platform-browser';
import { of } from 'rxjs';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { IdentityUserService, setupTestBed } from '@alfresco/adf-core';
import { setupTestBed } from '@alfresco/adf-core';
import { ProcessServiceCloudTestingModule } from '../../../testing/process-service-cloud.testing.module';
import { TaskFormCloudComponent } from './task-form-cloud.component';
import {
@@ -32,6 +32,7 @@ import {
} from '../../start-task/models/task-details-cloud.model';
import { TaskCloudService } from '../../services/task-cloud.service';
import { TranslateModule } from '@ngx-translate/core';
import { IdentityUserService } from '../../../people/services/identity-user.service';
const taskDetails: TaskDetailsCloudModel = {
appName: 'simple-app',

View File

@@ -25,11 +25,7 @@ import {
AppConfigServiceMock,
TranslationService,
TranslationMock,
CoreModule,
IdentityUserService,
IdentityUserServiceMock,
IdentityGroupService,
IdentityGroupServiceMock
CoreModule
} from '@alfresco/adf-core';
import { TranslateModule } from '@ngx-translate/core';
import { ProcessServicesCloudModule } from '../process-services-cloud.module';
@@ -47,9 +43,7 @@ import { RouterTestingModule } from '@angular/router/testing';
providers: [
{ provide: AlfrescoApiService, useClass: AlfrescoApiServiceMock },
{ provide: AppConfigService, useClass: AppConfigServiceMock },
{ provide: TranslationService, useClass: TranslationMock },
{ provide: IdentityUserService, useClass: IdentityUserServiceMock },
{ provide: IdentityGroupService, useClass: IdentityGroupServiceMock }
{ provide: TranslationService, useClass: TranslationMock }
],
exports: [
NoopAnimationsModule,