Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
7be5d99
Difficult to find users to delete from group in large groups
iam-vir-harshit Apr 1, 2025
8f43fa7
Merge branch 'main' into main
iam-vir-harshit May 13, 2025
b1b9375
fixed unit test
iam-vir-harshit May 14, 2025
bbeaa7d
Merge branch 'main' into main
iam-vir-harshit Jun 11, 2025
e6e9c69
solve merge conflict for imports
iam-vir-harshit Jun 11, 2025
ad8abcf
Merge branch 'DSpace:main' into main
iam-vir-harshit Aug 18, 2025
6b352d1
Fix for difficult to find users to delete from group in large groups.
im-shubham-vish Aug 18, 2025
49f7e69
Indentation Fix for difficult to find users to delete from group in l…
iam-vir-harshit Aug 19, 2025
fd69ae3
Difficult to find users to delete from group in large groups - Remove…
iam-vir-harshit Aug 25, 2025
b554ab7
Merge branch 'main' of https://github.com/iam-vir-harshit/dspace-angular
iam-vir-harshit Aug 25, 2025
b02949d
Difficult to find users to delete from group in large groups - Missin…
iam-vir-harshit Aug 25, 2025
b907e86
Difficult to find users to delete from group in large groups
iam-vir-harshit Aug 25, 2025
07f2b80
Difficult to find users to delete from group in large groups
iam-vir-harshit Aug 25, 2025
611d969
Merge branch 'DSpace:main' into main
iam-vir-harshit Oct 9, 2025
0343eef
Merge branch 'main' into main
iam-vir-harshit Nov 12, 2025
0807a2b
Fix Lint Errors
iam-vir-harshit Nov 12, 2025
fc7cd5f
Fix Lint Errors
iam-vir-harshit Nov 12, 2025
533d5c1
Merge branch 'DSpace:main' into main
iam-vir-harshit Feb 25, 2026
60cf661
Merge branch 'DSpace:main' into main
iam-vir-harshit Mar 6, 2026
f1bad18
Retrigger unit tests.
iam-vir-harshit Mar 6, 2026
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 @@ -599,6 +599,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) {
Comment thread
iam-vir-harshit marked this conversation as resolved.
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 @@ -170,9 +170,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 @@ -193,12 +199,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 @@ -353,6 +365,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 @@ -390,4 +453,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