Skip to content

Commit 7be5d99

Browse files
Difficult to find users to delete from group in large groups
1 parent 9c23b4c commit 7be5d99

File tree

10 files changed

+383
-82
lines changed

10 files changed

+383
-82
lines changed

src/app/access-control/epeople-registry/eperson-form/eperson-form.component.html

Lines changed: 73 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -3,109 +3,102 @@
33
<div class="col-12">
44

55
@if (activeEPerson$ | async) {
6-
<h1 class="border-bottom pb-2">{{messagePrefix + '.edit' | translate}}</h1>
6+
<h1 class="border-bottom pb-2">{{messagePrefix + '.edit' | translate}}</h1>
77
} @else {
8-
<h1 class="border-bottom pb-2">{{messagePrefix + '.create' | translate}}</h1>
8+
<h1 class="border-bottom pb-2">{{messagePrefix + '.create' | translate}}</h1>
99
}
1010

1111

1212

13-
<ds-form [formId]="formId"
14-
[formModel]="formModel"
15-
[formGroup]="formGroup"
16-
[formLayout]="formLayout"
17-
[displayCancel]="false"
18-
[submitLabel]="submitLabel"
19-
(submitForm)="onSubmit()">
13+
<ds-form [formId]="formId" [formModel]="formModel" [formGroup]="formGroup" [formLayout]="formLayout"
14+
[displayCancel]="false" [submitLabel]="submitLabel" (submitForm)="onSubmit()">
2015
<div before class="btn-group">
2116
<button (click)="onCancel()" type="button" class="btn btn-outline-secondary">
2217
<i class="fas fa-arrow-left"></i> {{messagePrefix + '.return' | translate}}
2318
</button>
2419
</div>
2520
@if (displayResetPassword) {
26-
<div between class="btn-group">
27-
<button class="btn btn-primary" [dsBtnDisabled]="(canReset$ | async) !== true" type="button" (click)="resetPassword()">
28-
<i class="fa fa-key"></i> {{'admin.access-control.epeople.actions.reset' | translate}}
29-
</button>
30-
</div>
21+
<div between class="btn-group">
22+
<button class="btn btn-primary" [dsBtnDisabled]="(canReset$ | async) !== true" type="button"
23+
(click)="resetPassword()">
24+
<i class="fa fa-key"></i> {{'admin.access-control.epeople.actions.reset' | translate}}
25+
</button>
26+
</div>
3127
}
3228
@if (canImpersonate$ | async) {
33-
<div between class="btn-group ms-1">
34-
@if (!isImpersonated) {
35-
<button class="btn btn-primary" type="button" (click)="impersonate()">
36-
<i class="fa fa-user-secret"></i> {{'admin.access-control.epeople.actions.impersonate' | translate}}
37-
</button>
38-
}
39-
@if (isImpersonated) {
40-
<button class="btn btn-primary" type="button" (click)="stopImpersonating()">
41-
<i class="fa fa-user-secret"></i> {{'admin.access-control.epeople.actions.stop-impersonating' | translate}}
42-
</button>
43-
}
44-
</div>
29+
<div between class="btn-group ms-1">
30+
@if (!isImpersonated) {
31+
<button class="btn btn-primary" type="button" (click)="impersonate()">
32+
<i class="fa fa-user-secret"></i> {{'admin.access-control.epeople.actions.impersonate' | translate}}
33+
</button>
34+
}
35+
@if (isImpersonated) {
36+
<button class="btn btn-primary" type="button" (click)="stopImpersonating()">
37+
<i class="fa fa-user-secret"></i> {{'admin.access-control.epeople.actions.stop-impersonating' | translate}}
38+
</button>
39+
}
40+
</div>
4541
}
4642
@if (canDelete$ | async) {
47-
<button after class="btn btn-danger delete-button" type="button" (click)="delete()">
48-
<i class="fas fa-trash"></i> {{'admin.access-control.epeople.actions.delete' | translate}}
49-
</button>
43+
<button after class="btn btn-danger delete-button" type="button" (click)="delete()">
44+
<i class="fas fa-trash"></i> {{'admin.access-control.epeople.actions.delete' | translate}}
45+
</button>
5046
}
5147
</ds-form>
5248

5349
@if (!formGroup) {
54-
<ds-loading [showMessage]="false"></ds-loading>
50+
<ds-loading [showMessage]="false"></ds-loading>
5551
}
5652

5753
@if (activeEPerson$ | async) {
58-
<div>
59-
<h2>{{messagePrefix + '.groupsEPersonIsMemberOf' | translate}}</h2>
60-
@if (groups$ | async | dsHasNoValue) {
61-
<ds-loading [showMessage]="false"></ds-loading>
62-
}
63-
@if ((groups$ | async)?.payload?.totalElements > 0) {
64-
<ds-pagination
65-
[paginationOptions]="config"
66-
[collectionSize]="(groups$ | async)?.payload?.totalElements"
67-
[hideGear]="true"
68-
[hidePagerWhenSinglePage]="true"
69-
(pageChange)="onPageChange($event)">
70-
<div class="table-responsive">
71-
<table id="groups" class="table table-striped table-hover table-bordered">
72-
<thead>
73-
<tr>
74-
<th scope="col" class="align-middle">{{messagePrefix + '.table.id' | translate}}</th>
75-
<th scope="col" class="align-middle">{{messagePrefix + '.table.name' | translate}}</th>
76-
<th scope="col" class="align-middle">{{messagePrefix + '.table.collectionOrCommunity' | translate}}</th>
77-
</tr>
78-
</thead>
79-
<tbody>
80-
@for (group of (groups$ | async)?.payload?.page; track group) {
81-
<tr>
82-
<td class="align-middle">{{group.id}}</td>
83-
<td class="align-middle">
84-
<a (click)="groupsDataService.startEditingNewGroup(group)"
85-
[routerLink]="[groupsDataService.getGroupEditPageRouterLink(group)]">
86-
{{ dsoNameService.getName(group) }}
87-
</a>
88-
</td>
89-
<td class="align-middle">
90-
{{ dsoNameService.getName((group.object | async)?.payload) }}
91-
</td>
92-
</tr>
93-
}
94-
</tbody>
95-
</table>
96-
</div>
97-
</ds-pagination>
98-
}
99-
@if ((groups$ | async)?.payload?.totalElements === 0) {
100-
<div class="alert alert-info w-100 mb-2" role="alert">
101-
<div>{{messagePrefix + '.memberOfNoGroups' | translate}}</div>
102-
<div>
103-
<button [routerLink]="[groupsDataService.getGroupRegistryRouterLink()]"
104-
class="btn btn-primary">{{messagePrefix + '.goToGroups' | translate}}</button>
105-
</div>
106-
</div>
107-
}
54+
<div>
55+
<h2>{{messagePrefix + '.groupsEPersonIsMemberOf' | translate}}</h2>
56+
@if (groups$ | async | dsHasNoValue) {
57+
<ds-loading [showMessage]="false"></ds-loading>
58+
}
59+
@if ((groups$ | async)?.payload?.totalElements > 0) {
60+
<ds-pagination [paginationOptions]="config" [collectionSize]="(groups$ | async)?.payload?.totalElements"
61+
[hideGear]="true" [hidePagerWhenSinglePage]="true" (pageChange)="onPageChange($event)">
62+
<div class="table-responsive">
63+
<table id="groups" class="table table-striped table-hover table-bordered">
64+
<thead>
65+
<tr>
66+
<th scope="col" class="align-middle">{{messagePrefix + '.table.id' | translate}}</th>
67+
<th scope="col" class="align-middle">{{messagePrefix + '.table.name' | translate}}</th>
68+
<th scope="col" class="align-middle">{{messagePrefix + '.table.collectionOrCommunity' | translate}}
69+
</th>
70+
</tr>
71+
</thead>
72+
<tbody>
73+
@for (group of (groups$ | async)?.payload?.page; track group) {
74+
<tr>
75+
<td class="align-middle">{{group.id}}</td>
76+
<td class="align-middle">
77+
<a (click)="groupsDataService.startEditingNewGroup(group)"
78+
[routerLink]="[groupsDataService.getGroupEditPageRouterLink(group)]">
79+
{{ dsoNameService.getName(group) }}
80+
</a>
81+
</td>
82+
<td class="align-middle">
83+
{{ dsoNameService.getName((group.object | async)?.payload) }}
84+
</td>
85+
</tr>
86+
}
87+
</tbody>
88+
</table>
89+
</div>
90+
</ds-pagination>
91+
}
92+
@if ((groups$ | async)?.payload?.totalElements === 0) {
93+
<div class="alert alert-info w-100 mb-2" role="alert">
94+
<div>{{messagePrefix + '.memberOfNoGroups' | translate}}</div>
95+
<div>
96+
<button [routerLink]="[groupsDataService.getGroupRegistryRouterLink()]"
97+
class="btn btn-primary">{{messagePrefix + '.goToGroups' | translate}}</button>
98+
</div>
10899
</div>
100+
}
101+
</div>
109102
}
110103
</div>
111104
</div>

src/app/access-control/epeople-registry/eperson-form/eperson-form.component.spec.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,10 @@ import { ThemedLoadingComponent } from '../../../shared/loading/themed-loading.c
5050
import { getMockFormBuilderService } from '../../../shared/mocks/form-builder-service.mock';
5151
import { NotificationsService } from '../../../shared/notifications/notifications.service';
5252
import { PaginationComponent } from '../../../shared/pagination/pagination.component';
53-
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
53+
import {
54+
createFailedRemoteDataObject$,
55+
createSuccessfulRemoteDataObject$,
56+
} from '../../../shared/remote-data.utils';
5457
import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub';
5558
import { AuthServiceStub } from '../../../shared/testing/auth-service.stub';
5659
import {
@@ -216,6 +219,7 @@ describe('EPersonFormComponent', () => {
216219
groupsDataService = jasmine.createSpyObj('groupsDataService', {
217220
findListByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])),
218221
getGroupRegistryRouterLink: '',
222+
deleteMemberFromGroup: 'deleteMemberFromGroup',
219223
});
220224

221225
paginationService = new PaginationServiceStub();
@@ -525,6 +529,47 @@ describe('EPersonFormComponent', () => {
525529
});
526530
});
527531

532+
describe('delete group from member', () => {
533+
534+
it('should delete group from member and show notification', (done) => {
535+
const group = { id: 'group1' } as any;
536+
const activeEPerson = EPersonMock;
537+
538+
spyOn(component.epersonService, 'getActiveEPerson').and.returnValue(observableOf(activeEPerson));
539+
(groupsDataService.deleteMemberFromGroup as jasmine.Spy)
540+
.and.returnValue(createSuccessfulRemoteDataObject$(null));
541+
542+
spyOn(component.dsoNameService, 'getName').and.returnValue('Mock Group Name');
543+
544+
const notifySpy = spyOn(component, 'showNotifications').and.callFake(() => {
545+
expect(groupsDataService.deleteMemberFromGroup).toHaveBeenCalledWith(group, activeEPerson);
546+
expect(notifySpy).toHaveBeenCalled();
547+
done();
548+
});
549+
550+
component.deleteGroupFromMember(group);
551+
});
552+
553+
it('should show success notification on successful operation', () => {
554+
const response = createSuccessfulRemoteDataObject$(null);
555+
const successSpy = spyOn((component as any).notificationsService, 'success');
556+
557+
component.showNotifications('deleteMembership', response, 'TestGroup', EPersonMock);
558+
559+
expect(successSpy).toHaveBeenCalled();
560+
});
561+
562+
it('should show error notification when response hasSucceeded is false', () => {
563+
const response = createFailedRemoteDataObject$(null);
564+
const errorSpy = spyOn((component as any).notificationsService, 'error');
565+
566+
component.showNotifications('deleteMembership', response, 'TestGroup', EPersonMock);
567+
568+
expect(errorSpy).toHaveBeenCalled();
569+
});
570+
571+
});
572+
528573
describe('Reset Password', () => {
529574
let ePersonId;
530575
let ePersonEmail;

src/app/access-control/epeople-registry/eperson-form/eperson-form.component.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -595,6 +595,38 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
595595
}
596596
}
597597

598+
/**
599+
* Deletes a given group from the Group list of the eperson currently being edited present in
600+
* @param group group we want to delete as of which the current eperson being edited is member of
601+
*/
602+
deleteGroupFromMember(group: Group) {
603+
this.epersonService.getActiveEPerson().pipe(take(1)).subscribe((activeEPerson: EPerson) => {
604+
if (activeEPerson != null) {
605+
const response = this.groupsDataService.deleteMemberFromGroup(group, activeEPerson);
606+
this.showNotifications('deleteMembership', response, this.dsoNameService.getName(group), activeEPerson);
607+
} else {
608+
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveEPerson'));
609+
}
610+
});
611+
}
612+
613+
/**
614+
* Shows a notification based on the success/failure of the request
615+
* @param messageSuffix Suffix for message
616+
* @param response RestResponse observable containing success/failure request
617+
* @param nameObject Object request was about
618+
* @param activeEPerson EPerson currently being edited
619+
*/
620+
showNotifications(messageSuffix: string, response: Observable<RemoteData<any>>, nameObject: string, activeEPerson: EPerson) {
621+
response.pipe(getFirstCompletedRemoteData()).subscribe((rd: RemoteData<any>) => {
622+
if (rd.hasSucceeded) {
623+
this.notificationsService.success(this.translateService.get(this.messagePrefix + '.notification.success.' + messageSuffix, { name: nameObject }));
624+
} else {
625+
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.' + messageSuffix, { name: nameObject }));
626+
}
627+
});
628+
}
629+
598630
/**
599631
* Cancel the current edit when component is destroyed & unsub all subscriptions
600632
*/

src/app/access-control/group-registry/group-form/members-list/members-list.component.html

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,23 @@
22
<h2 class="border-bottom pb-2">{{messagePrefix + '.head' | translate}}</h2>
33

44
<h3>{{messagePrefix + '.headMembers' | translate}}</h3>
5+
6+
<form [formGroup]="searchCurrentMembersForm" (ngSubmit)="searchMembers(searchCurrentMembersForm.value)" class="d-flex justify-content-between gap-3">
7+
<div class="flex-grow-1 mr-3">
8+
<div class="form-group input-group mr-3">
9+
<input type="text" name="queryCurrentMembers" id="queryCurrentMembers" formControlName="queryCurrentMembers"
10+
class="form-control" aria-label="Search input">
11+
<span class="input-group-append">
12+
<button type="submit" class="search-button btn btn-primary">
13+
<i class="fas fa-search"></i> {{ messagePrefix + '.search.button' | translate }}</button>
14+
</span>
15+
</div>
16+
</div>
17+
<div>
18+
<button (click)="clearCurrentMembersFormAndResetResult()"
19+
class="btn btn-secondary">{{messagePrefix + '.button.see-all' | translate}}</button>
20+
</div>
21+
</form>
522

623
@if ((ePeopleMembersOfGroup | async)?.totalElements > 0) {
724
<ds-pagination

0 commit comments

Comments
 (0)