Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ <h2>{{messagePrefix + '.groupsEPersonIsMemberOf' | translate}}</h2>
<th scope="col" class="align-middle">{{messagePrefix + '.table.id' | translate}}</th>
<th scope="col" class="align-middle">{{messagePrefix + '.table.name' | translate}}</th>
<th scope="col" class="align-middle">{{messagePrefix + '.table.collectionOrCommunity' | translate}}</th>
<th scope="col" class="align-middle">{{messagePrefix + '.table.remove' | translate}}</th>
</tr>
</thead>
<tbody>
Expand All @@ -89,6 +90,14 @@ <h2>{{messagePrefix + '.groupsEPersonIsMemberOf' | translate}}</h2>
<td class="align-middle">
{{ dsoNameService.getName((group.object | async)?.payload) }}
</td>
<td class="align-middle">
<button (click)="deleteGroupFromMember(group)"
[dsBtnDisabled]="false"
class="btn btn-sm btn-outline-danger"
title="{{messagePrefix + '.table.edit.buttons.removegroup' | translate: { name: dsoNameService.getName(group) } }}">
<i class="fas fa-trash-alt fa-fw"></i>
</button>
</td>
</tr>
}
</tbody>
Expand All @@ -109,4 +118,4 @@ <h2>{{messagePrefix + '.groupsEPersonIsMemberOf' | translate}}</h2>
}
</div>
</div>
</div>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ import { NotificationsServiceStub } from '@dspace/core/testing/notifications-ser
import { PaginationServiceStub } from '@dspace/core/testing/pagination-service.stub';
import { RouterStub } from '@dspace/core/testing/router.stub';
import { createPaginatedList } from '@dspace/core/testing/utils.test';
import { createSuccessfulRemoteDataObject$ } from '@dspace/core/utilities/remote-data.utils';
import {
createFailedRemoteDataObject$,
createSuccessfulRemoteDataObject$,
} from '@dspace/core/utilities/remote-data.utils';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { TranslateModule } from '@ngx-translate/core';
import {
Expand Down Expand Up @@ -223,6 +226,7 @@ describe('EPersonFormComponent', () => {
groupsDataService = jasmine.createSpyObj('groupsDataService', {
findListByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])),
getGroupRegistryRouterLink: '',
deleteMemberFromGroup: 'deleteMemberFromGroup',
});
groupRegistryService = jasmine.createSpyObj('GroupRegistryService', {
startEditingNewGroup: jasmine.createSpy('startEditingNewGroup'),
Expand Down Expand Up @@ -537,6 +541,52 @@ describe('EPersonFormComponent', () => {
});
});

describe('delete group from member', () => {
let successSpy: jasmine.Spy;
let errorSpy: jasmine.Spy;

beforeEach(() => {
successSpy = spyOn((component as any).notificationsService, 'success');
errorSpy = spyOn((component as any).notificationsService, 'error');
});

it('should delete group from member and show notification', (done) => {
const group = { id: 'group1' } as any;
const activeEperson = EPersonMock;

spyOn(component.epeopleRegistryService, 'getActiveEPerson').and.returnValue(of(activeEperson));
(groupsDataService.deleteMemberFromGroup as jasmine.Spy)
.and.returnValue(createSuccessfulRemoteDataObject$(null));

spyOn(component.dsoNameService, 'getName').and.returnValue('Mock Group Name');

const notifySpy = spyOn(component, 'showNotifications').and.callFake(() => {
expect(groupsDataService.deleteMemberFromGroup).toHaveBeenCalledWith(group, activeEperson);
expect(notifySpy).toHaveBeenCalled();
done();
});

component.deleteGroupFromMember(group);
});

it('should show success notification on successful operation', () => {
const response = createSuccessfulRemoteDataObject$(null);

component.showNotifications('deleteMembership', response, 'TestGroup', EPersonMock);

expect(successSpy).toHaveBeenCalled();
});

it('should show error notification when response hasSucceeded is false', () => {
const response = createFailedRemoteDataObject$(null);

component.showNotifications('deleteMembership', response, 'TestGroup', EPersonMock);

expect(errorSpy).toHaveBeenCalled();
});
});


describe('Reset Password', () => {
let ePersonId;
let ePersonEmail;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,38 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
}
}

/**
* Deletes a given group from the Group list of the eperson currently being edited present in
* @param group group we want to delete as of which the current eperson being edited is member of
*/
deleteGroupFromMember(group: Group) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method is only being called in your spec file. I suspect it's supposed to be called by an actual delete button.

this.epeopleRegistryService.getActiveEPerson().pipe(take(1)).subscribe((activeEPerson: EPerson) => {
if (activeEPerson != null) {
const response = this.groupsDataService.deleteMemberFromGroup(group, activeEPerson);
this.showNotifications('deleteMembership', response, this.dsoNameService.getName(group), activeEPerson);
} else {
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveEPerson'));
}
});
}

/**
* Shows a notification based on the success/failure of the request
* @param messageSuffix Suffix for message
* @param response RestResponse observable containing success/failure request
* @param nameObject Object request was about
* @param activeEPerson EPerson currently being edited
*/
showNotifications(messageSuffix: string, response: Observable<RemoteData<any>>, nameObject: string, activeEPerson: EPerson) {
response.pipe(getFirstCompletedRemoteData()).subscribe((rd: RemoteData<any>) => {
if (rd.hasSucceeded) {
this.notificationsService.success(this.translateService.get(this.messagePrefix + '.notification.success.' + messageSuffix, { name: nameObject }));
} else {
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.' + messageSuffix, { name: nameObject }));
}
});
}

/**
* Cancel the current edit when component is destroyed & unsub all subscriptions
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,23 @@
<h2 class="border-bottom pb-2">{{messagePrefix + '.head' | translate}}</h2>

<h3>{{messagePrefix + '.headMembers' | translate}}</h3>

<form [formGroup]="searchCurrentMembersForm" (ngSubmit)="searchMembers(searchCurrentMembersForm.value)" class="d-flex justify-content-between gap-3">
<div class="flex-grow-1 mr-3">
<div class="form-group input-group mr-3">
<input type="text" name="queryCurrentMembers" id="queryCurrentMembers" formControlName="queryCurrentMembers"
class="form-control" aria-label="Search input">
<span class="input-group-append">
<button type="submit" class="search-button btn btn-primary">
<i class="fas fa-search"></i> {{ messagePrefix + '.search.button' | translate }}</button>
</span>
</div>
</div>
<div>
<button (click)="clearCurrentMembersFormAndResetResult()"
class="btn btn-secondary">{{messagePrefix + '.button.see-all' | translate}}</button>
</div>
</form>

@if ((ePeopleMembersOfGroup | async)?.totalElements > 0) {
<ds-pagination
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
waitForAsync,
} from '@angular/core/testing';
import {
FormControl,
FormGroup,
FormsModule,
ReactiveFormsModule,
} from '@angular/forms';
Expand Down Expand Up @@ -50,7 +52,10 @@ import { PaginationServiceStub } from '@dspace/core/testing/pagination-service.s
import { RouterMock } from '@dspace/core/testing/router.mock';
import { getMockTranslateService } from '@dspace/core/testing/translate.service.mock';
import { TranslateLoaderMock } from '@dspace/core/testing/translate-loader.mock';
import { createSuccessfulRemoteDataObject$ } from '@dspace/core/utilities/remote-data.utils';
import {
createFailedRemoteDataObject$,
createSuccessfulRemoteDataObject$,
} from '@dspace/core/utilities/remote-data.utils';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import {
TranslateLoader,
Expand All @@ -61,6 +66,7 @@ import {
Observable,
of,
} from 'rxjs';
import { mockGroup } from 'src/app/submission/utils/submission.mock';

import { ContextHelpDirective } from '../../../../shared/context-help.directive';
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
Expand Down Expand Up @@ -103,6 +109,9 @@ describe('MembersListComponent', () => {
}
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), []));
},
searchMembers(query: string, groupId: string, pagination, exact: boolean, currentMembers: boolean) {
return of(createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])));
},
clearEPersonRequests() {
// empty
},
Expand Down Expand Up @@ -282,4 +291,62 @@ describe('MembersListComponent', () => {
});
});

describe('test for searchMembers', () => {
let comp: any;
let ePersonDataServiceSpy: any;
let notificationsServiceSpy: any;

beforeEach(() => {
ePersonDataServiceSpy = jasmine.createSpyObj('ePersonDataService', ['searchMembers']);
notificationsServiceSpy = jasmine.createSpyObj('notificationsService', ['error']);

comp = component as any;
comp.ePersonDataService = ePersonDataServiceSpy;
comp.notificationsService = notificationsServiceSpy;
});

it('should search members and update ePeopleMembersOfGroup', fakeAsync(() => {
const fakeGroup = mockGroup;
const fakeMember = EPersonMock;
const fakePaginatedList = { pageInfo: { totalPages: 1 }, page: [fakeMember] };
const fakeResponse = createSuccessfulRemoteDataObject$(fakePaginatedList);

comp.groupBeingEdited = fakeGroup;
ePersonDataServiceSpy.searchMembers.and.returnValue(fakeResponse);

spyOn(comp, 'isMemberOfGroup').and.returnValue(of(true));
const groupSpy = spyOn(comp.ePeopleMembersOfGroup, 'next');

comp.searchMembers({ queryCurrentMembers: 'John' });
tick();

expect(comp.ePersonDataService.searchMembers).toHaveBeenCalled();
expect(groupSpy).toHaveBeenCalled();
}));

it('should show error notification when API call fails', fakeAsync(() => {
const fakeGroup = mockGroup;
comp.groupBeingEdited = fakeGroup;

ePersonDataServiceSpy.searchMembers.and.returnValue(createFailedRemoteDataObject$('Server Error'));

comp.searchMembers({ queryCurrentMembers: 'John' });
tick();

expect(comp.notificationsService.error).toHaveBeenCalled();
}));

it('should reset the searchCurrentMembersForm and call searchMembers with empty query', () => {
comp.searchCurrentMembersForm = new FormGroup({
queryCurrentMembers: new FormControl('John Doe'),
});

const searchSpy = spyOn(comp, 'searchMembers');
comp.clearCurrentMembersFormAndResetResult();

expect(comp.searchCurrentMembersForm.value.queryCurrentMembers).toBe('');
expect(searchSpy).toHaveBeenCalledWith({ queryCurrentMembers: '' });
});
});

});
Original file line number Diff line number Diff line change
Expand Up @@ -171,9 +171,15 @@ export class MembersListComponent implements OnInit, OnDestroy {
// The search form
searchForm;

// The current member search form
searchCurrentMembersForm;

// Current search in edit group - epeople search form
currentSearchQuery: string;

// Current search in edit group - epeople current members search form
currentMembersSearchQuery: string;

// Whether or not user has done a EPeople search yet
searchDone: boolean;

Expand All @@ -194,12 +200,18 @@ export class MembersListComponent implements OnInit, OnDestroy {
public dsoNameService: DSONameService,
) {
this.currentSearchQuery = '';
this.currentMembersSearchQuery = '';
}

ngOnInit(): void {
this.searchForm = this.formBuilder.group(({
query: '',
}));

this.searchCurrentMembersForm = this.formBuilder.group(({
queryCurrentMembers: '',
}));

this.subs.set(SubKey.ActiveGroup, this.groupRegistryService.getActiveGroup().subscribe((activeGroup: Group) => {
if (activeGroup != null) {
this.groupBeingEdited = activeGroup;
Expand Down Expand Up @@ -354,6 +366,57 @@ export class MembersListComponent implements OnInit, OnDestroy {
}));
}

/**
* Search all EPeople who are a member of the current group by name, email or metadata
* @param data Contains query param
*/
searchMembers(data: any) {
this.unsubFrom(SubKey.Members);
this.subs.set(SubKey.Members,
this.paginationService.getCurrentPagination(this.config.id, this.config).pipe(
switchMap((paginationOptions) => {
const query: string = data.queryCurrentMembers;
if (query != null && this.currentMembersSearchQuery !== query && this.groupBeingEdited) {
this.currentMembersSearchQuery = query;
this.paginationService.resetPage(this.config.id);
}

return this.ePersonDataService.searchMembers(this.currentMembersSearchQuery, this.groupBeingEdited.id, {
currentPage: paginationOptions.currentPage,
elementsPerPage: paginationOptions.pageSize,
}, false, true);
}),
getAllCompletedRemoteData(),
map((rd: RemoteData<any>) => {
if (rd.hasFailed) {
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure', { cause: rd.errorMessage }));
} else {
return rd;
}
}),
switchMap((epersonListRD: RemoteData<PaginatedList<EPerson>>) => {
if (!epersonListRD || !epersonListRD.payload || !epersonListRD.payload.page) {
return of(buildPaginatedList(undefined, []));
} // added null check
const dtos$ = observableCombineLatest([...epersonListRD.payload.page.map((member: EPerson) => {
const dto$: Observable<EpersonDtoModel> = observableCombineLatest(
this.isMemberOfGroup(member), (isMember: ObservedValueOf<Observable<boolean>>) => {
const epersonDtoModel: EpersonDtoModel = new EpersonDtoModel();
epersonDtoModel.eperson = member;
epersonDtoModel.ableToDelete = isMember;
return epersonDtoModel;
});
return dto$;
})]);
return dtos$.pipe(defaultIfEmpty([]), map((dtos: EpersonDtoModel[]) => {
return buildPaginatedList(epersonListRD.payload.pageInfo, dtos);
}));
}),
).subscribe((paginatedListOfDTOs: PaginatedList<EpersonDtoModel>) => {
this.ePeopleMembersOfGroup.next(paginatedListOfDTOs);
}));
}

/**
* unsub all subscriptions
*/
Expand Down Expand Up @@ -391,4 +454,14 @@ export class MembersListComponent implements OnInit, OnDestroy {
});
this.search({ query: '' });
}

/**
* Reset all input-fields to be empty and search all search
*/
clearCurrentMembersFormAndResetResult() {
this.searchCurrentMembersForm.patchValue({
queryCurrentMembers:'',
});
this.searchMembers({ queryCurrentMembers: '' });
}
}
Loading