[groups/epeople admin pages] Changes/Fixes:

* Added check with Group.permanent on whether or not to show delete button
* Disabled groupName input field if group that is being edited is permanent
* Cancel edit group button sends you back to group registry now
* In group edit, cache only gets cleared of that groups subgroups and epersons cache (at member delete/add) instead of cleared of all groups info
* Separated search groups result list and list of subgroups in edit group page and eperson members of edit group page
* Fixed pagination after search issues in groups registry, group edit page (groups search, subgroups, epeople search and members)
* Applied same fixes to epeople registry
* Browse All button added to all group/eperson searches to browse all groups/epeople
* Fixed immediately being able to add members after group creation in edit group page
* Fixed hover i18n tags on groups & epeople registry page => Missing tags
* Fixed tests after changes
This commit is contained in:
Marie Verdonck
2020-03-30 11:10:02 +02:00
parent 54b351c0d3
commit 48d7f6987e
21 changed files with 626 additions and 291 deletions

View File

@@ -176,6 +176,8 @@
"admin.access-control.epeople.search.head": "Search", "admin.access-control.epeople.search.head": "Search",
"admin.access-control.epeople.button.see-all": "Browse All",
"admin.access-control.epeople.search.scope.metadata": "Metadata", "admin.access-control.epeople.search.scope.metadata": "Metadata",
"admin.access-control.epeople.search.scope.email": "E-mail (exact)", "admin.access-control.epeople.search.scope.email": "E-mail (exact)",
@@ -192,9 +194,9 @@
"admin.access-control.epeople.table.edit": "Edit", "admin.access-control.epeople.table.edit": "Edit",
"admin.access-control.epeople.table.edit.buttons.edit": "Edit", "admin.access-control.epeople.table.edit.buttons.edit": "Edit \"{{name}}\"",
"admin.access-control.epeople.table.edit.buttons.remove": "Remove", "admin.access-control.epeople.table.edit.buttons.remove": "Delete \"{{name}}\"",
"admin.access-control.epeople.no-items": "No EPeople to show.", "admin.access-control.epeople.no-items": "No EPeople to show.",
@@ -250,6 +252,8 @@
"admin.access-control.groups.search.head": "Search groups", "admin.access-control.groups.search.head": "Search groups",
"admin.access-control.groups.button.see-all": "Browse all",
"admin.access-control.groups.search.button": "Search", "admin.access-control.groups.search.button": "Search",
"admin.access-control.groups.table.id": "ID", "admin.access-control.groups.table.id": "ID",
@@ -262,6 +266,10 @@
"admin.access-control.groups.table.edit": "Edit", "admin.access-control.groups.table.edit": "Edit",
"admin.access-control.groups.table.edit.buttons.edit": "Edit \"{{name}}\"",
"admin.access-control.groups.table.edit.buttons.remove": "Delete \"{{name}}\"",
"admin.access-control.groups.no-items": "No groups found with this in their name or this as UUID", "admin.access-control.groups.no-items": "No groups found with this in their name or this as UUID",
"admin.access-control.groups.notification.deleted.success": "Successfully deleted group \"{{name}}\"", "admin.access-control.groups.notification.deleted.success": "Successfully deleted group \"{{name}}\"",
@@ -283,10 +291,14 @@
"admin.access-control.groups.form.notification.created.failure.groupNameInUse": "Failed to create Group with name: \"{{name}}\", make sure the name is not already in use.", "admin.access-control.groups.form.notification.created.failure.groupNameInUse": "Failed to create Group with name: \"{{name}}\", make sure the name is not already in use.",
"admin.access-control.groups.form.members-list.head": "Members", "admin.access-control.groups.form.members-list.head": "EPeople",
"admin.access-control.groups.form.members-list.search.head": "Search EPeople", "admin.access-control.groups.form.members-list.search.head": "Search EPeople",
"admin.access-control.groups.form.members-list.button.see-all": "Browse All",
"admin.access-control.groups.form.members-list.headMembers": "Browse Members",
"admin.access-control.groups.form.members-list.search.scope.metadata": "Metadata", "admin.access-control.groups.form.members-list.search.scope.metadata": "Metadata",
"admin.access-control.groups.form.members-list.search.scope.email": "E-mail (exact)", "admin.access-control.groups.form.members-list.search.scope.email": "E-mail (exact)",
@@ -315,14 +327,16 @@
"admin.access-control.groups.form.members-list.no-members-yet": "No members in group yet, search and add.", "admin.access-control.groups.form.members-list.no-members-yet": "No members in group yet, search and add.",
"admin.access-control.groups.form.members-list.button.see-all": "Search all",
"admin.access-control.groups.form.members-list.no-items": "No EPeople found in that search", "admin.access-control.groups.form.members-list.no-items": "No EPeople found in that search",
"admin.access-control.groups.form.subgroups-list.head": "Subgroups", "admin.access-control.groups.form.subgroups-list.head": "Groups",
"admin.access-control.groups.form.subgroups-list.search.head": "Search Groups", "admin.access-control.groups.form.subgroups-list.search.head": "Search Groups",
"admin.access-control.groups.form.subgroups-list.button.see-all": "Browse All",
"admin.access-control.groups.form.subgroups-list.headSubgroups": "Browse Subgroups",
"admin.access-control.groups.form.subgroups-list.search.button": "Search", "admin.access-control.groups.form.subgroups-list.search.button": "Search",
"admin.access-control.groups.form.subgroups-list.table.id": "ID", "admin.access-control.groups.form.subgroups-list.table.id": "ID",
@@ -347,9 +361,7 @@
"admin.access-control.groups.form.subgroups-list.no-items": "No groups found with this in their name or this as UUID", "admin.access-control.groups.form.subgroups-list.no-items": "No groups found with this in their name or this as UUID",
"admin.access-control.groups.form.subgroups-list.no-subgroups-yet": "No subgroups in group yet, search and add.", "admin.access-control.groups.form.subgroups-list.no-subgroups-yet": "No subgroups in group yet.",
"admin.access-control.groups.form.subgroups-list.button.see-all": "Search all",
"admin.access-control.groups.form.return": "Return to groups", "admin.access-control.groups.form.return": "Return to groups",

View File

@@ -15,7 +15,10 @@
</button> </button>
</div> </div>
<h3 id="search" class="border-bottom pb-2">{{labelPrefix + 'search.head' | translate}}</h3> <h3 id="search" class="border-bottom pb-2">{{labelPrefix + 'search.head' | translate}}
<button (click)="clearFormAndResetResult();"
class="btn btn-primary float-right">{{labelPrefix + 'button.see-all' | translate}}</button>
</h3>
<form [formGroup]="searchForm" (ngSubmit)="search(searchForm.value)" class="row"> <form [formGroup]="searchForm" (ngSubmit)="search(searchForm.value)" class="row">
<div class="col-12 col-sm-3"> <div class="col-12 col-sm-3">
<select name="scope" id="scope" formControlName="scope" class="form-control" aria-label="Search scope"> <select name="scope" id="scope" formControlName="scope" class="form-control" aria-label="Search scope">
@@ -64,12 +67,12 @@
<div class="btn-group edit-field"> <div class="btn-group edit-field">
<button (click)="toggleEditEPerson(eperson)" <button (click)="toggleEditEPerson(eperson)"
class="btn btn-outline-primary btn-sm access-control-editEPersonButton" class="btn btn-outline-primary btn-sm access-control-editEPersonButton"
title="{{labelPrefix + 'table.edit.buttons.edit' | translate}}"> title="{{labelPrefix + 'table.edit.buttons.edit' | translate: {name: eperson.name} }}">
<i class="fas fa-edit fa-fw"></i> <i class="fas fa-edit fa-fw"></i>
</button> </button>
<button (click)="deleteEPerson(eperson)" <button (click)="deleteEPerson(eperson)"
class="btn btn-outline-danger btn-sm access-control-deleteEPersonButton" class="btn btn-outline-danger btn-sm access-control-deleteEPersonButton"
title="{{labelPrefix + 'table.edit.buttons.remove' | translate}}"> title="{{labelPrefix + 'table.edit.buttons.remove' | translate: {name: eperson.name} }}">
<i class="fas fa-trash-alt fa-fw"></i> <i class="fas fa-trash-alt fa-fw"></i>
</button> </button>
</div> </div>

View File

@@ -1,3 +1,4 @@
import { Router } from '@angular/router';
import { of as observableOf } from 'rxjs'; import { of as observableOf } from 'rxjs';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { NO_ERRORS_SCHEMA } from '@angular/core'; import { NO_ERRORS_SCHEMA } from '@angular/core';
@@ -20,6 +21,7 @@ import { getMockTranslateService } from '../../../shared/mocks/mock-translate.se
import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { EPersonMock, EPersonMock2 } from '../../../shared/testing/eperson-mock'; import { EPersonMock, EPersonMock2 } from '../../../shared/testing/eperson-mock';
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub'; import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub';
import { RouterStub } from '../../../shared/testing/router-stub';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils';
import { EPeopleRegistryComponent } from './epeople-registry.component'; import { EPeopleRegistryComponent } from './epeople-registry.component';
@@ -51,6 +53,9 @@ describe('EPeopleRegistryComponent', () => {
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [result])); return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [result]));
} }
if (scope === 'metadata') { if (scope === 'metadata') {
if (query === '') {
return createSuccessfulRemoteDataObject$(new PaginatedList(null, this.allEpeople));
}
const result = this.allEpeople.find((ePerson: EPerson) => { const result = this.allEpeople.find((ePerson: EPerson) => {
return (ePerson.name.includes(query) || ePerson.email.includes(query)) return (ePerson.name.includes(query) || ePerson.email.includes(query))
}); });
@@ -72,6 +77,9 @@ describe('EPeopleRegistryComponent', () => {
}, },
clearEPersonRequests(): void { clearEPersonRequests(): void {
// empty // empty
},
getEPeoplePageRouterLink(): string {
return '/admin/access-control/epeople';
} }
}; };
builderService = getMockFormBuilderService(); builderService = getMockFormBuilderService();
@@ -89,7 +97,8 @@ describe('EPeopleRegistryComponent', () => {
providers: [EPeopleRegistryComponent, providers: [EPeopleRegistryComponent,
{ provide: EPersonDataService, useValue: ePersonDataServiceStub }, { provide: EPersonDataService, useValue: ePersonDataServiceStub },
{ provide: NotificationsService, useValue: new NotificationsServiceStub() }, { provide: NotificationsService, useValue: new NotificationsServiceStub() },
{ provide: FormBuilderService, useValue: builderService } { provide: FormBuilderService, useValue: builderService },
{ provide: Router, useValue: new RouterStub() },
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).compileComponents(); }).compileComponents();

View File

@@ -1,5 +1,6 @@
import { Component, OnDestroy, OnInit } from '@angular/core'; import { Component, OnDestroy, OnInit } from '@angular/core';
import { FormBuilder } from '@angular/forms'; import { FormBuilder } from '@angular/forms';
import { Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { Subscription } from 'rxjs/internal/Subscription'; import { Subscription } from 'rxjs/internal/Subscription';
@@ -46,6 +47,10 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
// The search form // The search form
searchForm; searchForm;
// Current search in epersons registry
currentSearchQuery: string;
currentSearchScope: string;
/** /**
* List of subscriptions * List of subscriptions
*/ */
@@ -54,24 +59,24 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
constructor(private epersonService: EPersonDataService, constructor(private epersonService: EPersonDataService,
private translateService: TranslateService, private translateService: TranslateService,
private notificationsService: NotificationsService, private notificationsService: NotificationsService,
private formBuilder: FormBuilder) { private formBuilder: FormBuilder,
private router: Router) {
this.currentSearchQuery = '';
this.currentSearchScope = 'metadata';
this.searchForm = this.formBuilder.group(({
scope: 'metadata',
query: '',
}));
} }
ngOnInit() { ngOnInit() {
this.isEPersonFormShown = false; this.isEPersonFormShown = false;
this.updateEPeople({ this.search({ scope: this.currentSearchScope, query: this.currentSearchQuery });
currentPage: 1,
elementsPerPage: this.config.pageSize
});
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => { this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
if (eperson != null && eperson.id) { if (eperson != null && eperson.id) {
this.isEPersonFormShown = true; this.isEPersonFormShown = true;
} }
})); }));
this.searchForm = this.formBuilder.group(({
scope: 'metadata',
query: '',
}));
} }
/** /**
@@ -79,17 +84,8 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
* @param event * @param event
*/ */
onPageChange(event) { onPageChange(event) {
this.updateEPeople({ this.config.currentPage = event;
currentPage: event, this.search({ scope: this.currentSearchScope, query: this.currentSearchQuery })
elementsPerPage: this.config.pageSize
});
}
/**
* Update the list of EPeople by fetching it from the rest api or cache
*/
private updateEPeople(options) {
this.ePeople = this.epersonService.getEPeople(options);
} }
/** /**
@@ -107,8 +103,20 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
* @param data Contains scope and query param * @param data Contains scope and query param
*/ */
search(data: any) { search(data: any) {
this.ePeople = this.epersonService.searchByScope(data.scope, data.query, { const query: string = data.query;
currentPage: 1, const scope: string = data.scope;
if (query != null && this.currentSearchQuery !== query) {
this.router.navigateByUrl(this.epersonService.getEPeoplePageRouterLink());
this.currentSearchQuery = query;
this.config.currentPage = 1;
}
if (scope != null && this.currentSearchScope !== scope) {
this.router.navigateByUrl(this.epersonService.getEPeoplePageRouterLink());
this.currentSearchScope = scope;
this.config.currentPage = 1;
}
this.ePeople = this.epersonService.searchByScope(this.currentSearchScope, this.currentSearchQuery, {
currentPage: this.config.currentPage,
elementsPerPage: this.config.pageSize elementsPerPage: this.config.pageSize
}); });
} }
@@ -181,4 +189,14 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
} }
})(); })();
} }
/**
* Reset all input-fields to be empty and search all search
*/
clearFormAndResetResult() {
this.searchForm.patchValue({
query: '',
});
this.search({ query: '' });
}
} }

View File

@@ -60,6 +60,9 @@ describe('EPersonFormComponent', () => {
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [result])); return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [result]));
} }
if (scope === 'metadata') { if (scope === 'metadata') {
if (query === '') {
return createSuccessfulRemoteDataObject$(new PaginatedList(null, this.allEpeople));
}
const result = this.allEpeople.find((ePerson: EPerson) => { const result = this.allEpeople.find((ePerson: EPerson) => {
return (ePerson.name.includes(query) || ePerson.email.includes(query)) return (ePerson.name.includes(query) || ePerson.email.includes(query))
}); });

View File

@@ -234,7 +234,6 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
onSubmit() { onSubmit() {
this.epersonService.getActiveEPerson().pipe(take(1)).subscribe( this.epersonService.getActiveEPerson().pipe(take(1)).subscribe(
(ePerson: EPerson) => { (ePerson: EPerson) => {
console.log('onsubmit ep', ePerson)
const values = { const values = {
metadata: { metadata: {
'eperson.firstname': [ 'eperson.firstname': [
@@ -345,19 +344,6 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
})); }));
} }
/**
* Reset all input-fields to be empty
*/
clearFields() {
this.formGroup.patchValue({
firstName: '',
lastName: '',
email: '',
canLogin: true,
requireCertificate: false
});
}
/** /**
* Event triggered when the user changes page * Event triggered when the user changes page
* @param event * @param event

View File

@@ -20,11 +20,12 @@
(submitForm)="onSubmit()"> (submitForm)="onSubmit()">
</ds-form> </ds-form>
<ds-subgroups-list [messagePrefix]="messagePrefix + '.subgroups-list'"></ds-subgroups-list>
<ds-members-list [messagePrefix]="messagePrefix + '.members-list'"></ds-members-list> <ds-members-list [messagePrefix]="messagePrefix + '.members-list'"></ds-members-list>
<ds-subgroups-list [messagePrefix]="messagePrefix + '.subgroups-list'"></ds-subgroups-list>
<div> <div>
<button [routerLink]="[this.groupDataService.getGroupRegistryRouterLink()]" class="btn btn-primary">{{messagePrefix + '.return' | translate}}</button> <button [routerLink]="[this.groupDataService.getGroupRegistryRouterLink()]"
class="btn btn-primary">{{messagePrefix + '.return' | translate}}</button>
</div> </div>
</div> </div>

View File

@@ -17,7 +17,7 @@ import { EPersonDataService } from '../../../../core/eperson/eperson-data.servic
import { GroupDataService } from '../../../../core/eperson/group-data.service'; import { GroupDataService } from '../../../../core/eperson/group-data.service';
import { Group } from '../../../../core/eperson/models/group.model'; import { Group } from '../../../../core/eperson/models/group.model';
import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators'; import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators';
import { hasValue } from '../../../../shared/empty.util'; import { hasValue, isNotEmpty } from '../../../../shared/empty.util';
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service'; import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
import { NotificationsService } from '../../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../../shared/notifications/notifications.service';
@@ -121,11 +121,16 @@ export class GroupFormComponent implements OnInit, OnDestroy {
this.groupDescription this.groupDescription
]; ];
this.formGroup = this.formBuilderService.createFormGroup(this.formModel); this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
this.subs.push(this.groupDataService.getActiveGroup().subscribe((group: Group) => { this.subs.push(this.groupDataService.getActiveGroup().subscribe((activeGroup: Group) => {
this.formGroup.patchValue({ if (activeGroup != null) {
groupName: group != null ? group.name : '', this.formGroup.patchValue({
groupDescription: group != null ? group.firstMetadataValue('dc.description') : '', groupName: activeGroup != null ? activeGroup.name : '',
}); groupDescription: activeGroup != null ? activeGroup.firstMetadataValue('dc.description') : '',
});
if (activeGroup.permanent) {
this.formGroup.get('groupName').disable();
}
}
})); }));
}); });
} }
@@ -136,6 +141,7 @@ export class GroupFormComponent implements OnInit, OnDestroy {
onCancel() { onCancel() {
this.groupDataService.cancelEditGroup(); this.groupDataService.cancelEditGroup();
this.cancelForm.emit(); this.cancelForm.emit();
this.router.navigate([this.groupDataService.getGroupRegistryRouterLink()]);
} }
/** /**
@@ -175,8 +181,12 @@ export class GroupFormComponent implements OnInit, OnDestroy {
const response = this.groupDataService.tryToCreate(groupToCreate); const response = this.groupDataService.tryToCreate(groupToCreate);
response.pipe(take(1)).subscribe((restResponse: RestResponse) => { response.pipe(take(1)).subscribe((restResponse: RestResponse) => {
if (restResponse.isSuccessful) { if (restResponse.isSuccessful) {
this.notificationsService.success(this.translateService.get(this.messagePrefix + '.notification.created.success', { name: groupToCreate.name })); this.notificationsService.success(this.translateService.get(this.messagePrefix + '.notification.created.success', { name: groupToCreate.name }));
this.submitForm.emit(groupToCreate); this.submitForm.emit(groupToCreate);
const resp: any = restResponse;
if (isNotEmpty(resp.resourceSelfLinks)) {
this.setActiveGroupWithLink(resp.resourceSelfLinks[0]);
}
} else { } else {
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.created.failure', { name: groupToCreate.name })); this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.created.failure', { name: groupToCreate.name }));
this.showNotificationIfNameInUse(groupToCreate, 'created'); this.showNotificationIfNameInUse(groupToCreate, 'created');
@@ -219,7 +229,7 @@ export class GroupFormComponent implements OnInit, OnDestroy {
/** /**
* Start editing the selected group * Start editing the selected group
* @param group * @param groupId ID of group to set as active
*/ */
setActiveGroup(groupId: string) { setActiveGroup(groupId: string) {
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => { this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => {
@@ -236,6 +246,25 @@ export class GroupFormComponent implements OnInit, OnDestroy {
}); });
} }
/**
* Start editing the selected group
* @param groupSelfLink SelfLink of group to set as active
*/
setActiveGroupWithLink(groupSelfLink: string) {
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => {
if (activeGroup === null) {
this.groupDataService.cancelEditGroup();
this.groupDataService.findByHref(groupSelfLink)
.pipe(
getSucceededRemoteData(),
getRemoteDataPayload())
.subscribe((group: Group) => {
this.groupDataService.editGroup(group);
})
}
});
}
/** /**
* Cancel the current edit when component is destroyed & unsub all subscriptions * Cancel the current edit when component is destroyed & unsub all subscriptions
*/ */

View File

@@ -1,7 +1,10 @@
<ng-container> <ng-container>
<h3 class="border-bottom pb-2">{{messagePrefix + '.head' | translate}}</h3> <h3 class="border-bottom pb-2">{{messagePrefix + '.head' | translate}}</h3>
<h4 id="search" class="border-bottom pb-2">{{messagePrefix + '.search.head' | translate}}</h4> <h4 id="search" class="border-bottom pb-2">{{messagePrefix + '.search.head' | translate}}
<button (click)="clearFormAndResetResult();"
class="btn btn-primary float-right">{{messagePrefix + '.button.see-all' | translate}}</button>
</h4>
<form [formGroup]="searchForm" (ngSubmit)="search(searchForm.value)" class="row"> <form [formGroup]="searchForm" (ngSubmit)="search(searchForm.value)" class="row">
<div class="col-12 col-sm-3"> <div class="col-12 col-sm-3">
<select name="scope" id="scope" formControlName="scope" class="form-control" aria-label="Search scope"> <select name="scope" id="scope" formControlName="scope" class="form-control" aria-label="Search scope">
@@ -21,16 +24,16 @@
</div> </div>
</form> </form>
<ds-pagination *ngIf="(ePeople | async)?.payload.totalElements > 0" <ds-pagination *ngIf="(ePeopleSearch | async)?.payload.totalElements > 0"
[paginationOptions]="config" [paginationOptions]="configSearch"
[pageInfoState]="(ePeople | async)?.payload" [pageInfoState]="(ePeopleSearch | async)?.payload"
[collectionSize]="(ePeople | async)?.payload?.totalElements" [collectionSize]="(ePeopleSearch | async)?.payload?.totalElements"
[hideGear]="true" [hideGear]="true"
[hidePagerWhenSinglePage]="true" [hidePagerWhenSinglePage]="true"
(pageChange)="onPageChange($event)"> (pageChange)="onPageChangeSearch($event)">
<div class="table-responsive"> <div class="table-responsive">
<table id="epersons" class="table table-striped table-hover table-bordered"> <table id="epersonsSearch" class="table table-striped table-hover table-bordered">
<thead> <thead>
<tr> <tr>
<th scope="col">{{messagePrefix + '.table.id' | translate}}</th> <th scope="col">{{messagePrefix + '.table.id' | translate}}</th>
@@ -39,7 +42,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr *ngFor="let ePerson of (ePeople | async)?.payload?.page"> <tr *ngFor="let ePerson of (ePeopleSearch | async)?.payload?.page">
<td>{{ePerson.id}}</td> <td>{{ePerson.id}}</td>
<td><a (click)="ePersonDataService.startEditingNewEPerson(ePerson)" <td><a (click)="ePersonDataService.startEditingNewEPerson(ePerson)"
[routerLink]="[ePersonDataService.getEPeoplePageRouterLink()]">{{ePerson.name}}</a></td> [routerLink]="[ePersonDataService.getEPeoplePageRouterLink()]">{{ePerson.name}}</a></td>
@@ -67,16 +70,55 @@
</ds-pagination> </ds-pagination>
<div *ngIf="(ePeople | async)?.payload.totalElements == 0 && !searchDone" class="alert alert-info w-100 mb-2" <div *ngIf="(ePeopleSearch | async)?.payload.totalElements == 0 && searchDone"
role="alert"> class="alert alert-info w-100 mb-2"
{{messagePrefix + '.no-members-yet' | translate}}
<button (click)="search({query: ''})"
class="btn btn-primary">{{messagePrefix + '.button.see-all' | translate}}</button>
</div>
<div *ngIf="(ePeople | async)?.payload.totalElements == 0 && searchDone" class="alert alert-info w-100 mb-2"
role="alert"> role="alert">
{{messagePrefix + '.no-items' | translate}} {{messagePrefix + '.no-items' | translate}}
</div> </div>
<h4>{{messagePrefix + '.headMembers' | translate}}</h4>
<ds-pagination *ngIf="(ePeopleMembersOfGroup | async)?.payload.totalElements > 0"
[paginationOptions]="config"
[pageInfoState]="(ePeopleMembersOfGroup | async)?.payload"
[collectionSize]="(ePeopleMembersOfGroup | async)?.payload?.totalElements"
[hideGear]="true"
[hidePagerWhenSinglePage]="true"
(pageChange)="onPageChange($event)">
<div class="table-responsive">
<table id="ePeopleMembersOfGroup" class="table table-striped table-hover table-bordered">
<thead>
<tr>
<th scope="col">{{messagePrefix + '.table.id' | translate}}</th>
<th scope="col">{{messagePrefix + '.table.name' | translate}}</th>
<th>{{messagePrefix + '.table.edit' | translate}}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let ePerson of (ePeopleMembersOfGroup | async)?.payload?.page">
<td>{{ePerson.id}}</td>
<td><a (click)="ePersonDataService.startEditingNewEPerson(ePerson)"
[routerLink]="[ePersonDataService.getEPeoplePageRouterLink()]">{{ePerson.name}}</a></td>
<td>
<div class="btn-group edit-field">
<button (click)="deleteMemberFromGroup(ePerson)"
class="btn btn-outline-danger btn-sm"
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: {name: ePerson.name} }}">
<i class="fas fa-trash-alt fa-fw"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</ds-pagination>
<div *ngIf="(ePeopleMembersOfGroup | async)?.payload.totalElements == 0" class="alert alert-info w-100 mb-2"
role="alert">
{{messagePrefix + '.no-members-yet' | translate}}
</div>
</ng-container> </ng-container>

View File

@@ -1,8 +1,9 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { NO_ERRORS_SCHEMA } from '@angular/core'; import { NO_ERRORS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, fakeAsync, inject, TestBed, tick } from '@angular/core/testing'; import { async, ComponentFixture, fakeAsync, flush, inject, TestBed, tick } from '@angular/core/testing';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { BrowserModule, By } from '@angular/platform-browser'; import { BrowserModule, By } from '@angular/platform-browser';
import { Router } from '@angular/router';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core'; import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core';
import { Observable } from 'rxjs/internal/Observable'; import { Observable } from 'rxjs/internal/Observable';
@@ -16,6 +17,7 @@ import { Group } from '../../../../../core/eperson/models/group.model';
import { PageInfo } from '../../../../../core/shared/page-info.model'; import { PageInfo } from '../../../../../core/shared/page-info.model';
import { FormBuilderService } from '../../../../../shared/form/builder/form-builder.service'; import { FormBuilderService } from '../../../../../shared/form/builder/form-builder.service';
import { getMockFormBuilderService } from '../../../../../shared/mocks/mock-form-builder-service'; import { getMockFormBuilderService } from '../../../../../shared/mocks/mock-form-builder-service';
import { MockRouter } from '../../../../../shared/mocks/mock-router';
import { getMockTranslateService } from '../../../../../shared/mocks/mock-translate.service'; import { getMockTranslateService } from '../../../../../shared/mocks/mock-translate.service';
import { NotificationsService } from '../../../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../../../shared/notifications/notifications.service';
import { EPersonMock, EPersonMock2 } from '../../../../../shared/testing/eperson-mock'; import { EPersonMock, EPersonMock2 } from '../../../../../shared/testing/eperson-mock';
@@ -36,15 +38,21 @@ describe('MembersListComponent', () => {
let activeGroup; let activeGroup;
let allEPersons; let allEPersons;
let allGroups; let allGroups;
let epersonMembers;
let subgroupMembers;
beforeEach(async(() => { beforeEach(async(() => {
activeGroup = GroupMock; activeGroup = GroupMock;
activeGroup.epersons = [EPersonMock2]; epersonMembers = [EPersonMock2];
subgroupMembers = [GroupMock2];
allEPersons = [EPersonMock, EPersonMock2]; allEPersons = [EPersonMock, EPersonMock2];
allGroups = [GroupMock, GroupMock2] allGroups = [GroupMock, GroupMock2];
ePersonDataServiceStub = { ePersonDataServiceStub = {
activeGroup: activeGroup,
epersonMembers: epersonMembers,
subgroupMembers: subgroupMembers,
findAllByHref(href: string): Observable<RemoteData<PaginatedList<EPerson>>> { findAllByHref(href: string): Observable<RemoteData<PaginatedList<EPerson>>> {
return createSuccessfulRemoteDataObject$(new PaginatedList<EPerson>(new PageInfo(), activeGroup.epersons)) return createSuccessfulRemoteDataObject$(new PaginatedList<EPerson>(new PageInfo(), groupsDataServiceStub.getEPersonMembers()))
}, },
searchByScope(scope: string, query: string): Observable<RemoteData<PaginatedList<EPerson>>> { searchByScope(scope: string, query: string): Observable<RemoteData<PaginatedList<EPerson>>> {
if (query === '') { if (query === '') {
@@ -55,33 +63,52 @@ describe('MembersListComponent', () => {
clearEPersonRequests() { clearEPersonRequests() {
// empty // empty
}, },
clearLinkRequests() {
// empty
},
getEPeoplePageRouterLink(): string { getEPeoplePageRouterLink(): string {
return '/admin/access-control/epeople'; return '/admin/access-control/epeople';
} }
}; };
groupsDataServiceStub = { groupsDataServiceStub = {
activeGroup: activeGroup,
epersonMembers: epersonMembers,
subgroupMembers: subgroupMembers,
allGroups: allGroups,
getActiveGroup(): Observable<Group> { getActiveGroup(): Observable<Group> {
return observableOf(activeGroup); return observableOf(activeGroup);
}, },
getEPersonMembers() {
return this.epersonMembers;
},
searchGroups(query: string): Observable<RemoteData<PaginatedList<Group>>> { searchGroups(query: string): Observable<RemoteData<PaginatedList<Group>>> {
if (query === '') { if (query === '') {
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), allGroups)) return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), this.allGroups))
} }
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])) return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), []))
}, },
addMemberToGroup(parentGroup, eperson: EPerson): Observable<RestResponse> { addMemberToGroup(parentGroup, eperson: EPerson): Observable<RestResponse> {
activeGroup.epersons = [...activeGroup.epersons, eperson]; this.epersonMembers = [...this.epersonMembers, eperson];
return observableOf(new RestResponse(true, 200, 'Success')); return observableOf(new RestResponse(true, 200, 'Success'));
}, },
clearGroupsRequests() { clearGroupsRequests() {
// empty // empty
}, },
clearGroupLinkRequests() {
// empty
},
getGroupEditPageRouterLink(group: Group): string {
return '/admin/access-control/groups/' + group.id;
},
deleteMemberFromGroup(parentGroup, epersonToDelete: EPerson): Observable<RestResponse> { deleteMemberFromGroup(parentGroup, epersonToDelete: EPerson): Observable<RestResponse> {
activeGroup.epersons = activeGroup.epersons.find((eperson: EPerson) => { this.epersonMembers = this.epersonMembers.find((eperson: EPerson) => {
if (eperson.id !== epersonToDelete.id) { if (eperson.id !== epersonToDelete.id) {
return eperson; return eperson;
} }
}); });
if (this.epersonMembers === undefined) {
this.epersonMembers = []
}
return observableOf(new RestResponse(true, 200, 'Success')); return observableOf(new RestResponse(true, 200, 'Success'));
} }
}; };
@@ -102,6 +129,7 @@ describe('MembersListComponent', () => {
{ provide: GroupDataService, useValue: groupsDataServiceStub }, { provide: GroupDataService, useValue: groupsDataServiceStub },
{ provide: NotificationsService, useValue: new NotificationsServiceStub() }, { provide: NotificationsService, useValue: new NotificationsServiceStub() },
{ provide: FormBuilderService, useValue: builderService }, { provide: FormBuilderService, useValue: builderService },
{ provide: Router, useValue: new MockRouter() },
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).compileComponents(); }).compileComponents();
@@ -112,15 +140,20 @@ describe('MembersListComponent', () => {
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });
afterEach(fakeAsync(() => {
fixture.destroy();
flush();
component = null;
}));
it('should create MembersListComponent', inject([MembersListComponent], (comp: MembersListComponent) => { it('should create MembersListComponent', inject([MembersListComponent], (comp: MembersListComponent) => {
expect(comp).toBeDefined(); expect(comp).toBeDefined();
})); }));
it('should show list of eperson members of current active group', () => { it('should show list of eperson members of current active group', () => {
const epersonIdsFound = fixture.debugElement.queryAll(By.css('#epersons tr td:first-child')); const epersonIdsFound = fixture.debugElement.queryAll(By.css('#ePeopleMembersOfGroup tr td:first-child'));
expect(epersonIdsFound.length).toEqual(1); expect(epersonIdsFound.length).toEqual(1);
activeGroup.epersons.map((eperson: EPerson) => { epersonMembers.map((eperson: EPerson) => {
expect(epersonIdsFound.find((foundEl) => { expect(epersonIdsFound.find((foundEl) => {
return (foundEl.nativeElement.textContent.trim() === eperson.uuid); return (foundEl.nativeElement.textContent.trim() === eperson.uuid);
})).toBeTruthy(); })).toBeTruthy();
@@ -134,14 +167,14 @@ describe('MembersListComponent', () => {
component.search({ scope: 'metadata', query: '' }); component.search({ scope: 'metadata', query: '' });
tick(); tick();
fixture.detectChanges(); fixture.detectChanges();
epersonsFound = fixture.debugElement.queryAll(By.css('#epersons tbody tr')); epersonsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr'));
})); }));
it('should display all epersons', () => { it('should display all epersons', () => {
expect(epersonsFound.length).toEqual(2); expect(epersonsFound.length).toEqual(2);
}); });
describe('if eperson is already a subeperson', () => { describe('if eperson is already a eperson', () => {
it('should have delete button, else it should have add button', () => { it('should have delete button, else it should have add button', () => {
activeGroup.epersons.map((eperson: EPerson) => { activeGroup.epersons.map((eperson: EPerson) => {
epersonsFound.map((foundEPersonRowElement) => { epersonsFound.map((foundEPersonRowElement) => {
@@ -164,27 +197,42 @@ describe('MembersListComponent', () => {
describe('if first add button is pressed', () => { describe('if first add button is pressed', () => {
beforeEach(fakeAsync(() => { beforeEach(fakeAsync(() => {
const addButton = fixture.debugElement.query(By.css('#epersons tbody .fa-plus')); const addButton = fixture.debugElement.query(By.css('#epersonsSearch tbody .fa-plus'));
addButton.nativeElement.click(); addButton.nativeElement.click();
tick(); tick();
fixture.detectChanges(); fixture.detectChanges();
})); }));
it('one more subeperson in list (from 1 to 2 total epersons)', () => { it('all groups in search member of selected group', () => {
epersonsFound = fixture.debugElement.queryAll(By.css('#epersons tbody tr')); epersonsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr'));
expect(epersonsFound.length).toEqual(2); expect(epersonsFound.length).toEqual(2);
epersonsFound.map((foundEPersonRowElement) => {
if (foundEPersonRowElement.debugElement !== undefined) {
const addButton = foundEPersonRowElement.debugElement.query(By.css('td:last-child .fa-plus'));
const deleteButton = foundEPersonRowElement.debugElement.query(By.css('td:last-child .fa-trash-alt'));
expect(addButton).toBeUndefined();
expect(deleteButton).toBeDefined();
}
})
}); });
}); });
describe('if first delete button is pressed', () => { describe('if first delete button is pressed', () => {
beforeEach(fakeAsync(() => { beforeEach(fakeAsync(() => {
const addButton = fixture.debugElement.query(By.css('#epersons tbody .fa-trash-alt')); const addButton = fixture.debugElement.query(By.css('#epersonsSearch tbody .fa-trash-alt'));
addButton.nativeElement.click(); addButton.nativeElement.click();
tick(); tick();
fixture.detectChanges(); fixture.detectChanges();
})); }));
it('one less subeperson in list from 1 to 0 (of 2 total epersons)', () => { it('first eperson in search delete button, because now member', () => {
epersonsFound = fixture.debugElement.queryAll(By.css('#epersons tbody tr')); epersonsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr'));
expect(epersonsFound.length).toEqual(0); epersonsFound.map((foundEPersonRowElement) => {
if (foundEPersonRowElement.debugElement !== undefined) {
const addButton = foundEPersonRowElement.debugElement.query(By.css('td:last-child .fa-plus'));
const deleteButton = foundEPersonRowElement.debugElement.query(By.css('td:last-child .fa-trash-alt'));
expect(deleteButton).toBeUndefined();
expect(addButton).toBeDefined();
}
})
}); });
}); });
}); });

View File

@@ -1,5 +1,6 @@
import { Component, Input, OnDestroy, OnInit } from '@angular/core'; import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { FormBuilder } from '@angular/forms'; import { FormBuilder } from '@angular/forms';
import { Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { Observable, of as observableOf, Subscription } from 'rxjs'; import { Observable, of as observableOf, Subscription } from 'rxjs';
import { map, mergeMap, take } from 'rxjs/operators'; import { map, mergeMap, take } from 'rxjs/operators';
@@ -28,12 +29,24 @@ export class MembersListComponent implements OnInit, OnDestroy {
messagePrefix: string; messagePrefix: string;
/** /**
* EPeople being displayed, initially all members, after search result of search * EPeople being displayed in search result, initially all members, after search result of search
*/ */
ePeople: Observable<RemoteData<PaginatedList<EPerson>>>; ePeopleSearch: Observable<RemoteData<PaginatedList<EPerson>>>;
/**
* List of EPeople members of currently active group being edited
*/
ePeopleMembersOfGroup: Observable<RemoteData<PaginatedList<EPerson>>>;
/** /**
* Pagination config used to display the list of EPeople * Pagination config used to display the list of EPeople that are result of EPeople search
*/
configSearch: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
id: 'search-members-list-pagination',
pageSize: 5,
currentPage: 1
});
/**
* Pagination config used to display the list of EPerson Membes of active group being edited
*/ */
config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
id: 'members-list-pagination', id: 'members-list-pagination',
@@ -49,50 +62,57 @@ export class MembersListComponent implements OnInit, OnDestroy {
// The search form // The search form
searchForm; searchForm;
/** // Current search in edit group - epeople search form
* Whether or not user has done a search yet currentSearchQuery: string;
*/ currentSearchScope: string;
// Whether or not user has done a EPeople search yet
searchDone: boolean; searchDone: boolean;
// current active group being edited
groupBeingEdited: Group;
constructor(private groupDataService: GroupDataService, constructor(private groupDataService: GroupDataService,
public ePersonDataService: EPersonDataService, public ePersonDataService: EPersonDataService,
private translateService: TranslateService, private translateService: TranslateService,
private notificationsService: NotificationsService, private notificationsService: NotificationsService,
private formBuilder: FormBuilder) { private formBuilder: FormBuilder,
private router: Router) {
this.currentSearchQuery = '';
this.currentSearchScope = 'metadata';
} }
ngOnInit() { ngOnInit() {
this.subs.push(this.groupDataService.getActiveGroup().subscribe((group: Group) => {
if (group != null) {
this.ePeople = this.ePersonDataService.findAllByHref(group._links.epersons.href, {
currentPage: 1,
elementsPerPage: this.config.pageSize
})
}
}));
this.searchForm = this.formBuilder.group(({ this.searchForm = this.formBuilder.group(({
scope: 'metadata', scope: 'metadata',
query: '', query: '',
})); }));
this.searchDone = false; this.subs.push(this.groupDataService.getActiveGroup().subscribe((activeGroup: Group) => {
if (activeGroup != null) {
this.groupBeingEdited = activeGroup;
this.forceUpdateEPeople(activeGroup);
}
}));
} }
/** /**
* Event triggered when the user changes page * Event triggered when the user changes page on search result
* @param event
*/
onPageChangeSearch(event) {
this.configSearch.currentPage = event;
this.search({ scope: this.currentSearchScope, query: this.currentSearchQuery });
}
/**
* Event triggered when the user changes page on EPerson embers of active group
* @param event * @param event
*/ */
onPageChange(event) { onPageChange(event) {
this.updateMembers({ this.ePeopleMembersOfGroup = this.ePersonDataService.findAllByHref(this.groupBeingEdited._links.epersons.href, {
currentPage: event, currentPage: event,
elementsPerPage: this.config.pageSize elementsPerPage: this.config.pageSize
}); })
}
/**
* Update the list of members by fetching it from the rest api or cache
*/
private updateMembers(options) {
this.ePeople = this.ePersonDataService.getEPeople(options);
} }
/** /**
@@ -120,7 +140,7 @@ export class MembersListComponent implements OnInit, OnDestroy {
if (activeGroup != null) { if (activeGroup != null) {
const response = this.groupDataService.addMemberToGroup(activeGroup, ePerson); const response = this.groupDataService.addMemberToGroup(activeGroup, ePerson);
this.showNotifications('addMember', response, ePerson.name, activeGroup); this.showNotifications('addMember', response, ePerson.name, activeGroup);
this.forceUpdateEPeople(activeGroup); this.forceUpdateEPeople(activeGroup, ePerson);
} else { } else {
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup')); this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup'));
} }
@@ -155,10 +175,22 @@ export class MembersListComponent implements OnInit, OnDestroy {
* @param data Contains scope and query param * @param data Contains scope and query param
*/ */
search(data: any) { search(data: any) {
const query: string = data.query;
const scope: string = data.scope;
if (query != null && this.currentSearchQuery !== query && this.groupBeingEdited) {
this.router.navigateByUrl(this.groupDataService.getGroupEditPageRouterLink(this.groupBeingEdited));
this.currentSearchQuery = query;
this.configSearch.currentPage = 1;
}
if (scope != null && this.currentSearchScope !== scope && this.groupBeingEdited) {
this.router.navigateByUrl(this.groupDataService.getGroupEditPageRouterLink(this.groupBeingEdited));
this.currentSearchScope = scope;
this.configSearch.currentPage = 1;
}
this.searchDone = true; this.searchDone = true;
this.ePeople = this.ePersonDataService.searchByScope(data.scope, data.query, { this.ePeopleSearch = this.ePersonDataService.searchByScope(this.currentSearchScope, this.currentSearchQuery, {
currentPage: 1, currentPage: this.configSearch.currentPage,
elementsPerPage: this.config.pageSize elementsPerPage: this.configSearch.pageSize
}); });
} }
@@ -167,12 +199,15 @@ export class MembersListComponent implements OnInit, OnDestroy {
* a new REST call * a new REST call
* @param activeGroup Group currently being edited * @param activeGroup Group currently being edited
*/ */
public forceUpdateEPeople(activeGroup: Group) { public forceUpdateEPeople(activeGroup: Group, ePersonToUpdate?: EPerson) {
this.groupDataService.clearGroupsRequests(); if (ePersonToUpdate != null) {
this.ePersonDataService.clearEPersonRequests(); this.ePersonDataService.clearLinkRequests(ePersonToUpdate._links.groups.href);
this.ePeople = this.ePersonDataService.findAllByHref(activeGroup._links.epersons.href, { }
currentPage: 1, this.ePersonDataService.clearLinkRequests(activeGroup._links.epersons.href);
elementsPerPage: this.config.pageSize this.router.navigateByUrl(this.groupDataService.getGroupEditPageRouterLink(activeGroup));
this.ePeopleMembersOfGroup = this.ePersonDataService.findAllByHref(activeGroup._links.epersons.href, {
currentPage: this.configSearch.currentPage,
elementsPerPage: this.configSearch.pageSize
}) })
} }
@@ -199,4 +234,14 @@ export class MembersListComponent implements OnInit, OnDestroy {
} }
}) })
} }
/**
* Reset all input-fields to be empty and search all search
*/
clearFormAndResetResult() {
this.searchForm.patchValue({
query: '',
});
this.search({ query: '' });
}
} }

View File

@@ -1,7 +1,10 @@
<ng-container> <ng-container>
<h3 class="border-bottom pb-2">{{messagePrefix + '.head' | translate}}</h3> <h3 class="border-bottom pb-2">{{messagePrefix + '.head' | translate}}</h3>
<h4 id="search" class="border-bottom pb-2">{{messagePrefix + '.search.head' | translate}}</h4> <h4 id="search" class="border-bottom pb-2">{{messagePrefix + '.search.head' | translate}}
<button (click)="clearFormAndResetResult();"
class="btn btn-primary float-right">{{messagePrefix + '.button.see-all' | translate}}</button>
</h4>
<form [formGroup]="searchForm" (ngSubmit)="search(searchForm.value)" class="row"> <form [formGroup]="searchForm" (ngSubmit)="search(searchForm.value)" class="row">
<div class="col-12"> <div class="col-12">
<div class="form-group input-group"> <div class="form-group input-group">
@@ -15,16 +18,16 @@
</div> </div>
</form> </form>
<ds-pagination *ngIf="(groups | async)?.payload.totalElements > 0" <ds-pagination *ngIf="(groupsSearch | async)?.payload.totalElements > 0"
[paginationOptions]="config" [paginationOptions]="configSearch"
[pageInfoState]="(groups | async)?.payload" [pageInfoState]="(groupsSearch | async)?.payload"
[collectionSize]="(groups | async)?.payload?.totalElements" [collectionSize]="(groupsSearch | async)?.payload?.totalElements"
[hideGear]="true" [hideGear]="true"
[hidePagerWhenSinglePage]="true" [hidePagerWhenSinglePage]="true"
(pageChange)="onPageChange($event)"> (pageChange)="onPageChangeSearch($event)">
<div class="table-responsive"> <div class="table-responsive">
<table id="groups" class="table table-striped table-hover table-bordered"> <table id="groupsSearch" class="table table-striped table-hover table-bordered">
<thead> <thead>
<tr> <tr>
<th scope="col">{{messagePrefix + '.table.id' | translate}}</th> <th scope="col">{{messagePrefix + '.table.id' | translate}}</th>
@@ -33,7 +36,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr *ngFor="let group of (groups | async)?.payload?.page"> <tr *ngFor="let group of (groupsSearch | async)?.payload?.page">
<td>{{group.id}}</td> <td>{{group.id}}</td>
<td><a (click)="groupDataService.startEditingNewGroup(group)" <td><a (click)="groupDataService.startEditingNewGroup(group)"
[routerLink]="[groupDataService.getGroupEditPageRouterLink(group)]">{{group.name}}</a></td> [routerLink]="[groupDataService.getGroupEditPageRouterLink(group)]">{{group.name}}</a></td>
@@ -41,14 +44,14 @@
<div class="btn-group edit-field"> <div class="btn-group edit-field">
<button *ngIf="(isSubgroupOfGroup(group) | async)" <button *ngIf="(isSubgroupOfGroup(group) | async)"
(click)="deleteSubgroupFromGroup(group)" (click)="deleteSubgroupFromGroup(group)"
class="btn btn-outline-danger btn-sm" class="btn btn-outline-danger btn-sm deleteButton"
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: {name: group.name} }}"> title="{{messagePrefix + '.table.edit.buttons.remove' | translate: {name: group.name} }}">
<i class="fas fa-trash-alt fa-fw"></i> <i class="fas fa-trash-alt fa-fw"></i>
</button> </button>
<button *ngIf="!(isSubgroupOfGroup(group) | async)" <button *ngIf="!(isSubgroupOfGroup(group) | async)"
(click)="addSubgroupToGroup(group)" (click)="addSubgroupToGroup(group)"
class="btn btn-outline-primary btn-sm" class="btn btn-outline-primary btn-sm addButton"
title="{{messagePrefix + '.table.edit.buttons.add' | translate: {name: group.name} }}"> title="{{messagePrefix + '.table.edit.buttons.add' | translate: {name: group.name} }}">
<i class="fas fa-plus fa-fw"></i> <i class="fas fa-plus fa-fw"></i>
</button> </button>
@@ -58,19 +61,55 @@
</tbody> </tbody>
</table> </table>
</div> </div>
</ds-pagination> </ds-pagination>
<div *ngIf="(groups | async)?.payload.totalElements == 0 && !searchDone" class="alert alert-info w-100 mb-2" <div *ngIf="(groupsSearch | async)?.payload.totalElements == 0 && searchDone" class="alert alert-info w-100 mb-2"
role="alert">
{{messagePrefix + '.no-subgroups-yet' | translate}}
<button (click)="search({query: ''})"
class="btn btn-primary">{{messagePrefix + '.button.see-all' | translate}}</button>
</div>
<div *ngIf="(groups | async)?.payload.totalElements == 0 && searchDone" class="alert alert-info w-100 mb-2"
role="alert"> role="alert">
{{messagePrefix + '.no-items' | translate}} {{messagePrefix + '.no-items' | translate}}
</div> </div>
<h4>{{messagePrefix + '.headSubgroups' | translate}}</h4>
<ds-pagination *ngIf="(subgroupsOfGroup | async)?.payload.totalElements > 0"
[paginationOptions]="config"
[pageInfoState]="(subgroupsOfGroup | async)?.payload"
[collectionSize]="(subgroupsOfGroup | async)?.payload?.totalElements"
[hideGear]="true"
[hidePagerWhenSinglePage]="true"
(pageChange)="onPageChange($event)">
<div class="table-responsive">
<table id="subgroupsOfGroup" class="table table-striped table-hover table-bordered">
<thead>
<tr>
<th scope="col">{{messagePrefix + '.table.id' | translate}}</th>
<th scope="col">{{messagePrefix + '.table.name' | translate}}</th>
<th>{{messagePrefix + '.table.edit' | translate}}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let group of (subgroupsOfGroup | async)?.payload?.page">
<td>{{group.id}}</td>
<td><a (click)="groupDataService.startEditingNewGroup(group)"
[routerLink]="[groupDataService.getGroupEditPageRouterLink(group)]">{{group.name}}</a></td>
<td>
<div class="btn-group edit-field">
<button (click)="deleteSubgroupFromGroup(group)"
class="btn btn-outline-danger btn-sm deleteButton"
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: {name: group.name} }}">
<i class="fas fa-trash-alt fa-fw"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</ds-pagination>
<div *ngIf="(subgroupsOfGroup | async)?.payload.totalElements == 0" class="alert alert-info w-100 mb-2"
role="alert">
{{messagePrefix + '.no-subgroups-yet' | translate}}
</div>
</ng-container> </ng-container>

View File

@@ -1,8 +1,9 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { NO_ERRORS_SCHEMA } from '@angular/core'; import { NO_ERRORS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, fakeAsync, inject, TestBed, tick } from '@angular/core/testing'; import { async, ComponentFixture, fakeAsync, flush, inject, TestBed, tick } from '@angular/core/testing';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { BrowserModule, By } from '@angular/platform-browser'; import { BrowserModule, By } from '@angular/platform-browser';
import { Router } from '@angular/router';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core'; import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core';
import { Observable } from 'rxjs/internal/Observable'; import { Observable } from 'rxjs/internal/Observable';
@@ -14,6 +15,7 @@ import { Group } from '../../../../../core/eperson/models/group.model';
import { PageInfo } from '../../../../../core/shared/page-info.model'; import { PageInfo } from '../../../../../core/shared/page-info.model';
import { FormBuilderService } from '../../../../../shared/form/builder/form-builder.service'; import { FormBuilderService } from '../../../../../shared/form/builder/form-builder.service';
import { getMockFormBuilderService } from '../../../../../shared/mocks/mock-form-builder-service'; import { getMockFormBuilderService } from '../../../../../shared/mocks/mock-form-builder-service';
import { MockRouter } from '../../../../../shared/mocks/mock-router';
import { getMockTranslateService } from '../../../../../shared/mocks/mock-translate.service'; import { getMockTranslateService } from '../../../../../shared/mocks/mock-translate.service';
import { NotificationsService } from '../../../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../../../shared/notifications/notifications.service';
import { GroupMock, GroupMock2 } from '../../../../../shared/testing/group-mock'; import { GroupMock, GroupMock2 } from '../../../../../shared/testing/group-mock';
@@ -31,19 +33,26 @@ describe('SubgroupsListComponent', () => {
let ePersonDataServiceStub: any; let ePersonDataServiceStub: any;
let groupsDataServiceStub: any; let groupsDataServiceStub: any;
let activeGroup; let activeGroup;
let subgroups;
let allGroups; let allGroups;
let routerStub;
beforeEach(async(() => { beforeEach(async(() => {
activeGroup = GroupMock; activeGroup = GroupMock;
allGroups = [GroupMock, GroupMock2] subgroups = [GroupMock2];
allGroups = [GroupMock, GroupMock2];
ePersonDataServiceStub = {}; ePersonDataServiceStub = {};
groupsDataServiceStub = { groupsDataServiceStub = {
activeGroup: activeGroup, activeGroup: activeGroup,
subgroups: subgroups,
getActiveGroup(): Observable<Group> { getActiveGroup(): Observable<Group> {
return observableOf(this.activeGroup); return observableOf(this.activeGroup);
}, },
getSubgroups(): Group {
return this.activeGroup;
},
findAllByHref(href: string): Observable<RemoteData<PaginatedList<Group>>> { findAllByHref(href: string): Observable<RemoteData<PaginatedList<Group>>> {
return createSuccessfulRemoteDataObject$(new PaginatedList<Group>(new PageInfo(), this.activeGroup.subgroups)) return createSuccessfulRemoteDataObject$(new PaginatedList<Group>(new PageInfo(), this.subgroups))
}, },
getGroupEditPageRouterLink(group: Group): string { getGroupEditPageRouterLink(group: Group): string {
return '/admin/access-control/groups/' + group.id; return '/admin/access-control/groups/' + group.id;
@@ -55,14 +64,17 @@ describe('SubgroupsListComponent', () => {
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])) return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), []))
}, },
addSubGroupToGroup(parentGroup, subgroup: Group): Observable<RestResponse> { addSubGroupToGroup(parentGroup, subgroup: Group): Observable<RestResponse> {
this.activeGroup.subgroups = [...this.activeGroup.subgroups, subgroup]; this.subgroups = [...this.subgroups, subgroup];
return observableOf(new RestResponse(true, 200, 'Success')); return observableOf(new RestResponse(true, 200, 'Success'));
}, },
clearGroupsRequests() { clearGroupsRequests() {
// empty // empty
}, },
deleteSubGroupFromGroup(parentGroup, subgroup: Group): Observable<RestResponse> { clearGroupLinkRequests() {
this.activeGroup.subgroups = this.activeGroup.subgroups.find((group: Group) => { // empty
},
deleteSubGroupFromGroup(parentGroup, subgroup: Group): Observable<RestResponse> {
this.subgroups = this.subgroups.find((group: Group) => {
if (group.id !== subgroup.id) { if (group.id !== subgroup.id) {
return group; return group;
} }
@@ -70,6 +82,7 @@ describe('SubgroupsListComponent', () => {
return observableOf(new RestResponse(true, 200, 'Success')); return observableOf(new RestResponse(true, 200, 'Success'));
} }
}; };
routerStub = new MockRouter();
builderService = getMockFormBuilderService(); builderService = getMockFormBuilderService();
translateService = getMockTranslateService(); translateService = getMockTranslateService();
TestBed.configureTestingModule({ TestBed.configureTestingModule({
@@ -86,6 +99,7 @@ describe('SubgroupsListComponent', () => {
{ provide: GroupDataService, useValue: groupsDataServiceStub }, { provide: GroupDataService, useValue: groupsDataServiceStub },
{ provide: NotificationsService, useValue: new NotificationsServiceStub() }, { provide: NotificationsService, useValue: new NotificationsServiceStub() },
{ provide: FormBuilderService, useValue: builderService }, { provide: FormBuilderService, useValue: builderService },
{ provide: Router, useValue: routerStub },
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).compileComponents(); }).compileComponents();
@@ -96,13 +110,18 @@ describe('SubgroupsListComponent', () => {
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });
afterEach(fakeAsync(() => {
fixture.destroy();
flush();
component = null;
}));
it('should create SubgroupsListComponent', inject([SubgroupsListComponent], (comp: SubgroupsListComponent) => { it('should create SubgroupsListComponent', inject([SubgroupsListComponent], (comp: SubgroupsListComponent) => {
expect(comp).toBeDefined(); expect(comp).toBeDefined();
})); }));
it('should show list of subgroups of current active group', () => { it('should show list of subgroups of current active group', () => {
const groupIdsFound = fixture.debugElement.queryAll(By.css('#groups tr td:first-child')); const groupIdsFound = fixture.debugElement.queryAll(By.css('#subgroupsOfGroup tr td:first-child'));
expect(groupIdsFound.length).toEqual(1); expect(groupIdsFound.length).toEqual(1);
activeGroup.subgroups.map((group: Group) => { activeGroup.subgroups.map((group: Group) => {
expect(groupIdsFound.find((foundEl) => { expect(groupIdsFound.find((foundEl) => {
@@ -111,64 +130,76 @@ describe('SubgroupsListComponent', () => {
}) })
}); });
describe('if first group delete button is pressed', () => {
let groupsFound;
beforeEach(fakeAsync(() => {
const addButton = fixture.debugElement.query(By.css('#subgroupsOfGroup tbody .deleteButton'));
addButton.triggerEventHandler('click', {
preventDefault: () => {/**/
}
});
tick();
fixture.detectChanges();
}));
it('one less subgroup in list from 1 to 0 (of 2 total groups)', () => {
groupsFound = fixture.debugElement.queryAll(By.css('#subgroupsOfGroup tbody tr'));
expect(groupsFound.length).toEqual(0);
});
});
describe('search', () => { describe('search', () => {
describe('when searching without query', () => { describe('when searching with empty query', () => {
let groupsFound; let groupsFound;
beforeEach(fakeAsync(() => { beforeEach(fakeAsync(() => {
component.search({ query: '' }); component.search({ query: '' });
tick(); groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr'));
fixture.detectChanges();
groupsFound = fixture.debugElement.queryAll(By.css('#groups tbody tr'));
})); }));
it('should display all groups', () => { it('should display all groups', () => {
fixture.detectChanges();
groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr'));
expect(groupsFound.length).toEqual(2); expect(groupsFound.length).toEqual(2);
groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr'));
const groupIdsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr td:first-child'));
allGroups.map((group: Group) => {
expect(groupIdsFound.find((foundEl) => {
return (foundEl.nativeElement.textContent.trim() === group.uuid);
})).toBeTruthy();
})
}); });
describe('if group is already a subgroup', () => { describe('if group is already a subgroup', () => {
it('should have delete button, else it should have add button', () => { it('should have delete button, else it should have add button', () => {
activeGroup.subgroups.map((group: Group) => { fixture.detectChanges();
groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr'));
const getSubgroups = groupsDataServiceStub.getSubgroups().subgroups;
if (getSubgroups !== undefined && getSubgroups.length > 0) {
groupsFound.map((foundGroupRowElement) => { groupsFound.map((foundGroupRowElement) => {
if (foundGroupRowElement.debugElement !== undefined) { if (foundGroupRowElement.debugElement !== undefined) {
const groupId = foundGroupRowElement.debugElement.query(By.css('td:first-child'));
const addButton = foundGroupRowElement.debugElement.query(By.css('td:last-child .fa-plus')); const addButton = foundGroupRowElement.debugElement.query(By.css('td:last-child .fa-plus'));
const deleteButton = foundGroupRowElement.debugElement.query(By.css('td:last-child .fa-trash-alt')); const deleteButton = foundGroupRowElement.debugElement.query(By.css('td:last-child .fa-trash-alt'));
if (groupId.nativeElement.textContent === group.id) { expect(addButton).toBeUndefined();
expect(addButton).toBeUndefined(); expect(deleteButton).toBeDefined();
expect(deleteButton).toBeDefined();
} else {
expect(deleteButton).toBeUndefined();
expect(addButton).toBeDefined();
}
} }
}) })
}) } else {
}); getSubgroups.map((group: Group) => {
}); groupsFound.map((foundGroupRowElement) => {
if (foundGroupRowElement.debugElement !== undefined) {
describe('if first add button is pressed', () => { const groupId = foundGroupRowElement.debugElement.query(By.css('td:first-child'));
beforeEach(fakeAsync(() => { const addButton = foundGroupRowElement.debugElement.query(By.css('td:last-child .fa-plus'));
const addButton = fixture.debugElement.query(By.css('#groups tbody .fa-plus')); const deleteButton = foundGroupRowElement.debugElement.query(By.css('td:last-child .fa-trash-alt'));
addButton.nativeElement.click(); if (groupId.nativeElement.textContent === group.id) {
tick(); expect(addButton).toBeUndefined();
fixture.detectChanges(); expect(deleteButton).toBeDefined();
})); } else {
it('one more subgroup in list (from 1 to 2 total groups)', () => { expect(deleteButton).toBeUndefined();
groupsFound = fixture.debugElement.queryAll(By.css('#groups tbody tr')); expect(addButton).toBeDefined();
expect(groupsFound.length).toEqual(2); }
}); }
}); })
})
describe('if first delete button is pressed', () => { }
beforeEach(fakeAsync(() => {
const addButton = fixture.debugElement.query(By.css('#groups tbody .fa-trash-alt'));
addButton.nativeElement.click();
tick();
fixture.detectChanges();
}));
it('one less subgroup in list from 1 to 0 (of 2 total groups)', () => {
groupsFound = fixture.debugElement.queryAll(By.css('#groups tbody tr'));
expect(groupsFound.length).toEqual(0);
}); });
}); });
}); });

View File

@@ -1,5 +1,6 @@
import { Component, Input, OnDestroy, OnInit } from '@angular/core'; import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { FormBuilder } from '@angular/forms'; import { FormBuilder } from '@angular/forms';
import { Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { Observable, of as observableOf, Subscription } from 'rxjs'; import { Observable, of as observableOf, Subscription } from 'rxjs';
import { map, mergeMap, take } from 'rxjs/operators'; import { map, mergeMap, take } from 'rxjs/operators';
@@ -12,7 +13,6 @@ import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../../cor
import { hasValue } from '../../../../../shared/empty.util'; import { hasValue } from '../../../../../shared/empty.util';
import { NotificationsService } from '../../../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../../../shared/notifications/notifications.service';
import { PaginationComponentOptions } from '../../../../../shared/pagination/pagination-component-options.model'; import { PaginationComponentOptions } from '../../../../../shared/pagination/pagination-component-options.model';
import { followLink } from '../../../../../shared/utils/follow-link-config.model';
@Component({ @Component({
selector: 'ds-subgroups-list', selector: 'ds-subgroups-list',
@@ -27,9 +27,13 @@ export class SubgroupsListComponent implements OnInit, OnDestroy {
messagePrefix: string; messagePrefix: string;
/** /**
* Groups being displayed, initially all subgroups, after search result of search * Result of search groups, initially all groups
*/ */
groups: Observable<RemoteData<PaginatedList<Group>>>; groupsSearch: Observable<RemoteData<PaginatedList<Group>>>;
/**
* List of all subgroups of group being edited
*/
subgroupsOfGroup: Observable<RemoteData<PaginatedList<Group>>>;
/** /**
* List of subscriptions * List of subscriptions
@@ -37,7 +41,15 @@ export class SubgroupsListComponent implements OnInit, OnDestroy {
subs: Subscription[] = []; subs: Subscription[] = [];
/** /**
* Pagination config used to display the list of subgroups * Pagination config used to display the list of groups that are result of groups search
*/
configSearch: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
id: 'search-subgroups-list-pagination',
pageSize: 5,
currentPage: 1
});
/**
* Pagination config used to display the list of subgroups of currently active group being edited
*/ */
config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
id: 'subgroups-list-pagination', id: 'subgroups-list-pagination',
@@ -48,50 +60,55 @@ export class SubgroupsListComponent implements OnInit, OnDestroy {
// The search form // The search form
searchForm; searchForm;
/** // Current search in edit group - groups search form
* Whether or not user has done a search yet currentSearchQuery: string;
*/
// Whether or not user has done a Groups search yet
searchDone: boolean; searchDone: boolean;
// current active group being edited
groupBeingEdited: Group;
constructor(public groupDataService: GroupDataService, constructor(public groupDataService: GroupDataService,
private translateService: TranslateService, private translateService: TranslateService,
private notificationsService: NotificationsService, private notificationsService: NotificationsService,
private formBuilder: FormBuilder) { private formBuilder: FormBuilder,
private router: Router) {
this.currentSearchQuery = '';
} }
ngOnInit() { ngOnInit() {
this.subs.push(this.groupDataService.getActiveGroup().subscribe((group: Group) => {
if (group != null) {
this.groups = this.groupDataService.findAllByHref(group._links.subgroups.href, {
currentPage: 1,
elementsPerPage: this.config.pageSize
})
}
}));
this.searchForm = this.formBuilder.group(({ this.searchForm = this.formBuilder.group(({
query: '', query: '',
})); }));
this.searchDone = false; this.subs.push(this.groupDataService.getActiveGroup().subscribe((activeGroup: Group) => {
if (activeGroup != null) {
this.groupBeingEdited = activeGroup;
this.forceUpdateGroups(activeGroup);
}
}));
} }
/** /**
* Event triggered when the user changes page * Event triggered when the user changes page on search result
* @param event
*/
onPageChangeSearch(event) {
this.configSearch.currentPage = event;
this.search({ query: this.currentSearchQuery });
}
/**
* Event triggered when the user changes page on subgroups of active group
* @param event * @param event
*/ */
onPageChange(event) { onPageChange(event) {
this.updateSubgroups({ this.subgroupsOfGroup = this.groupDataService.findAllByHref(this.groupBeingEdited._links.subgroups.href, {
currentPage: event, currentPage: event,
elementsPerPage: this.config.pageSize elementsPerPage: this.config.pageSize
}); });
} }
/**
* Update the list of subgroups by fetching it from the rest api or cache
*/
private updateSubgroups(options) {
this.groups = this.groupDataService.getGroups(options);
}
/** /**
* Whether or not the given group is a subgroup of the group currently being edited * Whether or not the given group is a subgroup of the group currently being edited
* @param possibleSubgroup Group that is a possible subgroup (being tested) of the group currently being edited * @param possibleSubgroup Group that is a possible subgroup (being tested) of the group currently being edited
@@ -152,22 +169,28 @@ export class SubgroupsListComponent implements OnInit, OnDestroy {
* @param data Contains query param * @param data Contains query param
*/ */
search(data: any) { search(data: any) {
this.searchDone = true;
const query: string = data.query; const query: string = data.query;
this.groups = this.groupDataService.searchGroups(query.trim(), { if (query != null && this.currentSearchQuery !== query) {
currentPage: 1, this.router.navigateByUrl(this.groupDataService.getGroupEditPageRouterLink(this.groupBeingEdited));
elementsPerPage: this.config.pageSize this.currentSearchQuery = query;
this.configSearch.currentPage = 1;
}
this.searchDone = true;
this.groupsSearch = this.groupDataService.searchGroups(this.currentSearchQuery, {
currentPage: this.configSearch.currentPage,
elementsPerPage: this.configSearch.pageSize
}); });
} }
/** /**
* Force-update the list of groups by first clearing the cache related to groups, then performing a new REST call * Force-update the list of groups by first clearing the cache of results of this active groups' subgroups, then performing a new REST call
* @param activeGroup Group currently being edited * @param activeGroup Group currently being edited
*/ */
public forceUpdateGroups(activeGroup: Group) { public forceUpdateGroups(activeGroup: Group) {
this.groupDataService.clearGroupsRequests(); this.groupDataService.clearGroupLinkRequests(activeGroup._links.subgroups.href);
this.groups = this.groupDataService.findAllByHref(activeGroup._links.subgroups.href, { this.router.navigateByUrl(this.groupDataService.getGroupEditPageRouterLink(activeGroup));
currentPage: 1, this.subgroupsOfGroup = this.groupDataService.findAllByHref(activeGroup._links.subgroups.href, {
currentPage: this.config.currentPage,
elementsPerPage: this.config.pageSize elementsPerPage: this.config.pageSize
}); });
} }
@@ -195,4 +218,14 @@ export class SubgroupsListComponent implements OnInit, OnDestroy {
} }
}) })
} }
/**
* Reset all input-fields to be empty and search all search
*/
clearFormAndResetResult() {
this.searchForm.patchValue({
query: '',
});
this.search({ query: '' });
}
} }

View File

@@ -12,7 +12,10 @@
</button> </button>
</div> </div>
<h3 id="search" class="border-bottom pb-2">{{messagePrefix + 'search.head' | translate}}</h3> <h3 id="search" class="border-bottom pb-2">{{messagePrefix + 'search.head' | translate}}
<button (click)="clearFormAndResetResult();"
class="btn btn-primary float-right">{{messagePrefix + 'button.see-all' | translate}}</button>
</h3>
<form [formGroup]="searchForm" (ngSubmit)="search(searchForm.value)" class="row"> <form [formGroup]="searchForm" (ngSubmit)="search(searchForm.value)" class="row">
<div class="col-12"> <div class="col-12">
<div class="form-group input-group"> <div class="form-group input-group">
@@ -42,7 +45,7 @@
<th scope="col">{{messagePrefix + 'table.id' | translate}}</th> <th scope="col">{{messagePrefix + 'table.id' | translate}}</th>
<th scope="col">{{messagePrefix + 'table.name' | translate}}</th> <th scope="col">{{messagePrefix + 'table.name' | translate}}</th>
<th scope="col">{{messagePrefix + 'table.members' | translate}}</th> <th scope="col">{{messagePrefix + 'table.members' | translate}}</th>
<!-- <th scope="col">{{messagePrefix + 'table.comcol' | translate}}</th>--> <!-- <th scope="col">{{messagePrefix + 'table.comcol' | translate}}</th>-->
<th>{{messagePrefix + 'table.edit' | translate}}</th> <th>{{messagePrefix + 'table.edit' | translate}}</th>
</tr> </tr>
</thead> </thead>
@@ -51,16 +54,17 @@
<td>{{group.id}}</td> <td>{{group.id}}</td>
<td>{{group.name}}</td> <td>{{group.name}}</td>
<td>{{(getMembers(group) | async)?.payload?.totalElements + (getSubgroups(group) | async)?.payload?.totalElements}}</td> <td>{{(getMembers(group) | async)?.payload?.totalElements + (getSubgroups(group) | async)?.payload?.totalElements}}</td>
<!-- <td>{{getOptionalComColFromName(group.name)}}</td>--> <!-- <td>{{getOptionalComColFromName(group.name)}}</td>-->
<td> <td>
<div class="btn-group edit-field"> <div class="btn-group edit-field">
<button [routerLink]="groupService.getGroupEditPageRouterLink(group)" class="btn btn-outline-primary btn-sm" <button [routerLink]="groupService.getGroupEditPageRouterLink(group)"
title="{{messagePrefix + 'table.edit.buttons.edit' | translate}}"> class="btn btn-outline-primary btn-sm"
title="{{messagePrefix + 'table.edit.buttons.edit' | translate: {name: group.name} }}">
<i class="fas fa-edit fa-fw"></i> <i class="fas fa-edit fa-fw"></i>
</button> </button>
<button (click)="deleteGroup(group)" <button *ngIf="!group?.permanent" (click)="deleteGroup(group)"
class="btn btn-outline-danger btn-sm" class="btn btn-outline-danger btn-sm"
title="{{messagePrefix + 'table.edit.buttons.remove' | translate}}"> title="{{messagePrefix + 'table.edit.buttons.remove' | translate: {name: group.name} }}">
<i class="fas fa-trash-alt fa-fw"></i> <i class="fas fa-trash-alt fa-fw"></i>
</button> </button>
</div> </div>
@@ -69,7 +73,6 @@
</tbody> </tbody>
</table> </table>
</div> </div>
</ds-pagination> </ds-pagination>
<div *ngIf="(groups | async)?.payload?.totalElements == 0" class="alert alert-info w-100 mb-2" role="alert"> <div *ngIf="(groups | async)?.payload?.totalElements == 0" class="alert alert-info w-100 mb-2" role="alert">

View File

@@ -3,6 +3,7 @@ import { NO_ERRORS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, fakeAsync, inject, TestBed, tick } from '@angular/core/testing'; import { async, ComponentFixture, fakeAsync, inject, TestBed, tick } from '@angular/core/testing';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { BrowserModule, By } from '@angular/platform-browser'; import { BrowserModule, By } from '@angular/platform-browser';
import { Router } from '@angular/router';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { Observable } from 'rxjs/internal/Observable'; import { Observable } from 'rxjs/internal/Observable';
@@ -12,12 +13,15 @@ import { EPersonDataService } from '../../../core/eperson/eperson-data.service';
import { GroupDataService } from '../../../core/eperson/group-data.service'; import { GroupDataService } from '../../../core/eperson/group-data.service';
import { EPerson } from '../../../core/eperson/models/eperson.model'; import { EPerson } from '../../../core/eperson/models/eperson.model';
import { Group } from '../../../core/eperson/models/group.model'; import { Group } from '../../../core/eperson/models/group.model';
import { RouteService } from '../../../core/services/route.service';
import { PageInfo } from '../../../core/shared/page-info.model'; import { PageInfo } from '../../../core/shared/page-info.model';
import { MockRouter } from '../../../shared/mocks/mock-router';
import { MockTranslateLoader } from '../../../shared/mocks/mock-translate-loader'; import { MockTranslateLoader } from '../../../shared/mocks/mock-translate-loader';
import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { EPersonMock, EPersonMock2 } from '../../../shared/testing/eperson-mock'; import { EPersonMock, EPersonMock2 } from '../../../shared/testing/eperson-mock';
import { GroupMock, GroupMock2 } from '../../../shared/testing/group-mock'; import { GroupMock, GroupMock2 } from '../../../shared/testing/group-mock';
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub'; import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub';
import { routeServiceStub } from '../../../shared/testing/route-service-stub';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils';
import { GroupsRegistryComponent } from './groups-registry.component'; import { GroupsRegistryComponent } from './groups-registry.component';
@@ -47,9 +51,6 @@ describe('GroupRegistryComponent', () => {
}; };
groupsDataServiceStub = { groupsDataServiceStub = {
allGroups: mockGroups, allGroups: mockGroups,
getGroups(): Observable<RemoteData<PaginatedList<Group>>> {
return createSuccessfulRemoteDataObject$(new PaginatedList(null, this.allGroups));
},
findAllByHref(href: string): Observable<RemoteData<PaginatedList<Group>>> { findAllByHref(href: string): Observable<RemoteData<PaginatedList<Group>>> {
switch (href) { switch (href) {
case 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid2/groups': case 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid2/groups':
@@ -63,7 +64,13 @@ describe('GroupRegistryComponent', () => {
getGroupEditPageRouterLink(group: Group): string { getGroupEditPageRouterLink(group: Group): string {
return '/admin/access-control/groups/' + group.id; return '/admin/access-control/groups/' + group.id;
}, },
getGroupRegistryRouterLink(): string {
return '/admin/access-control/groups';
},
searchGroups(query: string): Observable<RemoteData<PaginatedList<Group>>> { searchGroups(query: string): Observable<RemoteData<PaginatedList<Group>>> {
if (query === '') {
return createSuccessfulRemoteDataObject$(new PaginatedList(null, this.allGroups));
}
const result = this.allGroups.find((group: Group) => { const result = this.allGroups.find((group: Group) => {
return (group.id.includes(query)) return (group.id.includes(query))
}); });
@@ -84,6 +91,8 @@ describe('GroupRegistryComponent', () => {
{ provide: EPersonDataService, useValue: ePersonDataServiceStub }, { provide: EPersonDataService, useValue: ePersonDataServiceStub },
{ provide: GroupDataService, useValue: groupsDataServiceStub }, { provide: GroupDataService, useValue: groupsDataServiceStub },
{ provide: NotificationsService, useValue: new NotificationsServiceStub() }, { provide: NotificationsService, useValue: new NotificationsServiceStub() },
{ provide: RouteService, useValue: routeServiceStub },
{ provide: Router, useValue: new MockRouter() },
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).compileComponents(); }).compileComponents();

View File

@@ -1,5 +1,6 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { FormBuilder } from '@angular/forms'; import { FormBuilder } from '@angular/forms';
import { Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { take } from 'rxjs/operators'; import { take } from 'rxjs/operators';
@@ -9,10 +10,10 @@ import { EPersonDataService } from '../../../core/eperson/eperson-data.service';
import { GroupDataService } from '../../../core/eperson/group-data.service'; import { GroupDataService } from '../../../core/eperson/group-data.service';
import { EPerson } from '../../../core/eperson/models/eperson.model'; import { EPerson } from '../../../core/eperson/models/eperson.model';
import { Group } from '../../../core/eperson/models/group.model'; import { Group } from '../../../core/eperson/models/group.model';
import { RouteService } from '../../../core/services/route.service';
import { hasValue } from '../../../shared/empty.util'; import { hasValue } from '../../../shared/empty.util';
import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
import { followLink } from '../../../shared/utils/follow-link-config.model';
@Component({ @Component({
selector: 'ds-groups-registry', selector: 'ds-groups-registry',
@@ -43,21 +44,24 @@ export class GroupsRegistryComponent implements OnInit {
// The search form // The search form
searchForm; searchForm;
// Current search in groups registry
currentSearchQuery: string;
constructor(private groupService: GroupDataService, constructor(private groupService: GroupDataService,
private ePersonDataService: EPersonDataService, private ePersonDataService: EPersonDataService,
private translateService: TranslateService, private translateService: TranslateService,
private notificationsService: NotificationsService, private notificationsService: NotificationsService,
private formBuilder: FormBuilder) { private formBuilder: FormBuilder,
protected routeService: RouteService,
private router: Router) {
this.currentSearchQuery = '';
this.searchForm = this.formBuilder.group(({ this.searchForm = this.formBuilder.group(({
query: '', query: this.currentSearchQuery,
})); }));
} }
ngOnInit() { ngOnInit() {
this.updateGroups({ this.search({ query: this.currentSearchQuery });
currentPage: 1,
elementsPerPage: this.config.pageSize
});
} }
/** /**
@@ -65,17 +69,8 @@ export class GroupsRegistryComponent implements OnInit {
* @param event * @param event
*/ */
onPageChange(event) { onPageChange(event) {
this.updateGroups({ this.config.currentPage = event;
currentPage: event, this.search({ query: this.currentSearchQuery })
elementsPerPage: this.config.pageSize
});
}
/**
* Update the list of groups by fetching it from the rest api or cache
*/
private updateGroups(options) {
this.groups = this.groupService.getGroups(options, followLink('epersons'), followLink('subgroups'));
} }
/** /**
@@ -84,8 +79,13 @@ export class GroupsRegistryComponent implements OnInit {
*/ */
search(data: any) { search(data: any) {
const query: string = data.query; const query: string = data.query;
this.groups = this.groupService.searchGroups(query.trim(), { if (query != null && this.currentSearchQuery !== query) {
currentPage: 1, this.router.navigateByUrl(this.groupService.getGroupRegistryRouterLink());
this.currentSearchQuery = query;
this.config.currentPage = 1;
}
this.groups = this.groupService.searchGroups(this.currentSearchQuery.trim(), {
currentPage: this.config.currentPage,
elementsPerPage: this.config.pageSize elementsPerPage: this.config.pageSize
}); });
} }
@@ -114,7 +114,7 @@ export class GroupsRegistryComponent implements OnInit {
*/ */
public forceUpdateGroup() { public forceUpdateGroup() {
this.groupService.clearGroupsRequests(); this.groupService.clearGroupsRequests();
this.search({ query: '' }) this.search({ query: this.currentSearchQuery })
} }
/** /**
@@ -133,6 +133,16 @@ export class GroupsRegistryComponent implements OnInit {
return this.groupService.findAllByHref(group._links.subgroups.href); return this.groupService.findAllByHref(group._links.subgroups.href);
} }
/**
* Reset all input-fields to be empty and search all search
*/
clearFormAndResetResult() {
this.searchForm.patchValue({
query: '',
});
this.search({ query: '' });
}
/** /**
* Extract optional UUID from a group name => To be resolved to community or collection with link * Extract optional UUID from a group name => To be resolved to community or collection with link
* (Or will be resolved in backend and added to group object, tbd) //TODO * (Or will be resolved in backend and added to group object, tbd) //TODO

View File

@@ -276,8 +276,6 @@ export class ObjectCacheService {
* list of operations to perform * list of operations to perform
*/ */
public addPatch(selfLink: string, patch: Operation[]) { public addPatch(selfLink: string, patch: Operation[]) {
console.log('selfLink addPatch', selfLink)
console.log('patch addPatch', patch)
this.store.dispatch(new AddPatchObjectCacheAction(selfLink, patch)); this.store.dispatch(new AddPatchObjectCacheAction(selfLink, patch));
this.store.dispatch(new AddToSSBAction(selfLink, RestRequestMethod.PATCH)); this.store.dispatch(new AddToSSBAction(selfLink, RestRequestMethod.PATCH));
} }

View File

@@ -181,7 +181,7 @@ export class EPersonDataService extends DataService<EPerson> {
} }
/** /**
* Method that clears a cached EPerson request and returns its REST url * Method that clears a cached EPerson request
*/ */
public clearEPersonRequests(): void { public clearEPersonRequests(): void {
this.getBrowseEndpoint().pipe(take(1)).subscribe((link: string) => { this.getBrowseEndpoint().pipe(take(1)).subscribe((link: string) => {
@@ -189,6 +189,13 @@ export class EPersonDataService extends DataService<EPerson> {
}); });
} }
/**
* Method that clears a link's requests in cache
*/
public clearLinkRequests(href: string): void {
this.requestService.removeByHrefSubstring(href);
}
/** /**
* Method to retrieve the eperson that is currently being edited * Method to retrieve the eperson that is currently being edited
*/ */

View File

@@ -246,7 +246,7 @@ export class GroupDataService extends DataService<Group> {
} }
/** /**
* Method that clears a cached groups request and returns its REST url * Method that clears a cached groups request
*/ */
public clearGroupsRequests(): void { public clearGroupsRequests(): void {
this.getBrowseEndpoint().pipe(take(1)).subscribe((link: string) => { this.getBrowseEndpoint().pipe(take(1)).subscribe((link: string) => {
@@ -254,6 +254,13 @@ export class GroupDataService extends DataService<Group> {
}); });
} }
/**
* Method that clears a cached get subgroups of certain group request
*/
public clearGroupLinkRequests(href: string): void {
this.requestService.removeByHrefSubstring(href);
}
public getGroupRegistryRouterLink(): string { public getGroupRegistryRouterLink(): string {
return '/admin/access-control/groups'; return '/admin/access-control/groups';
} }

View File

@@ -2,35 +2,37 @@ import { Group } from '../../core/eperson/models/group.model';
import { EPersonMock } from './eperson-mock'; import { EPersonMock } from './eperson-mock';
export const GroupMock2: Group = Object.assign(new Group(), { export const GroupMock2: Group = Object.assign(new Group(), {
handle: null, handle: null,
subgroups: [], subgroups: [],
epersons: [], epersons: [],
selfRegistered: false, permanent: true,
_links: { selfRegistered: false,
self: { _links: {
href: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid2', self: {
href: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid2',
},
subgroups: { href: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid2/subgroups' },
epersons: { href: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid2/epersons' }
}, },
subgroups: { href: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid2/subgroups' }, id: 'testgroupid2',
epersons: { href: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid2/epersons' } uuid: 'testgroupid2',
}, type: 'group',
id: 'testgroupid2',
uuid: 'testgroupid2',
type: 'group',
}); });
export const GroupMock: Group = Object.assign(new Group(), { export const GroupMock: Group = Object.assign(new Group(), {
handle: null, handle: null,
subgroups: [GroupMock2], subgroups: [GroupMock2],
epersons: [EPersonMock], epersons: [EPersonMock],
selfRegistered: false, selfRegistered: false,
_links: { permanent: false,
self: { _links: {
href: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid', self: {
href: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid',
},
subgroups: { href: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid/subgroups' },
epersons: { href: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid/epersons' }
}, },
subgroups: { href: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid/subgroups' }, id: 'testgroupid',
epersons: { href: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid/epersons' } uuid: 'testgroupid',
}, type: 'group',
id: 'testgroupid',
uuid: 'testgroupid',
type: 'group',
}); });