From e6a454f863295f9694e5562b630239a5009e6401 Mon Sep 17 00:00:00 2001 From: Marie Verdonck Date: Wed, 26 Feb 2020 19:20:14 +0100 Subject: [PATCH 01/20] 69110: EPeople admin page - CRUD & search on name, email, md --- .../epeople-registry/epeople-registry.component.ts | 4 ++-- src/app/+admin/admin-routing.module.ts | 8 ++++---- src/app/core/cache/object-cache.service.ts | 2 ++ 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/app/+admin/admin-access-control/epeople-registry/epeople-registry.component.ts b/src/app/+admin/admin-access-control/epeople-registry/epeople-registry.component.ts index 38e15b1420..95d62bc431 100644 --- a/src/app/+admin/admin-access-control/epeople-registry/epeople-registry.component.ts +++ b/src/app/+admin/admin-access-control/epeople-registry/epeople-registry.component.ts @@ -12,8 +12,8 @@ import { NotificationsService } from '../../../shared/notifications/notification import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; @Component({ - selector: 'ds-epeople-registry', - templateUrl: './epeople-registry.component.html', + selector: 'ds-epeople-registry', + templateUrl: './epeople-registry.component.html', }) /** * A component used for managing all existing epeople within the repository. diff --git a/src/app/+admin/admin-routing.module.ts b/src/app/+admin/admin-routing.module.ts index b4a68d692a..3380306217 100644 --- a/src/app/+admin/admin-routing.module.ts +++ b/src/app/+admin/admin-routing.module.ts @@ -1,7 +1,7 @@ -import { RouterModule } from '@angular/router'; import { NgModule } from '@angular/core'; -import { URLCombiner } from '../core/url-combiner/url-combiner'; +import { RouterModule } from '@angular/router'; import { getAdminModulePath } from '../app-routing.module'; +import { URLCombiner } from '../core/url-combiner/url-combiner'; import { AdminSearchPageComponent } from './admin-search-page/admin-search-page.component'; import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; @@ -28,8 +28,8 @@ export function getRegistriesModulePath() { resolve: { breadcrumb: I18nBreadcrumbResolver }, component: AdminSearchPageComponent, data: { title: 'admin.search.title', breadcrumbKey: 'admin.search' } - }, - ]) + } + ]), ] }) export class AdminRoutingModule { diff --git a/src/app/core/cache/object-cache.service.ts b/src/app/core/cache/object-cache.service.ts index d82a1f31fe..036c9f0ed8 100644 --- a/src/app/core/cache/object-cache.service.ts +++ b/src/app/core/cache/object-cache.service.ts @@ -276,6 +276,8 @@ export class ObjectCacheService { * list of operations to perform */ 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 AddToSSBAction(selfLink, RestRequestMethod.PATCH)); } From 6f4167c6cf4e4bb73e1dea3476ef3fc71091caff Mon Sep 17 00:00:00 2001 From: Marie Verdonck Date: Fri, 28 Feb 2020 17:25:24 +0100 Subject: [PATCH 02/20] log removed --- src/app/core/cache/object-cache.service.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/app/core/cache/object-cache.service.ts b/src/app/core/cache/object-cache.service.ts index 036c9f0ed8..d82a1f31fe 100644 --- a/src/app/core/cache/object-cache.service.ts +++ b/src/app/core/cache/object-cache.service.ts @@ -276,8 +276,6 @@ export class ObjectCacheService { * list of operations to perform */ 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 AddToSSBAction(selfLink, RestRequestMethod.PATCH)); } From e0dc90ddd38480ac8086c0b55bb104036cf5dcef Mon Sep 17 00:00:00 2001 From: Marie Verdonck Date: Thu, 20 Feb 2020 18:23:08 +0100 Subject: [PATCH 03/20] 68930: add embed query param for followLinks in buildHref + tests removed fdescribe --- src/app/core/data/data.service.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/core/data/data.service.spec.ts b/src/app/core/data/data.service.spec.ts index c370be2b9e..f776dfea63 100644 --- a/src/app/core/data/data.service.spec.ts +++ b/src/app/core/data/data.service.spec.ts @@ -2,7 +2,6 @@ import { HttpClient } from '@angular/common/http'; import { Store } from '@ngrx/store'; import { compare, Operation } from 'fast-json-patch'; import { Observable, of as observableOf } from 'rxjs'; -import * as uuidv4 from 'uuid/v4'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; From c7963e5126887ca3bce2ebec636b5f51eda0040e Mon Sep 17 00:00:00 2001 From: Marie Verdonck Date: Mon, 2 Mar 2020 19:03:27 +0100 Subject: [PATCH 04/20] 69111: Groups admin page, WIP --- resources/i18n/en.json5 | 94 ++++++- .../admin-access-control-routing.module.ts | 13 + .../admin-access-control.module.ts | 10 +- .../epeople-registry.component.ts | 256 +++++++++--------- .../eperson-form/eperson-form.component.ts | 1 - .../group-form/group-form.component.html | 32 +++ .../group-form/group-form.component.ts | 227 ++++++++++++++++ .../members-list/members-list.component.html | 78 ++++++ .../members-list/members-list.component.ts | 152 +++++++++++ .../subgroups-list.component.html | 72 +++++ .../subgroup-list/subgroups-list.component.ts | 149 ++++++++++ .../group-registry/group-registry.actions.ts | 49 ++++ .../group-registry/group-registry.reducers.ts | 45 +++ .../groups-registry.component.html | 81 ++++++ .../groups-registry.component.ts | 135 +++++++++ .../admin-sidebar/admin-sidebar.component.ts | 2 +- src/app/app.reducer.ts | 6 + src/app/core/eperson/group-data.service.ts | 146 ++++++++-- src/app/core/eperson/models/group.model.ts | 16 ++ 19 files changed, 1413 insertions(+), 151 deletions(-) create mode 100644 src/app/+admin/admin-access-control/group-registry/group-form/group-form.component.html create mode 100644 src/app/+admin/admin-access-control/group-registry/group-form/group-form.component.ts create mode 100644 src/app/+admin/admin-access-control/group-registry/group-form/members-list/members-list.component.html create mode 100644 src/app/+admin/admin-access-control/group-registry/group-form/members-list/members-list.component.ts create mode 100644 src/app/+admin/admin-access-control/group-registry/group-form/subgroup-list/subgroups-list.component.html create mode 100644 src/app/+admin/admin-access-control/group-registry/group-form/subgroup-list/subgroups-list.component.ts create mode 100644 src/app/+admin/admin-access-control/group-registry/group-registry.actions.ts create mode 100644 src/app/+admin/admin-access-control/group-registry/group-registry.reducers.ts create mode 100644 src/app/+admin/admin-access-control/group-registry/groups-registry.component.html create mode 100644 src/app/+admin/admin-access-control/group-registry/groups-registry.component.ts diff --git a/resources/i18n/en.json5 b/resources/i18n/en.json5 index 24fbd9baf9..ff1c51461f 100644 --- a/resources/i18n/en.json5 +++ b/resources/i18n/en.json5 @@ -176,12 +176,10 @@ "admin.access-control.epeople.search.head": "Search", - "admin.access-control.epeople.search.scope.name": "Name", + "admin.access-control.epeople.search.scope.metadata": "Metadata", "admin.access-control.epeople.search.scope.email": "E-mail (exact)", - "admin.access-control.epeople.search.scope.metadata": "Metadata", - "admin.access-control.epeople.search.button": "Search", "admin.access-control.epeople.button.add": "Add EPerson", @@ -233,6 +231,96 @@ "admin.access-control.epeople.notification.deleted.success": "Successfully deleted EPerson: \"{{name}}\"", + "admin.access-control.groups.title": "DSpace Angular :: Groups", + + "admin.access-control.groups.head": "Groups", + + "admin.access-control.groups.button.add": "Add group", + + "admin.access-control.groups.search.head": "Search groups", + + "admin.access-control.groups.search.button": "Search", + + "admin.access-control.groups.table.id": "ID", + + "admin.access-control.groups.table.name": "Name", + + "admin.access-control.groups.table.members": "Members", + + "admin.access-control.groups.table.comcol": "Community / Collection", + + "admin.access-control.groups.table.edit": "Edit", + + "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}}\"", + // TODO + "admin.access-control.groups.notification.deleted.failure": "Failed to delete group \"{{name}}\" (Not yet implemented)", + + + "admin.access-control.groups.form.head.create": "Create group", + + "admin.access-control.groups.form.head.edit": "Edit group", + + "admin.access-control.groups.form.groupName": "Group name", + + "admin.access-control.groups.form.button.return": "Return", + + "admin.access-control.groups.form.notification.created.success": "Successfully created group \"{{name}}\"", + + "admin.access-control.groups.form.notification.created.failure": "Failed to create group \"{{name}}\"", + + "admin.access-control.groups.form.members-list.head": "Members", + + "admin.access-control.groups.form.members-list.search.head": "Search EPeople", + + "admin.access-control.groups.form.members-list.search.scope.metadata": "Metadata", + + "admin.access-control.groups.form.members-list.search.scope.email": "E-mail", + + "admin.access-control.groups.form.members-list.search.button": "Search", + + "admin.access-control.groups.form.members-list.table.id": "ID", + + "admin.access-control.groups.form.members-list.table.name": "Name", + + "admin.access-control.groups.form.members-list.table.edit": "Remove / Add", + + "admin.access-control.groups.form.members-list.table.edit.buttons.remove": "Remove member with name \"{{name}}\"", + + "admin.access-control.groups.form.members-list.table.edit.buttons.add": "Add member with name \"{{name}}\"", + + "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.subgroups-list.head": "Subgroups", + + "admin.access-control.groups.form.subgroups-list.search.head": "Search Groups", + + "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.name": "Name", + + "admin.access-control.groups.form.subgroups-list.table.edit": "Remove / Add", + + "admin.access-control.groups.form.subgroups-list.table.edit.buttons.remove": "Remove subgroup with name \"{{name}}\"", + + "admin.access-control.groups.form.subgroups-list.table.edit.buttons.add": "Add subgroup with name \"{{name}}\"", + + "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.button.see-all": "Search all", + + "admin.access-control.groups.form.return": "Return", + + "admin.search.breadcrumbs": "Administrative Search", diff --git a/src/app/+admin/admin-access-control/admin-access-control-routing.module.ts b/src/app/+admin/admin-access-control/admin-access-control-routing.module.ts index 83f67a770e..93e65708bc 100644 --- a/src/app/+admin/admin-access-control/admin-access-control-routing.module.ts +++ b/src/app/+admin/admin-access-control/admin-access-control-routing.module.ts @@ -1,11 +1,24 @@ import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; import { EPeopleRegistryComponent } from './epeople-registry/epeople-registry.component'; +import { GroupFormComponent } from './group-registry/group-form/group-form.component'; +import { GroupsRegistryComponent } from './group-registry/groups-registry.component'; @NgModule({ imports: [ RouterModule.forChild([ { path: 'epeople', component: EPeopleRegistryComponent, data: { title: 'admin.access-control.epeople.title' } }, + { path: 'groups', component: GroupsRegistryComponent, data: { title: 'admin.access-control.groups.title' } }, + { + path: 'groups/:groupId', + component: GroupFormComponent, + data: {title: 'admin.registries.schema.title'} + }, + { + path: 'groups/newGroup', + component: GroupFormComponent, + data: {title: 'admin.registries.schema.title'} + }, ]) ] }) diff --git a/src/app/+admin/admin-access-control/admin-access-control.module.ts b/src/app/+admin/admin-access-control/admin-access-control.module.ts index 0c8573e135..8b8ad2a420 100644 --- a/src/app/+admin/admin-access-control/admin-access-control.module.ts +++ b/src/app/+admin/admin-access-control/admin-access-control.module.ts @@ -6,6 +6,10 @@ import { SharedModule } from '../../shared/shared.module'; import { AdminAccessControlRoutingModule } from './admin-access-control-routing.module'; import { EPeopleRegistryComponent } from './epeople-registry/epeople-registry.component'; import { EPersonFormComponent } from './epeople-registry/eperson-form/eperson-form.component'; +import { GroupFormComponent } from './group-registry/group-form/group-form.component'; +import { MembersListComponent } from './group-registry/group-form/members-list/members-list.component'; +import { SubgroupsListComponent } from './group-registry/group-form/subgroup-list/subgroups-list.component'; +import { GroupsRegistryComponent } from './group-registry/groups-registry.component'; @NgModule({ imports: [ @@ -17,7 +21,11 @@ import { EPersonFormComponent } from './epeople-registry/eperson-form/eperson-fo ], declarations: [ EPeopleRegistryComponent, - EPersonFormComponent + EPersonFormComponent, + GroupsRegistryComponent, + GroupFormComponent, + SubgroupsListComponent, + MembersListComponent ], entryComponents: [] }) diff --git a/src/app/+admin/admin-access-control/epeople-registry/epeople-registry.component.ts b/src/app/+admin/admin-access-control/epeople-registry/epeople-registry.component.ts index 95d62bc431..4fd663a6b8 100644 --- a/src/app/+admin/admin-access-control/epeople-registry/epeople-registry.component.ts +++ b/src/app/+admin/admin-access-control/epeople-registry/epeople-registry.component.ts @@ -21,143 +21,143 @@ import { PaginationComponentOptions } from '../../../shared/pagination/paginatio */ export class EPeopleRegistryComponent { - labelPrefix = 'admin.access-control.epeople.'; + labelPrefix = 'admin.access-control.epeople.'; - /** - * A list of all the current EPeople within the repository or the result of the search - */ - ePeople: Observable>>; + /** + * A list of all the current EPeople within the repository or the result of the search + */ + ePeople: Observable>>; - /** - * Pagination config used to display the list of epeople - */ - config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { - id: 'epeople-list-pagination', - pageSize: 5, - currentPage: 1 + /** + * Pagination config used to display the list of epeople + */ + config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { + id: 'epeople-list-pagination', + pageSize: 5, + currentPage: 1 + }); + + /** + * Whether or not to show the EPerson form + */ + isEPersonFormShown: boolean; + + // The search form + searchForm; + + constructor(private epersonService: EPersonDataService, + private translateService: TranslateService, + private notificationsService: NotificationsService, + private formBuilder: FormBuilder) { + this.updateEPeople({ + currentPage: 1, + elementsPerPage: this.config.pageSize }); + this.isEPersonFormShown = false; + this.searchForm = this.formBuilder.group(({ + scope: 'metadata', + query: '', + })); + } - /** - * Whether or not to show the EPerson form - */ - isEPersonFormShown: boolean; + /** + * Event triggered when the user changes page + * @param event + */ + onPageChange(event) { + this.updateEPeople({ + currentPage: event, + elementsPerPage: this.config.pageSize + }); + } - // The search form - searchForm; + /** + * Update the list of EPeople by fetching it from the rest api or cache + */ + private updateEPeople(options) { + this.ePeople = this.epersonService.getEPeople(options); + } - constructor(private epersonService: EPersonDataService, - private translateService: TranslateService, - private notificationsService: NotificationsService, - private formBuilder: FormBuilder) { - this.updateEPeople({ - currentPage: 1, - elementsPerPage: this.config.pageSize - }); + /** + * Force-update the list of EPeople by first clearing the cache related to EPeople, then performing + * a new REST call + */ + public forceUpdateEPeople() { + this.epersonService.clearEPersonRequests(); + this.isEPersonFormShown = false; + this.search({ query: '', scope: 'metadata' }) + } + + /** + * Search in the EPeople by metadata (default) or email + * @param data Contains scope and query param + */ + search(data: any) { + this.ePeople = this.epersonService.searchByScope(data.scope, data.query, { + currentPage: 1, + elementsPerPage: this.config.pageSize + }); + } + + /** + * Checks whether the given EPerson is active (being edited) + * @param eperson + */ + isActive(eperson: EPerson): Observable { + return this.getActiveEPerson().pipe( + map((activeEPerson) => eperson === activeEPerson) + ); + } + + /** + * Gets the active eperson (being edited) + */ + getActiveEPerson(): Observable { + return this.epersonService.getActiveEPerson(); + } + + /** + * Start editing the selected EPerson + * @param ePerson + */ + toggleEditEPerson(ePerson: EPerson) { + this.getActiveEPerson().pipe(take(1)).subscribe((activeEPerson: EPerson) => { + if (ePerson === activeEPerson) { + this.epersonService.cancelEditEPerson(); this.isEPersonFormShown = false; - this.searchForm = this.formBuilder.group(({ - scope: 'metadata', - query: '', - })); - } + } else { + this.epersonService.editEPerson(ePerson); + this.isEPersonFormShown = true; + } + }); + this.scrollToTop() + } - /** - * Event triggered when the user changes page - * @param event - */ - onPageChange(event) { - this.updateEPeople({ - currentPage: event, - 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); - } - - /** - * Force-update the list of EPeople by first clearing the cache related to EPeople, then performing - * a new REST call - */ - public forceUpdateEPeople() { - this.epersonService.clearEPersonRequests(); - this.isEPersonFormShown = false; - this.search({ query: '', scope: 'metadata' }) - } - - /** - * Search in the EPeople by metadata (default) or email - * @param data Contains scope and query param - */ - search(data: any) { - this.ePeople = this.epersonService.searchByScope(data.scope, data.query, { - currentPage: 1, - elementsPerPage: this.config.pageSize - }); - } - - /** - * Checks whether the given EPerson is active (being edited) - * @param eperson - */ - isActive(eperson: EPerson): Observable { - return this.getActiveEPerson().pipe( - map((activeEPerson) => eperson === activeEPerson) - ); - } - - /** - * Gets the active eperson (being edited) - */ - getActiveEPerson(): Observable { - return this.epersonService.getActiveEPerson(); - } - - /** - * Start editing the selected EPerson - * @param ePerson - */ - toggleEditEPerson(ePerson: EPerson) { - this.getActiveEPerson().pipe(take(1)).subscribe((activeEPerson: EPerson) => { - if (ePerson === activeEPerson) { - this.epersonService.cancelEditEPerson(); - this.isEPersonFormShown = false; - } else { - this.epersonService.editEPerson(ePerson); - this.isEPersonFormShown = true; - } - }); - this.scrollToTop() - } - - /** - * Deletes EPerson, show notification on success/failure & updates EPeople list - */ - deleteEPerson(ePerson: EPerson) { - if (hasValue(ePerson.id)) { - this.epersonService.deleteEPerson(ePerson).pipe(take(1)).subscribe((success: boolean) => { - if (success) { - this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', { name: ePerson.name })); - this.forceUpdateEPeople(); - } else { - this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.deleted.failure', { name: ePerson.name })); - } - this.epersonService.cancelEditEPerson(); - this.isEPersonFormShown = false; - }) + /** + * Deletes EPerson, show notification on success/failure & updates EPeople list + */ + deleteEPerson(ePerson: EPerson) { + if (hasValue(ePerson.id)) { + this.epersonService.deleteEPerson(ePerson).pipe(take(1)).subscribe((success: boolean) => { + if (success) { + this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', { name: ePerson.name })); + this.forceUpdateEPeople(); + } else { + this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.deleted.failure', { name: ePerson.name })); } + this.epersonService.cancelEditEPerson(); + this.isEPersonFormShown = false; + }) } + } - scrollToTop() { - (function smoothscroll() { - const currentScroll = document.documentElement.scrollTop || document.body.scrollTop; - if (currentScroll > 0) { - window.requestAnimationFrame(smoothscroll); - window.scrollTo(0, currentScroll - (currentScroll / 8)); - } - })(); - } + scrollToTop() { + (function smoothscroll() { + const currentScroll = document.documentElement.scrollTop || document.body.scrollTop; + if (currentScroll > 0) { + window.requestAnimationFrame(smoothscroll); + window.scrollTo(0, currentScroll - (currentScroll / 8)); + } + })(); + } } diff --git a/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.ts b/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.ts index d3900aef87..c6cae75ab0 100644 --- a/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.ts +++ b/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.ts @@ -241,7 +241,6 @@ export class EPersonFormComponent implements OnInit, OnDestroy { * @param values */ createNewEPerson(values) { - console.log('createNewEPerson(values)', values) const ePersonToCreate = Object.assign(new EPerson(), values); const response = this.epersonService.tryToCreate(ePersonToCreate); diff --git a/src/app/+admin/admin-access-control/group-registry/group-form/group-form.component.html b/src/app/+admin/admin-access-control/group-registry/group-form/group-form.component.html new file mode 100644 index 0000000000..47c61cb024 --- /dev/null +++ b/src/app/+admin/admin-access-control/group-registry/group-form/group-form.component.html @@ -0,0 +1,32 @@ +
+
+
+ +
+ + +

{{messagePrefix + '.head.create' | translate}}

+
+ + +

{{messagePrefix + '.head.edit' | translate}}

+
+ + + + + + + +
+ +
+ +
+
+
diff --git a/src/app/+admin/admin-access-control/group-registry/group-form/group-form.component.ts b/src/app/+admin/admin-access-control/group-registry/group-form/group-form.component.ts new file mode 100644 index 0000000000..7ef0592d91 --- /dev/null +++ b/src/app/+admin/admin-access-control/group-registry/group-form/group-form.component.ts @@ -0,0 +1,227 @@ +import { Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core'; +import { FormGroup } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { + DynamicFormControlModel, + DynamicFormLayout, + DynamicInputModel, + DynamicTextAreaModel +} from '@ng-dynamic-forms/core'; +import { TranslateService } from '@ngx-translate/core'; +import { combineLatest } from 'rxjs/internal/observable/combineLatest'; +import { Subscription } from 'rxjs/internal/Subscription'; +import { take } from 'rxjs/operators'; +import { EPersonDataService } from '../../../../core/eperson/eperson-data.service'; +import { GroupDataService } from '../../../../core/eperson/group-data.service'; +import { Group } from '../../../../core/eperson/models/group.model'; +import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators'; +import { hasValue } from '../../../../shared/empty.util'; +import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service'; +import { NotificationsService } from '../../../../shared/notifications/notifications.service'; + +@Component({ + selector: 'ds-group-form', + templateUrl: './group-form.component.html' +}) +/** + * A form used for creating and editing groups + */ +export class GroupFormComponent implements OnInit, OnDestroy { + + messagePrefix = 'admin.access-control.groups.form'; + + /** + * A unique id used for ds-form + */ + formId = 'group-form'; + + /** + * Dynamic models for the inputs of form + */ + groupName: DynamicInputModel; + groupDescription: DynamicTextAreaModel; + + /** + * A list of all dynamic input models + */ + formModel: DynamicFormControlModel[]; + + /** + * Layout used for structuring the form inputs + */ + formLayout: DynamicFormLayout = { + groupName: { + grid: { + host: 'row' + } + }, + groupDescription: { + grid: { + host: 'row' + } + }, + }; + + /** + * A FormGroup that combines all inputs + */ + formGroup: FormGroup; + + /** + * An EventEmitter that's fired whenever the form is being submitted + */ + @Output() submitForm: EventEmitter = new EventEmitter(); + + /** + * An EventEmitter that's fired whenever the form is cancelled + */ + @Output() cancelForm: EventEmitter = new EventEmitter(); + + /** + * List of subscriptions + */ + subs: Subscription[] = []; + + constructor(public groupDataService: GroupDataService, + private ePersonDataService: EPersonDataService, + private formBuilderService: FormBuilderService, + private translateService: TranslateService, + private notificationsService: NotificationsService, + private route: ActivatedRoute, + protected router: Router) { + } + + ngOnInit() { + this.route.params.subscribe((params) => { + this.setActiveGroup(params.groupId) + }); + combineLatest( + this.translateService.get(`${this.messagePrefix}.groupName`), + this.translateService.get(`${this.messagePrefix}.groupDescription`), + ).subscribe(([groupName, groupDescription]) => { + this.groupName = new DynamicInputModel({ + id: 'groupName', + label: groupName, + name: 'groupName', + validators: { + required: null, + }, + required: true, + }); + this.groupDescription = new DynamicTextAreaModel({ + id: 'description', + label: groupDescription, + name: 'dc.description', + required: false, + }); + this.formModel = [ + this.groupName, + ]; + this.formGroup = this.formBuilderService.createFormGroup(this.formModel); + this.subs.push(this.groupDataService.getActiveGroup().subscribe((group: Group) => { + this.formGroup.patchValue({ + groupName: group != null ? group.name : '', + groupDescription: group != null ? group.firstMetadataValue('dc.description') : '', + }); + })); + }); + } + + /** + * Stop editing the currently selected group + */ + onCancel() { + this.groupDataService.cancelEditGroup(); + this.cancelForm.emit(); + this.router.navigate(['/admin/access-control/groups']); + } + + /** + * Submit the form + * When the eperson has an id attached -> Edit the eperson + * When the eperson has no id attached -> Create new eperson + * Emit the updated/created eperson using the EventEmitter submitForm + */ + onSubmit() { + this.groupDataService.getActiveGroup().pipe(take(1)).subscribe( + (group: Group) => { + const values = { + name: this.groupName.value, + metadata: { + 'dc.description': [ + { + value: this.groupDescription.value + } + ], + }, + }; + if (group == null) { + this.createNewGroup(values); + } else { + this.editGroup(group, values); + } + this.clearFields(); + } + ); + } + + /** + * Creates new Group based on given values from form + * @param values + */ + createNewGroup(values) { + this.subs.push(this.groupDataService.createOrUpdateGroup(Object.assign(new Group(), values)) + .pipe( + getSucceededRemoteData(), + getRemoteDataPayload()) + .subscribe((group: Group) => { + this.notificationsService.success(this.translateService.get(this.messagePrefix + '.notification.created.success', { name: group.name })); + this.submitForm.emit(group); + })); + } + + /** + * //TODO + * @param group + * @param values + */ + editGroup(group: Group, values) { + // TODO + } + + /** + * Reset all input-fields to be empty + */ + clearFields() { + this.formGroup.patchValue({ + groupName: '', + }); + } + + /** + * Start editing the selected group + * @param group + */ + setActiveGroup(groupId: string) { + this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => { + if (activeGroup === null) { + this.groupDataService.cancelEditGroup(); + this.groupDataService.findById(groupId) + .pipe( + getSucceededRemoteData(), + getRemoteDataPayload()) + .subscribe((group: Group) => { + this.groupDataService.editGroup(group); + }) + } + }); + } + + /** + * Cancel the current edit when component is destroyed & unsub all subscriptions + */ + ngOnDestroy(): void { + this.onCancel(); + this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe()); + } +} diff --git a/src/app/+admin/admin-access-control/group-registry/group-form/members-list/members-list.component.html b/src/app/+admin/admin-access-control/group-registry/group-form/members-list/members-list.component.html new file mode 100644 index 0000000000..5fb30ecf82 --- /dev/null +++ b/src/app/+admin/admin-access-control/group-registry/group-form/members-list/members-list.component.html @@ -0,0 +1,78 @@ + +

{{messagePrefix + '.head' | translate}}

+ + +
+
+ +
+
+
+ + + + +
+
+
+ + + +
+ + + + + + + + + + + + + + + +
{{messagePrefix + '.table.id' | translate}}{{messagePrefix + '.table.name' | translate}}{{messagePrefix + '.table.edit' | translate}}
{{ePerson.id}}{{ePerson.name}} +
+ + + +
+
+
+ +
+ + + + + +
diff --git a/src/app/+admin/admin-access-control/group-registry/group-form/members-list/members-list.component.ts b/src/app/+admin/admin-access-control/group-registry/group-form/members-list/members-list.component.ts new file mode 100644 index 0000000000..d794976db0 --- /dev/null +++ b/src/app/+admin/admin-access-control/group-registry/group-form/members-list/members-list.component.ts @@ -0,0 +1,152 @@ +import { Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; +import { TranslateService } from '@ngx-translate/core'; +import { Observable, of as observableOf, Subscription } from 'rxjs'; +import { map, mergeMap, take } from 'rxjs/operators'; +import { PaginatedList } from '../../../../../core/data/paginated-list'; +import { RemoteData } from '../../../../../core/data/remote-data'; +import { EPersonDataService } from '../../../../../core/eperson/eperson-data.service'; +import { GroupDataService } from '../../../../../core/eperson/group-data.service'; +import { EPerson } from '../../../../../core/eperson/models/eperson.model'; +import { Group } from '../../../../../core/eperson/models/group.model'; +import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../../core/shared/operators'; +import { hasValue } from '../../../../../shared/empty.util'; +import { NotificationsService } from '../../../../../shared/notifications/notifications.service'; +import { PaginationComponentOptions } from '../../../../../shared/pagination/pagination-component-options.model'; + +@Component({ + selector: 'ds-members-list', + templateUrl: './members-list.component.html' +}) +/** + * The list of members in the edit group page + */ +export class MembersListComponent implements OnInit, OnDestroy { + + @Input() + messagePrefix: string; + + ePeople: Observable>>; + + /** + * Pagination config used to display the list of EPeople + */ + config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { + id: 'members-list-pagination', + pageSize: 5, + currentPage: 1 + }); + + /** + * List of subscriptions + */ + subs: Subscription[] = []; + + // The search form + searchForm; + + /** + * Whether or not user has done a search yet + */ + searchDone: boolean; + + constructor(private groupDataService: GroupDataService, + private ePersonDataService: EPersonDataService, + private translateService: TranslateService, + private notificationsService: NotificationsService, + private formBuilder: FormBuilder) { + } + + 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(({ + scope: 'metadata', + query: '', + })); + this.searchDone = false; + } + + /** + * Event triggered when the user changes page + * @param event + */ + onPageChange(event) { + this.updateMembers({ + currentPage: event, + 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); + } + + deleteMemberFromGroup(ePerson: EPerson) { + // TODO + console.log('deleteMember TODO', ePerson); + // this.forceUpdateEPeople(); + } + + addMemberToGroup(ePerson: EPerson) { + // TODO + console.log('addMember TODO', ePerson); + // this.forceUpdateEPeople(); + } + + isMemberOfGroup(ePerson: EPerson): Observable { + return this.groupDataService.getActiveGroup().pipe(take(1), + mergeMap((group: Group) => { + if (group != null) { + return this.groupDataService.findAllByHref(ePerson._links.groups.href, { + currentPage: 0, + elementsPerPage: Number.MAX_SAFE_INTEGER + }) + .pipe( + getSucceededRemoteData(), + getRemoteDataPayload(), + map((listTotalGroups: PaginatedList) => listTotalGroups.page.filter((groupInList: Group) => groupInList.id === group.id)), + map((groups: Group[]) => groups.length > 0)) + } else { + return observableOf(false); + } + })) + } + + /** + * Search in the EPeople by name, email or metadata + * @param data Contains scope and query param + */ + search(data: any) { + this.searchDone = true; + this.ePeople = this.ePersonDataService.searchByScope(data.scope, data.query, { + currentPage: 1, + elementsPerPage: this.config.pageSize + }); + } + + /** + * Force-update the list of EPeople by first clearing the cache related to EPeople, then performing + * a new REST call + */ + public forceUpdateEPeople() { + this.ePersonDataService.clearEPersonRequests(); + this.search({ query: '', scope: 'metadata' }) + } + + /** + * unsub all subscriptions + */ + ngOnDestroy(): void { + this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe()); + } +} diff --git a/src/app/+admin/admin-access-control/group-registry/group-form/subgroup-list/subgroups-list.component.html b/src/app/+admin/admin-access-control/group-registry/group-form/subgroup-list/subgroups-list.component.html new file mode 100644 index 0000000000..16de581e96 --- /dev/null +++ b/src/app/+admin/admin-access-control/group-registry/group-form/subgroup-list/subgroups-list.component.html @@ -0,0 +1,72 @@ + +

{{messagePrefix + '.head' | translate}}

+ + +
+
+
+ + + + +
+
+
+ + + +
+ + + + + + + + + + + + + + + +
{{messagePrefix + '.table.id' | translate}}{{messagePrefix + '.table.name' | translate}}{{messagePrefix + '.table.edit' | translate}}
{{group.id}}{{group.name}} +
+ + + +
+
+
+ +
+ + + + + +
diff --git a/src/app/+admin/admin-access-control/group-registry/group-form/subgroup-list/subgroups-list.component.ts b/src/app/+admin/admin-access-control/group-registry/group-form/subgroup-list/subgroups-list.component.ts new file mode 100644 index 0000000000..afa226ef76 --- /dev/null +++ b/src/app/+admin/admin-access-control/group-registry/group-form/subgroup-list/subgroups-list.component.ts @@ -0,0 +1,149 @@ +import { Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; +import { TranslateService } from '@ngx-translate/core'; +import { Observable, of as observableOf, Subscription } from 'rxjs'; +import { map, mergeMap, take } from 'rxjs/operators'; +import { PaginatedList } from '../../../../../core/data/paginated-list'; +import { RemoteData } from '../../../../../core/data/remote-data'; +import { GroupDataService } from '../../../../../core/eperson/group-data.service'; +import { Group } from '../../../../../core/eperson/models/group.model'; +import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../../core/shared/operators'; +import { hasValue } from '../../../../../shared/empty.util'; +import { NotificationsService } from '../../../../../shared/notifications/notifications.service'; +import { PaginationComponentOptions } from '../../../../../shared/pagination/pagination-component-options.model'; +import { followLink } from '../../../../../shared/utils/follow-link-config.model'; + +@Component({ + selector: 'ds-subgroups-list', + templateUrl: './subgroups-list.component.html' +}) +/** + * The list of subgroups in the edit group page + */ +export class SubgroupsListComponent implements OnInit, OnDestroy { + + @Input() + messagePrefix: string; + + groups: Observable>>; + + /** + * List of subscriptions + */ + subs: Subscription[] = []; + + /** + * Pagination config used to display the list of subgroups + */ + config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { + id: 'subgroups-list-pagination', + pageSize: 5, + currentPage: 1 + }); + + // The search form + searchForm; + + /** + * Whether or not user has done a search yet + */ + searchDone: boolean; + + constructor(public groupDataService: GroupDataService, + private translateService: TranslateService, + private notificationsService: NotificationsService, + private formBuilder: FormBuilder) { + } + + ngOnInit() { + this.subs.push(this.groupDataService.getActiveGroup().subscribe((group: Group) => { + if (group != null) { + this.groups = this.groupDataService.findAllByHref(group._links.groups.href, { + currentPage: 1, + elementsPerPage: this.config.pageSize + }) + } + })); + this.searchForm = this.formBuilder.group(({ + query: '', + })); + this.searchDone = false; + } + + /** + * Event triggered when the user changes page + * @param event + */ + onPageChange(event) { + this.updateSubgroups({ + currentPage: event, + 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); + } + + isSubgroupOfGroup(possibleSubgroup: Group): Observable { + return this.groupDataService.getActiveGroup().pipe(take(1), + mergeMap((group: Group) => { + if (group != null) { + return this.groupDataService.findAllByHref(group._links.groups.href, { + currentPage: 0, + elementsPerPage: Number.MAX_SAFE_INTEGER + }) + .pipe( + getSucceededRemoteData(), + getRemoteDataPayload(), + map((listTotalGroups: PaginatedList) => listTotalGroups.page.filter((groupInList: Group) => groupInList.id === possibleSubgroup.id)), + map((groups: Group[]) => groups.length > 0)) + } else { + return observableOf(false); + } + })) + } + + deleteSubgroupFromGroup(group: Group) { + // TODO + console.log('deleteSubgroup TODO', group); + // this.forceUpdateGroup(); + } + + addSubgroupToGroup(group: Group) { + // TODO + console.log('addSubgroup TODO', group); + // this.forceUpdateGroup(); + } + + /** + * Search in the groups (searches by group name and by uuid exact match) + * @param data Contains query param + */ + search(data: any) { + this.searchDone = true; + const query: string = data.query; + this.groups = this.groupDataService.searchGroups(query.trim(), { + currentPage: 1, + elementsPerPage: this.config.pageSize + }, followLink('epersons')); + } + + /** + * Force-update the list of groups by first clearing the cache related to groups, then performing a new REST call + */ + public forceUpdateGroup() { + this.groupDataService.clearGroupsRequests(); + this.search({ query: '' }) + } + + /** + * unsub all subscriptions + */ + ngOnDestroy(): void { + this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe()); + } +} diff --git a/src/app/+admin/admin-access-control/group-registry/group-registry.actions.ts b/src/app/+admin/admin-access-control/group-registry/group-registry.actions.ts new file mode 100644 index 0000000000..3a0f3bc5c5 --- /dev/null +++ b/src/app/+admin/admin-access-control/group-registry/group-registry.actions.ts @@ -0,0 +1,49 @@ +import { Action } from '@ngrx/store'; +import { Group } from '../../../core/eperson/models/group.model'; +import { type } from '../../../shared/ngrx/type'; + +/** + * For each action type in an action group, make a simple + * enum object for all of this group's action types. + * + * The 'type' utility function coerces strings into string + * literal types and runs a simple check to guarantee all + * action types in the application are unique. + */ +export const GroupRegistryActionTypes = { + + EDIT_GROUP: type('dspace/epeople-registry/EDIT_GROUP'), + CANCEL_EDIT_GROUP: type('dspace/epeople-registry/CANCEL_EDIT_GROUP'), +}; + +/* tslint:disable:max-classes-per-file */ +/** + * Used to edit a Group in the Group registry + */ +export class GroupRegistryEditGroupAction implements Action { + type = GroupRegistryActionTypes.EDIT_GROUP; + + group: Group; + + constructor(group: Group) { + this.group = group; + } +} + +/** + * Used to cancel the editing of a Group in the Group registry + */ +export class GroupRegistryCancelGroupAction implements Action { + type = GroupRegistryActionTypes.CANCEL_EDIT_GROUP; +} + +/* tslint:enable:max-classes-per-file */ + +/** + * Export a type alias of all actions in this action group + * so that reducers can easily compose action types + * These are all the actions to perform on the EPeople registry state + */ +export type GroupRegistryAction + = GroupRegistryEditGroupAction + | GroupRegistryCancelGroupAction diff --git a/src/app/+admin/admin-access-control/group-registry/group-registry.reducers.ts b/src/app/+admin/admin-access-control/group-registry/group-registry.reducers.ts new file mode 100644 index 0000000000..60bfd33041 --- /dev/null +++ b/src/app/+admin/admin-access-control/group-registry/group-registry.reducers.ts @@ -0,0 +1,45 @@ +import { Group } from '../../../core/eperson/models/group.model'; +import { GroupRegistryAction, GroupRegistryActionTypes, GroupRegistryEditGroupAction } from './group-registry.actions'; + +/** + * The metadata registry state. + * @interface GroupRegistryState + */ +export interface GroupRegistryState { + editGroup: Group; + selectedGroup: Group[]; +} + +/** + * The initial state. + */ +const initialState: GroupRegistryState = { + editGroup: null, + selectedGroup: [], +}; + +/** + * Reducer that handles GroupRegistryActions to modify Groups + * @param state The current GroupRegistryState + * @param action The GroupRegistryAction to perform on the state + */ +export function groupRegistryReducer(state = initialState, action: GroupRegistryAction): GroupRegistryState { + + switch (action.type) { + + case GroupRegistryActionTypes.EDIT_GROUP: { + return Object.assign({}, state, { + editGroup: (action as GroupRegistryEditGroupAction).group + }); + } + + case GroupRegistryActionTypes.CANCEL_EDIT_GROUP: { + return Object.assign({}, state, { + editGroup: null + }); + } + + default: + return state; + } +} diff --git a/src/app/+admin/admin-access-control/group-registry/groups-registry.component.html b/src/app/+admin/admin-access-control/group-registry/groups-registry.component.html new file mode 100644 index 0000000000..f9883bfd5b --- /dev/null +++ b/src/app/+admin/admin-access-control/group-registry/groups-registry.component.html @@ -0,0 +1,81 @@ +
+
+
+ + + +
+ +
+ + +
+
+
+ + + + +
+
+
+ + + +
+ + + + + + + + + + + + + + + + + + + +
{{messagePrefix + 'table.id' | translate}}{{messagePrefix + 'table.name' | translate}}{{messagePrefix + 'table.members' | translate}}{{messagePrefix + 'table.edit' | translate}}
{{group.id}}{{group.name}}{{(getMembers(group) | async)?.payload?.totalElements}} +
+ + +
+
+
+ +
+ + + +
+
+
diff --git a/src/app/+admin/admin-access-control/group-registry/groups-registry.component.ts b/src/app/+admin/admin-access-control/group-registry/groups-registry.component.ts new file mode 100644 index 0000000000..62912c94fb --- /dev/null +++ b/src/app/+admin/admin-access-control/group-registry/groups-registry.component.ts @@ -0,0 +1,135 @@ +import { Component } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; +import { TranslateService } from '@ngx-translate/core'; +import { Observable } from 'rxjs'; +import { take } from 'rxjs/operators'; +import { PaginatedList } from '../../../core/data/paginated-list'; +import { RemoteData } from '../../../core/data/remote-data'; +import { EPersonDataService } from '../../../core/eperson/eperson-data.service'; +import { GroupDataService } from '../../../core/eperson/group-data.service'; +import { EPerson } from '../../../core/eperson/models/eperson.model'; +import { Group } from '../../../core/eperson/models/group.model'; +import { hasValue } from '../../../shared/empty.util'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; +import { followLink } from '../../../shared/utils/follow-link-config.model'; + +@Component({ + selector: 'ds-groups-registry', + templateUrl: './groups-registry.component.html', +}) +/** + * A component used for managing all existing groups within the repository. + * The admin can create, edit or delete groups here. + */ +export class GroupsRegistryComponent { + + messagePrefix = 'admin.access-control.groups.'; + + /** + * Pagination config used to display the list of groups + */ + config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { + id: 'groups-list-pagination', + pageSize: 5, + currentPage: 1 + }); + + /** + * A list of all the current groups within the repository or the result of the search + */ + groups: Observable>>; + + // The search form + searchForm; + + constructor(private groupService: GroupDataService, + private ePersonDataService: EPersonDataService, + private translateService: TranslateService, + private notificationsService: NotificationsService, + private formBuilder: FormBuilder) { + this.updateGroups({ + currentPage: 1, + elementsPerPage: this.config.pageSize + }); + this.searchForm = this.formBuilder.group(({ + query: '', + })); + } + + /** + * Event triggered when the user changes page + * @param event + */ + onPageChange(event) { + this.updateGroups({ + currentPage: event, + 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')); + } + + /** + * Search in the groups (searches by group name and by uuid exact match) + * @param data Contains query param + */ + search(data: any) { + const query: string = data.query; + this.groups = this.groupService.searchGroups(query.trim(), { + currentPage: 1, + elementsPerPage: this.config.pageSize + }, followLink('epersons')); + } + + /** + * Delete Group + */ + deleteGroup(group: Group) { + if (hasValue(group.id)) { + this.groupService.deleteGroup(group).pipe(take(1)).subscribe((success: boolean) => { + if (success) { + this.notificationsService.success(this.translateService.get(this.messagePrefix + 'notification.deleted.success', { name: group.name })); + this.forceUpdateGroup(); + } else { + this.notificationsService.error(this.translateService.get(this.messagePrefix + 'notification.deleted.failure', { name: group.name })); + } + }) + } + } + + /** + * Force-update the list of groups by first clearing the cache related to groups, then performing a new REST call + */ + public forceUpdateGroup() { + this.groupService.clearGroupsRequests(); + this.search({ query: '' }) + } + + /** + * Get the amount of members (epersons embedded value of a group) //TODO + * @param group + */ + getMembers(group: Group): Observable>> { + return this.ePersonDataService.findAllByHref(group._links.epersons.href); + } + + /** + * 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 + * @param groupName + */ + getOptionalComColFromName(groupName: string): string { + let optionalComColName = ''; + const uuidMatches = groupName.match(/[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}/g); + if (uuidMatches != null) { + optionalComColName = uuidMatches[0]; + } + return optionalComColName; + } +} diff --git a/src/app/+admin/admin-sidebar/admin-sidebar.component.ts b/src/app/+admin/admin-sidebar/admin-sidebar.component.ts index e3f55b8e18..79aad4599d 100644 --- a/src/app/+admin/admin-sidebar/admin-sidebar.component.ts +++ b/src/app/+admin/admin-sidebar/admin-sidebar.component.ts @@ -336,7 +336,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit { model: { type: MenuItemType.LINK, text: 'menu.section.access_control_groups', - link: '' + link: '/admin/access-control/groups' } as LinkMenuItemModel, }, { diff --git a/src/app/app.reducer.ts b/src/app/app.reducer.ts index a40005814a..e25ddcd44d 100644 --- a/src/app/app.reducer.ts +++ b/src/app/app.reducer.ts @@ -4,6 +4,10 @@ import { ePeopleRegistryReducer, EPeopleRegistryState } from './+admin/admin-access-control/epeople-registry/epeople-registry.reducers'; +import { + groupRegistryReducer, + GroupRegistryState +} from './+admin/admin-access-control/group-registry/group-registry.reducers'; import { metadataRegistryReducer, MetadataRegistryState @@ -47,6 +51,7 @@ export interface AppState { relationshipLists: NameVariantListsState; communityList: CommunityListState; epeopleRegistry: EPeopleRegistryState; + groupRegistry: GroupRegistryState; } export const appReducers: ActionReducerMap = { @@ -66,6 +71,7 @@ export const appReducers: ActionReducerMap = { relationshipLists: nameVariantReducer, communityList: CommunityListReducer, epeopleRegistry: ePeopleRegistryReducer, + groupRegistry: groupRegistryReducer, }; export const routerStateSelector = (state: AppState) => state.router; diff --git a/src/app/core/eperson/group-data.service.ts b/src/app/core/eperson/group-data.service.ts index 532f42323a..86fe8a88cd 100644 --- a/src/app/core/eperson/group-data.service.ts +++ b/src/app/core/eperson/group-data.service.ts @@ -1,28 +1,38 @@ -import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; -import { Store } from '@ngrx/store'; +import { createSelector, select, Store } from '@ngrx/store'; import { Observable } from 'rxjs'; import { filter, map, take } from 'rxjs/operators'; +import { + GroupRegistryCancelGroupAction, + GroupRegistryEditGroupAction +} from '../../+admin/admin-access-control/group-registry/group-registry.actions'; +import { GroupRegistryState } from '../../+admin/admin-access-control/group-registry/group-registry.reducers'; +import { AppState } from '../../app.reducer'; +import { hasValue } from '../../shared/empty.util'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { SearchParam } from '../cache/models/search-param.model'; +import { ObjectCacheService } from '../cache/object-cache.service'; import { DataService } from '../data/data.service'; +import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service'; +import { PaginatedList } from '../data/paginated-list'; +import { RemoteData } from '../data/remote-data'; +import { FindListOptions, FindListRequest } from '../data/request.models'; import { RequestService } from '../data/request.service'; -import { FindListOptions } from '../data/request.models'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { Group } from './models/group.model'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { CoreState } from '../core.reducers'; -import { ObjectCacheService } from '../cache/object-cache.service'; -import { SearchParam } from '../cache/models/search-param.model'; -import { RemoteData } from '../data/remote-data'; -import { PaginatedList } from '../data/paginated-list'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service'; import { dataService } from '../cache/builders/build-decorators'; import { GROUP } from './models/group.resource-type'; +const groupRegistryStateSelector = (state: AppState) => state.groupRegistry; +const editGroupSelector = createSelector(groupRegistryStateSelector, (groupRegistryState: GroupRegistryState) => groupRegistryState.editGroup); + /** - * Provides methods to retrieve eperson group resources. + * Provides methods to retrieve eperson group resources from the REST API & Group related CRUD actions. */ @Injectable({ providedIn: 'root' @@ -38,13 +48,51 @@ export class GroupDataService extends DataService { protected notificationsService: NotificationsService, protected requestService: RequestService, protected rdbService: RemoteDataBuildService, - protected store: Store, + protected store: Store, protected objectCache: ObjectCacheService, protected halService: HALEndpointService ) { super(); } + /** + * Retrieves all groups + * @param pagination The pagination info used to retrieve the groups + */ + public getGroups(options: FindListOptions = {}, ...linksToFollow: Array>): Observable>> { + const hrefObs = this.getFindAllHref(options, this.linkPath, ...linksToFollow); + hrefObs.pipe( + filter((href: string) => hasValue(href)), + take(1)) + .subscribe((href: string) => { + const request = new FindListRequest(this.requestService.generateRequestId(), href, options); + this.requestService.configure(request); + }); + + return this.rdbService.buildList(hrefObs) as Observable>>; + } + + /** + * Returns a search result list of groups, with certain query (searches in group name and by exact uuid) + * Endpoint used: /eperson/groups/search/byMetadata?query=<:name> + * @param query search query param + * @param options + * @param linksToFollow + */ + public searchGroups(query: string, options?: FindListOptions, ...linksToFollow: Array>): Observable>> { + const searchParams = [new SearchParam('query', query)]; + let findListOptions = new FindListOptions(); + if (options) { + findListOptions = Object.assign(new FindListOptions(), options); + } + if (findListOptions.searchParams) { + findListOptions.searchParams = [...findListOptions.searchParams, ...searchParams]; + } else { + findListOptions.searchParams = searchParams; + } + return this.searchBy('byMetadata', findListOptions, ...linksToFollow); + } + /** * Check if the current user is member of to the indicated group * @@ -59,10 +107,74 @@ export class GroupDataService extends DataService { options.searchParams = [new SearchParam('groupName', groupName)]; return this.searchBy(searchHref, options).pipe( - filter((groups: RemoteData>) => !groups.isResponsePending), - take(1), - map((groups: RemoteData>) => groups.payload.totalElements > 0) - ); + filter((groups: RemoteData>) => !groups.isResponsePending), + take(1), + map((groups: RemoteData>) => groups.payload.totalElements > 0) + ); + } + + /** + * Method to delete a group + * @param id The group id to delete + */ + public deleteGroup(group: Group): Observable { + return this.delete(group); + } + + /** + * Create or Update a group + * If the group contains an id, it is assumed the eperson already exists and is updated instead + * //TODO + * @param group The group to create or update + */ + public createOrUpdateGroup(group: Group): Observable> { + const isUpdate = hasValue(group.id); + if (isUpdate) { + return this.updateGroup(group); + } else { + console.log('group create', group) + return this.create(group, null); + } + } + + /** + * // TODO + * @param {DSpaceObject} ePerson The given object + */ + updateGroup(group: Group): Observable> { + // TODO + return null; + } + + /** + * Method to retrieve the group that is currently being edited + */ + public getActiveGroup(): Observable { + return this.store.pipe(select(editGroupSelector)) + } + + /** + * Method to cancel editing a group, dispatches a cancel group action + */ + public cancelEditGroup() { + this.store.dispatch(new GroupRegistryCancelGroupAction()); + } + + /** + * Method to set the group being edited, dispatches an edit group action + * @param group The group to edit + */ + public editGroup(group: Group) { + this.store.dispatch(new GroupRegistryEditGroupAction(group)); + } + + /** + * Method that clears a cached groups request and returns its REST url + */ + public clearGroupsRequests(): void { + this.getBrowseEndpoint().pipe(take(1)).subscribe((link: string) => { + this.requestService.removeByHrefSubstring(link); + }); } } diff --git a/src/app/core/eperson/models/group.model.ts b/src/app/core/eperson/models/group.model.ts index 5d531800b8..dcdfefb0b0 100644 --- a/src/app/core/eperson/models/group.model.ts +++ b/src/app/core/eperson/models/group.model.ts @@ -6,6 +6,8 @@ import { RemoteData } from '../../data/remote-data'; import { DSpaceObject } from '../../shared/dspace-object.model'; import { HALLink } from '../../shared/hal-link.model'; +import { EPerson } from './eperson.model'; +import { EPERSON } from './eperson.resource-type'; import { GROUP } from './group.resource-type'; @typedObject @@ -13,6 +15,12 @@ import { GROUP } from './group.resource-type'; export class Group extends DSpaceObject { static type = GROUP; + /** + * A string representing the unique name of this Group + */ + @autoserialize + public name: string; + /** * A string representing the unique handle of this Group */ @@ -32,6 +40,7 @@ export class Group extends DSpaceObject { _links: { self: HALLink; groups: HALLink; + epersons: HALLink; }; /** @@ -41,4 +50,11 @@ export class Group extends DSpaceObject { @link(GROUP, true) public groups?: Observable>>; + /** + * The list of EPeople in this group + * Will be undefined unless the epersons {@link HALLink} has been resolved. + */ + @link(EPERSON, true) + public epersons?: Observable>>; + } From 8e3699f6a35dd5d738ef8add49acbba414e3678f Mon Sep 17 00:00:00 2001 From: Marie Verdonck Date: Tue, 3 Mar 2020 15:26:45 +0100 Subject: [PATCH 05/20] 69111: Groups admin page, edit group add subgroups/members ok --- resources/i18n/en.json5 | 10 ++- .../group-form/group-form.component.ts | 22 +++---- .../members-list/members-list.component.ts | 50 ++++++++++++--- .../subgroup-list/subgroups-list.component.ts | 51 +++++++++++---- .../groups-registry.component.ts | 5 +- src/app/core/eperson/group-data.service.ts | 63 +++++++++++++++++-- 6 files changed, 159 insertions(+), 42 deletions(-) diff --git a/resources/i18n/en.json5 b/resources/i18n/en.json5 index ff1c51461f..3292ebeb69 100644 --- a/resources/i18n/en.json5 +++ b/resources/i18n/en.json5 @@ -254,8 +254,8 @@ "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}}\"", - // TODO - "admin.access-control.groups.notification.deleted.failure": "Failed to delete group \"{{name}}\" (Not yet implemented)", + + "admin.access-control.groups.notification.deleted.failure": "Failed to delete group \"{{name}}\"", "admin.access-control.groups.form.head.create": "Create group", @@ -264,6 +264,8 @@ "admin.access-control.groups.form.groupName": "Group name", + "admin.access-control.groups.form.groupDescription": "Description", + "admin.access-control.groups.form.button.return": "Return", "admin.access-control.groups.form.notification.created.success": "Successfully created group \"{{name}}\"", @@ -290,6 +292,8 @@ "admin.access-control.groups.form.members-list.table.edit.buttons.add": "Add member with name \"{{name}}\"", + "admin.access-control.groups.form.members-list.notification.failure.noActiveGroup": "No current active group, submit a name first.", + "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", @@ -312,6 +316,8 @@ "admin.access-control.groups.form.subgroups-list.table.edit.buttons.add": "Add subgroup with name \"{{name}}\"", + "admin.access-control.groups.form.subgroups-list.notification.failure.noActiveGroup": "No current active group, submit a name first.", + "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.", diff --git a/src/app/+admin/admin-access-control/group-registry/group-form/group-form.component.ts b/src/app/+admin/admin-access-control/group-registry/group-form/group-form.component.ts index 7ef0592d91..eb75a1d024 100644 --- a/src/app/+admin/admin-access-control/group-registry/group-form/group-form.component.ts +++ b/src/app/+admin/admin-access-control/group-registry/group-form/group-form.component.ts @@ -109,13 +109,14 @@ export class GroupFormComponent implements OnInit, OnDestroy { required: true, }); this.groupDescription = new DynamicTextAreaModel({ - id: 'description', + id: 'groupDescription', label: groupDescription, - name: 'dc.description', + name: 'groupDescription', required: false, }); this.formModel = [ this.groupName, + this.groupDescription ]; this.formGroup = this.formBuilderService.createFormGroup(this.formModel); this.subs.push(this.groupDataService.getActiveGroup().subscribe((group: Group) => { @@ -160,7 +161,6 @@ export class GroupFormComponent implements OnInit, OnDestroy { } else { this.editGroup(group, values); } - this.clearFields(); } ); } @@ -176,26 +176,20 @@ export class GroupFormComponent implements OnInit, OnDestroy { getRemoteDataPayload()) .subscribe((group: Group) => { this.notificationsService.success(this.translateService.get(this.messagePrefix + '.notification.created.success', { name: group.name })); + this.setActiveGroup(group.id); this.submitForm.emit(group); })); } /** - * //TODO + * // TODO * @param group * @param values */ editGroup(group: Group, values) { - // TODO - } - - /** - * Reset all input-fields to be empty - */ - clearFields() { - this.formGroup.patchValue({ - groupName: '', - }); + // TODO (backend) + console.log('TODO implement editGroup', values); + this.notificationsService.error('TODO implement editGroup (not yet implemented in backend) '); } /** diff --git a/src/app/+admin/admin-access-control/group-registry/group-form/members-list/members-list.component.ts b/src/app/+admin/admin-access-control/group-registry/group-form/members-list/members-list.component.ts index d794976db0..545e23d0bb 100644 --- a/src/app/+admin/admin-access-control/group-registry/group-form/members-list/members-list.component.ts +++ b/src/app/+admin/admin-access-control/group-registry/group-form/members-list/members-list.component.ts @@ -26,6 +26,9 @@ export class MembersListComponent implements OnInit, OnDestroy { @Input() messagePrefix: string; + /** + * EPeople being displayed, initially all members, after search result of search + */ ePeople: Observable>>; /** @@ -91,23 +94,45 @@ export class MembersListComponent implements OnInit, OnDestroy { this.ePeople = this.ePersonDataService.getEPeople(options); } + /** + * Deletes a given EPerson from the members list of the group currently being edited + * @param ePerson EPerson we want to delete as member from group that is currently being edited + */ deleteMemberFromGroup(ePerson: EPerson) { - // TODO - console.log('deleteMember TODO', ePerson); - // this.forceUpdateEPeople(); + this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => { + if (activeGroup != null) { + this.groupDataService.deleteMemberFromGroup(activeGroup, ePerson); + this.forceUpdateEPeople(activeGroup); + } else { + this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup')); + } + }); } + /** + * Adds a given EPerson to the members list of the group currently being edited + * @param ePerson EPerson we want to add as member to group that is currently being edited + */ addMemberToGroup(ePerson: EPerson) { - // TODO - console.log('addMember TODO', ePerson); - // this.forceUpdateEPeople(); + this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => { + if (activeGroup != null) { + this.groupDataService.addMemberToGroup(activeGroup, ePerson); + this.forceUpdateEPeople(activeGroup); + } else { + this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup')); + } + }); } - isMemberOfGroup(ePerson: EPerson): Observable { + /** + * Whether or not the given ePerson is a member of the group currently being edited + * @param possibleMember EPerson that is a possible member (being tested) of the group currently being edited + */ + isMemberOfGroup(possibleMember: EPerson): Observable { return this.groupDataService.getActiveGroup().pipe(take(1), mergeMap((group: Group) => { if (group != null) { - return this.groupDataService.findAllByHref(ePerson._links.groups.href, { + return this.groupDataService.findAllByHref(possibleMember._links.groups.href, { currentPage: 0, elementsPerPage: Number.MAX_SAFE_INTEGER }) @@ -137,10 +162,15 @@ export class MembersListComponent implements OnInit, OnDestroy { /** * Force-update the list of EPeople by first clearing the cache related to EPeople, then performing * a new REST call + * @param activeGroup Group currently being edited */ - public forceUpdateEPeople() { + public forceUpdateEPeople(activeGroup: Group) { + this.groupDataService.clearGroupsRequests(); this.ePersonDataService.clearEPersonRequests(); - this.search({ query: '', scope: 'metadata' }) + this.ePeople = this.ePersonDataService.findAllByHref(activeGroup._links.epersons.href, { + currentPage: 1, + elementsPerPage: this.config.pageSize + }) } /** diff --git a/src/app/+admin/admin-access-control/group-registry/group-form/subgroup-list/subgroups-list.component.ts b/src/app/+admin/admin-access-control/group-registry/group-form/subgroup-list/subgroups-list.component.ts index afa226ef76..deeb1826d6 100644 --- a/src/app/+admin/admin-access-control/group-registry/group-form/subgroup-list/subgroups-list.component.ts +++ b/src/app/+admin/admin-access-control/group-registry/group-form/subgroup-list/subgroups-list.component.ts @@ -25,6 +25,9 @@ export class SubgroupsListComponent implements OnInit, OnDestroy { @Input() messagePrefix: string; + /** + * Groups being displayed, initially all subgroups, after search result of search + */ groups: Observable>>; /** @@ -88,6 +91,10 @@ export class SubgroupsListComponent implements OnInit, OnDestroy { this.groups = this.groupDataService.getGroups(options); } + /** + * 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 + */ isSubgroupOfGroup(possibleSubgroup: Group): Observable { return this.groupDataService.getActiveGroup().pipe(take(1), mergeMap((group: Group) => { @@ -104,19 +111,37 @@ export class SubgroupsListComponent implements OnInit, OnDestroy { } else { return observableOf(false); } - })) + })); } - deleteSubgroupFromGroup(group: Group) { - // TODO - console.log('deleteSubgroup TODO', group); - // this.forceUpdateGroup(); + /** + * Deletes given subgroup from the group currently being edited + * @param subgroup Group we want to delete from the subgroups of the group currently being edited + */ + deleteSubgroupFromGroup(subgroup: Group) { + this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => { + if (activeGroup != null) { + this.groupDataService.deleteSubGroupFromGroup(activeGroup, subgroup); + this.forceUpdateGroups(activeGroup); + } else { + this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup')); + } + }); } - addSubgroupToGroup(group: Group) { - // TODO - console.log('addSubgroup TODO', group); - // this.forceUpdateGroup(); + /** + * Adds given subgroup to the group currently being edited + * @param subgroup Subgroup to add to group currently being edited + */ + addSubgroupToGroup(subgroup: Group) { + this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => { + if (activeGroup != null) { + this.groupDataService.addSubGroupToGroup(activeGroup, subgroup); + this.forceUpdateGroups(activeGroup); + } else { + this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup')); + } + }); } /** @@ -134,10 +159,14 @@ export class SubgroupsListComponent implements OnInit, OnDestroy { /** * Force-update the list of groups by first clearing the cache related to groups, then performing a new REST call + * @param activeGroup Group currently being edited */ - public forceUpdateGroup() { + public forceUpdateGroups(activeGroup: Group) { this.groupDataService.clearGroupsRequests(); - this.search({ query: '' }) + this.groups = this.groupDataService.findAllByHref(activeGroup._links.groups.href, { + currentPage: 1, + elementsPerPage: this.config.pageSize + }) } /** diff --git a/src/app/+admin/admin-access-control/group-registry/groups-registry.component.ts b/src/app/+admin/admin-access-control/group-registry/groups-registry.component.ts index 62912c94fb..e6a8b931ee 100644 --- a/src/app/+admin/admin-access-control/group-registry/groups-registry.component.ts +++ b/src/app/+admin/admin-access-control/group-registry/groups-registry.component.ts @@ -91,6 +91,9 @@ export class GroupsRegistryComponent { * Delete Group */ deleteGroup(group: Group) { + // TODO (backend) + console.log('TODO implement editGroup', group); + this.notificationsService.error('TODO implement deleteGroup (not yet implemented in backend)'); if (hasValue(group.id)) { this.groupService.deleteGroup(group).pipe(take(1)).subscribe((success: boolean) => { if (success) { @@ -112,7 +115,7 @@ export class GroupsRegistryComponent { } /** - * Get the amount of members (epersons embedded value of a group) //TODO + * Get the amount of members (epersons embedded value of a group) * @param group */ getMembers(group: Group): Observable>> { diff --git a/src/app/core/eperson/group-data.service.ts b/src/app/core/eperson/group-data.service.ts index 86fe8a88cd..b6233bf015 100644 --- a/src/app/core/eperson/group-data.service.ts +++ b/src/app/core/eperson/group-data.service.ts @@ -1,4 +1,4 @@ -import { HttpClient } from '@angular/common/http'; +import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { createSelector, select, Store } from '@ngrx/store'; @@ -20,10 +20,12 @@ import { DataService } from '../data/data.service'; import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service'; import { PaginatedList } from '../data/paginated-list'; import { RemoteData } from '../data/remote-data'; -import { FindListOptions, FindListRequest } from '../data/request.models'; +import { DeleteRequest, FindListOptions, FindListRequest, PostRequest } from '../data/request.models'; import { RequestService } from '../data/request.service'; +import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { EPerson } from './models/eperson.model'; import { Group } from './models/group.model'; import { dataService } from '../cache/builders/build-decorators'; import { GROUP } from './models/group.resource-type'; @@ -41,6 +43,8 @@ const editGroupSelector = createSelector(groupRegistryStateSelector, (groupRegis export class GroupDataService extends DataService { protected linkPath = 'groups'; protected browseEndpoint = ''; + protected ePersonsEndpoint = 'epersons'; + protected subgroupsEndpoint = 'subgroups'; constructor( protected comparator: DSOChangeAnalyzer, @@ -132,7 +136,6 @@ export class GroupDataService extends DataService { if (isUpdate) { return this.updateGroup(group); } else { - console.log('group create', group) return this.create(group, null); } } @@ -142,10 +145,62 @@ export class GroupDataService extends DataService { * @param {DSpaceObject} ePerson The given object */ updateGroup(group: Group): Observable> { - // TODO + // TODO return null; } + /** + * Adds given subgroup as a subgroup to the given active group + * @param activeGroup Group we want to add subgroup to + * @param subgroup Group we want to add as subgroup to activeGroup + */ + addSubGroupToGroup(activeGroup: Group, subgroup: Group) { + const requestId = this.requestService.generateRequestId(); + const options: HttpOptions = Object.create({}); + let headers = new HttpHeaders(); + headers = headers.append('Content-Type', 'text/uri-list'); + options.headers = headers; + const postRequest = new PostRequest(requestId, activeGroup.self + '/' + this.subgroupsEndpoint, subgroup.self, options); + this.requestService.configure(postRequest); + } + + /** + * Deletes a given subgroup from the subgroups of the given active group + * @param activeGroup Group we want to delete subgroup from + * @param subgroup Subgroup we want to delete from activeGroup + */ + deleteSubGroupFromGroup(activeGroup: Group, subgroup: Group) { + const requestId = this.requestService.generateRequestId(); + const deleteRequest = new DeleteRequest(requestId, activeGroup.self + '/' + this.subgroupsEndpoint + '/' + subgroup.id); + this.requestService.configure(deleteRequest); + } + + /** + * Adds given ePerson as member to given group + * @param activeGroup Group we want to add member to + * @param ePerson EPerson we want to add as member to given activeGroup + */ + addMemberToGroup(activeGroup: Group, ePerson: EPerson) { + const requestId = this.requestService.generateRequestId(); + const options: HttpOptions = Object.create({}); + let headers = new HttpHeaders(); + headers = headers.append('Content-Type', 'text/uri-list'); + options.headers = headers; + const postRequest = new PostRequest(requestId, activeGroup.self + '/' + this.ePersonsEndpoint, ePerson.self, options); + this.requestService.configure(postRequest); + } + + /** + * Deletes a given ePerson from the members of the given active group + * @param activeGroup Group we want to delete member from + * @param ePerson EPerson we want to delete from members of given activeGroup + */ + deleteMemberFromGroup(activeGroup: Group, ePerson: EPerson) { + const requestId = this.requestService.generateRequestId(); + const deleteRequest = new DeleteRequest(requestId, activeGroup.self + '/' + this.ePersonsEndpoint + '/' + ePerson.id); + this.requestService.configure(deleteRequest); + } + /** * Method to retrieve the group that is currently being edited */ From 6f75ab6237966b18d9b7bd42eb9ea6eb1deb34bb Mon Sep 17 00:00:00 2001 From: Marie Verdonck Date: Wed, 4 Mar 2020 13:34:37 +0100 Subject: [PATCH 06/20] 69111: bug fix isMemberOf, notifications add/delete member/group, group count added to member count, bug fix disappearing group table in registry after leaving edit --- resources/i18n/en.json5 | 16 +++++++++ .../members-list/members-list.component.ts | 30 ++++++++++++++--- .../subgroup-list/subgroups-list.component.ts | 24 ++++++++++++-- .../groups-registry.component.html | 2 +- .../groups-registry.component.ts | 25 ++++++++++---- src/app/core/eperson/group-data.service.ts | 33 ++++++++++++++++--- 6 files changed, 110 insertions(+), 20 deletions(-) diff --git a/resources/i18n/en.json5 b/resources/i18n/en.json5 index 3292ebeb69..5c311715ca 100644 --- a/resources/i18n/en.json5 +++ b/resources/i18n/en.json5 @@ -290,6 +290,14 @@ "admin.access-control.groups.form.members-list.table.edit.buttons.remove": "Remove member with name \"{{name}}\"", + "admin.access-control.groups.form.members-list.notification.success.addMember": "Successfully added member: \"{{name}}\"", + + "admin.access-control.groups.form.members-list.notification.failure.addMember": "Failed to add member: \"{{name}}\"", + + "admin.access-control.groups.form.members-list.notification.success.deleteMember": "Successfully deleted member: \"{{name}}\"", + + "admin.access-control.groups.form.members-list.notification.failure.deleteMember": "Failed to delete member: \"{{name}}\"", + "admin.access-control.groups.form.members-list.table.edit.buttons.add": "Add member with name \"{{name}}\"", "admin.access-control.groups.form.members-list.notification.failure.noActiveGroup": "No current active group, submit a name first.", @@ -316,6 +324,14 @@ "admin.access-control.groups.form.subgroups-list.table.edit.buttons.add": "Add subgroup with name \"{{name}}\"", + "admin.access-control.groups.form.subgroups-list.notification.success.addSubgroup": "Successfully added subgroup: \"{{name}}\"", + + "admin.access-control.groups.form.subgroups-list.notification.failure.addSubgroup": "Failed to add subgroup: \"{{name}}\"", + + "admin.access-control.groups.form.subgroups-list.notification.success.deleteSubgroup": "Successfully deleted subgroup: \"{{name}}\"", + + "admin.access-control.groups.form.subgroups-list.notification.failure.deleteSubgroup": "Failed to delete subgroup: \"{{name}}\"", + "admin.access-control.groups.form.subgroups-list.notification.failure.noActiveGroup": "No current active group, submit a name first.", "admin.access-control.groups.form.subgroups-list.no-items": "No groups found with this in their name or this as UUID", diff --git a/src/app/+admin/admin-access-control/group-registry/group-form/members-list/members-list.component.ts b/src/app/+admin/admin-access-control/group-registry/group-form/members-list/members-list.component.ts index 545e23d0bb..7bb41725b1 100644 --- a/src/app/+admin/admin-access-control/group-registry/group-form/members-list/members-list.component.ts +++ b/src/app/+admin/admin-access-control/group-registry/group-form/members-list/members-list.component.ts @@ -3,6 +3,7 @@ import { FormBuilder } from '@angular/forms'; import { TranslateService } from '@ngx-translate/core'; import { Observable, of as observableOf, Subscription } from 'rxjs'; import { map, mergeMap, take } from 'rxjs/operators'; +import { RestResponse } from '../../../../../core/cache/response.models'; import { PaginatedList } from '../../../../../core/data/paginated-list'; import { RemoteData } from '../../../../../core/data/remote-data'; import { EPersonDataService } from '../../../../../core/eperson/eperson-data.service'; @@ -101,7 +102,8 @@ export class MembersListComponent implements OnInit, OnDestroy { deleteMemberFromGroup(ePerson: EPerson) { this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => { if (activeGroup != null) { - this.groupDataService.deleteMemberFromGroup(activeGroup, ePerson); + const response = this.groupDataService.deleteMemberFromGroup(activeGroup, ePerson); + this.showNotifications('deleteMember', response, ePerson.name, activeGroup); this.forceUpdateEPeople(activeGroup); } else { this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup')); @@ -116,7 +118,8 @@ export class MembersListComponent implements OnInit, OnDestroy { addMemberToGroup(ePerson: EPerson) { this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => { if (activeGroup != null) { - this.groupDataService.addMemberToGroup(activeGroup, ePerson); + const response = this.groupDataService.addMemberToGroup(activeGroup, ePerson); + this.showNotifications('addMember', response, ePerson.name, activeGroup); this.forceUpdateEPeople(activeGroup); } else { this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup')); @@ -132,15 +135,15 @@ export class MembersListComponent implements OnInit, OnDestroy { return this.groupDataService.getActiveGroup().pipe(take(1), mergeMap((group: Group) => { if (group != null) { - return this.groupDataService.findAllByHref(possibleMember._links.groups.href, { + return this.ePersonDataService.findAllByHref(group._links.epersons.href, { currentPage: 0, elementsPerPage: Number.MAX_SAFE_INTEGER }) .pipe( getSucceededRemoteData(), getRemoteDataPayload(), - map((listTotalGroups: PaginatedList) => listTotalGroups.page.filter((groupInList: Group) => groupInList.id === group.id)), - map((groups: Group[]) => groups.length > 0)) + map((listEPeopleInGroup: PaginatedList) => listEPeopleInGroup.page.filter((ePersonInList: EPerson) => ePersonInList.id === possibleMember.id)), + map((epeople: EPerson[]) => epeople.length > 0)) } else { return observableOf(false); } @@ -179,4 +182,21 @@ export class MembersListComponent implements OnInit, OnDestroy { ngOnDestroy(): void { this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe()); } + + /** + * Shows a notification based on the success/failure of the request + * @param messageSuffix Suffix for message + * @param response RestResponse observable containing success/failure request + * @param nameObject Object request was about + * @param activeGroup Group currently being edited + */ + showNotifications(messageSuffix: string, response: Observable, nameObject: string, activeGroup: Group) { + response.pipe(take(1)).subscribe((restResponse: RestResponse) => { + if (restResponse.isSuccessful) { + this.notificationsService.success(this.translateService.get(this.messagePrefix + '.notification.success.' + messageSuffix, { name: nameObject })); + } else { + this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.' + messageSuffix, { name: nameObject })); + } + }) + } } diff --git a/src/app/+admin/admin-access-control/group-registry/group-form/subgroup-list/subgroups-list.component.ts b/src/app/+admin/admin-access-control/group-registry/group-form/subgroup-list/subgroups-list.component.ts index deeb1826d6..890b95bfeb 100644 --- a/src/app/+admin/admin-access-control/group-registry/group-form/subgroup-list/subgroups-list.component.ts +++ b/src/app/+admin/admin-access-control/group-registry/group-form/subgroup-list/subgroups-list.component.ts @@ -3,6 +3,7 @@ import { FormBuilder } from '@angular/forms'; import { TranslateService } from '@ngx-translate/core'; import { Observable, of as observableOf, Subscription } from 'rxjs'; import { map, mergeMap, take } from 'rxjs/operators'; +import { RestResponse } from '../../../../../core/cache/response.models'; import { PaginatedList } from '../../../../../core/data/paginated-list'; import { RemoteData } from '../../../../../core/data/remote-data'; import { GroupDataService } from '../../../../../core/eperson/group-data.service'; @@ -121,7 +122,8 @@ export class SubgroupsListComponent implements OnInit, OnDestroy { deleteSubgroupFromGroup(subgroup: Group) { this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => { if (activeGroup != null) { - this.groupDataService.deleteSubGroupFromGroup(activeGroup, subgroup); + const response = this.groupDataService.deleteSubGroupFromGroup(activeGroup, subgroup); + this.showNotifications('addSubgroup', response, subgroup.name, activeGroup); this.forceUpdateGroups(activeGroup); } else { this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup')); @@ -136,7 +138,8 @@ export class SubgroupsListComponent implements OnInit, OnDestroy { addSubgroupToGroup(subgroup: Group) { this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => { if (activeGroup != null) { - this.groupDataService.addSubGroupToGroup(activeGroup, subgroup); + const response = this.groupDataService.addSubGroupToGroup(activeGroup, subgroup); + this.showNotifications('deleteSubgroup', response, subgroup.name, activeGroup); this.forceUpdateGroups(activeGroup); } else { this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup')); @@ -175,4 +178,21 @@ export class SubgroupsListComponent implements OnInit, OnDestroy { ngOnDestroy(): void { this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe()); } + + /** + * Shows a notification based on the success/failure of the request + * @param messageSuffix Suffix for message + * @param response RestResponse observable containing success/failure request + * @param nameObject Object request was about + * @param activeGroup Group currently being edited + */ + showNotifications(messageSuffix: string, response: Observable, nameObject: string, activeGroup: Group) { + response.pipe(take(1)).subscribe((restResponse: RestResponse) => { + if (restResponse.isSuccessful) { + this.notificationsService.success(this.translateService.get(this.messagePrefix + '.notification.success.' + messageSuffix, { name: nameObject })); + } else { + this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.' + messageSuffix, { name: nameObject })); + } + }) + } } diff --git a/src/app/+admin/admin-access-control/group-registry/groups-registry.component.html b/src/app/+admin/admin-access-control/group-registry/groups-registry.component.html index f9883bfd5b..d54ab7f2c7 100644 --- a/src/app/+admin/admin-access-control/group-registry/groups-registry.component.html +++ b/src/app/+admin/admin-access-control/group-registry/groups-registry.component.html @@ -50,7 +50,7 @@ {{group.id}} {{group.name}} - {{(getMembers(group) | async)?.payload?.totalElements}} + {{(getMembers(group) | async)?.payload?.totalElements + (getSubgroups(group) | async)?.payload?.totalElements}}
diff --git a/src/app/+admin/admin-access-control/group-registry/groups-registry.component.ts b/src/app/+admin/admin-access-control/group-registry/groups-registry.component.ts index e6a8b931ee..ef86a914e1 100644 --- a/src/app/+admin/admin-access-control/group-registry/groups-registry.component.ts +++ b/src/app/+admin/admin-access-control/group-registry/groups-registry.component.ts @@ -1,4 +1,4 @@ -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { FormBuilder } from '@angular/forms'; import { TranslateService } from '@ngx-translate/core'; import { Observable } from 'rxjs'; @@ -22,7 +22,7 @@ import { followLink } from '../../../shared/utils/follow-link-config.model'; * A component used for managing all existing groups within the repository. * The admin can create, edit or delete groups here. */ -export class GroupsRegistryComponent { +export class GroupsRegistryComponent implements OnInit { messagePrefix = 'admin.access-control.groups.'; @@ -48,13 +48,16 @@ export class GroupsRegistryComponent { private translateService: TranslateService, private notificationsService: NotificationsService, private formBuilder: FormBuilder) { + this.searchForm = this.formBuilder.group(({ + query: '', + })); + } + + ngOnInit() { this.updateGroups({ currentPage: 1, elementsPerPage: this.config.pageSize }); - this.searchForm = this.formBuilder.group(({ - query: '', - })); } /** @@ -72,7 +75,7 @@ export class GroupsRegistryComponent { * 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')); + this.groups = this.groupService.getGroups(options, followLink('epersons'), followLink('groups')); } /** @@ -115,13 +118,21 @@ export class GroupsRegistryComponent { } /** - * Get the amount of members (epersons embedded value of a group) + * Get the members (epersons embedded value of a group) * @param group */ getMembers(group: Group): Observable>> { return this.ePersonDataService.findAllByHref(group._links.epersons.href); } + /** + * Get the subgroups (groups embedded value of a group) + * @param group + */ + getSubgroups(group: Group): Observable>> { + return this.groupService.findAllByHref(group._links.groups.href); + } + /** * 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 diff --git a/src/app/core/eperson/group-data.service.ts b/src/app/core/eperson/group-data.service.ts index b6233bf015..dc331d0b2c 100644 --- a/src/app/core/eperson/group-data.service.ts +++ b/src/app/core/eperson/group-data.service.ts @@ -16,15 +16,17 @@ import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { SearchParam } from '../cache/models/search-param.model'; import { ObjectCacheService } from '../cache/object-cache.service'; +import { RestResponse } from '../cache/response.models'; import { DataService } from '../data/data.service'; import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service'; import { PaginatedList } from '../data/paginated-list'; import { RemoteData } from '../data/remote-data'; -import { DeleteRequest, FindListOptions, FindListRequest, PostRequest } from '../data/request.models'; +import { DeleteRequest, FindListOptions, FindListRequest, PostRequest, RestRequest } from '../data/request.models'; import { RequestService } from '../data/request.service'; import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { getResponseFromEntry } from '../shared/operators'; import { EPerson } from './models/eperson.model'; import { Group } from './models/group.model'; import { dataService } from '../cache/builders/build-decorators'; @@ -154,7 +156,7 @@ export class GroupDataService extends DataService { * @param activeGroup Group we want to add subgroup to * @param subgroup Group we want to add as subgroup to activeGroup */ - addSubGroupToGroup(activeGroup: Group, subgroup: Group) { + addSubGroupToGroup(activeGroup: Group, subgroup: Group): Observable { const requestId = this.requestService.generateRequestId(); const options: HttpOptions = Object.create({}); let headers = new HttpHeaders(); @@ -162,6 +164,8 @@ export class GroupDataService extends DataService { options.headers = headers; const postRequest = new PostRequest(requestId, activeGroup.self + '/' + this.subgroupsEndpoint, subgroup.self, options); this.requestService.configure(postRequest); + + return this.fetchResponse(requestId); } /** @@ -169,10 +173,12 @@ export class GroupDataService extends DataService { * @param activeGroup Group we want to delete subgroup from * @param subgroup Subgroup we want to delete from activeGroup */ - deleteSubGroupFromGroup(activeGroup: Group, subgroup: Group) { + deleteSubGroupFromGroup(activeGroup: Group, subgroup: Group): Observable { const requestId = this.requestService.generateRequestId(); const deleteRequest = new DeleteRequest(requestId, activeGroup.self + '/' + this.subgroupsEndpoint + '/' + subgroup.id); this.requestService.configure(deleteRequest); + + return this.fetchResponse(requestId); } /** @@ -180,7 +186,7 @@ export class GroupDataService extends DataService { * @param activeGroup Group we want to add member to * @param ePerson EPerson we want to add as member to given activeGroup */ - addMemberToGroup(activeGroup: Group, ePerson: EPerson) { + addMemberToGroup(activeGroup: Group, ePerson: EPerson): Observable { const requestId = this.requestService.generateRequestId(); const options: HttpOptions = Object.create({}); let headers = new HttpHeaders(); @@ -188,6 +194,8 @@ export class GroupDataService extends DataService { options.headers = headers; const postRequest = new PostRequest(requestId, activeGroup.self + '/' + this.ePersonsEndpoint, ePerson.self, options); this.requestService.configure(postRequest); + + return this.fetchResponse(requestId); } /** @@ -195,10 +203,25 @@ export class GroupDataService extends DataService { * @param activeGroup Group we want to delete member from * @param ePerson EPerson we want to delete from members of given activeGroup */ - deleteMemberFromGroup(activeGroup: Group, ePerson: EPerson) { + deleteMemberFromGroup(activeGroup: Group, ePerson: EPerson): Observable { const requestId = this.requestService.generateRequestId(); const deleteRequest = new DeleteRequest(requestId, activeGroup.self + '/' + this.ePersonsEndpoint + '/' + ePerson.id); this.requestService.configure(deleteRequest); + + return this.fetchResponse(requestId); + } + + /** + * Gets the restResponse from the requestService + * @param requestId + */ + protected fetchResponse(requestId: string): Observable { + return this.requestService.getByUUID(requestId).pipe( + getResponseFromEntry(), + map((response: RestResponse) => { + return response; + }) + ); } /** From 2ba1106e0a393021fba8a1c17b24eea7b0d0844d Mon Sep 17 00:00:00 2001 From: Marie Verdonck Date: Wed, 4 Mar 2020 17:39:00 +0100 Subject: [PATCH 07/20] Small notification message fix --- .../group-form/subgroup-list/subgroups-list.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/+admin/admin-access-control/group-registry/group-form/subgroup-list/subgroups-list.component.ts b/src/app/+admin/admin-access-control/group-registry/group-form/subgroup-list/subgroups-list.component.ts index 890b95bfeb..65b0f6a6d0 100644 --- a/src/app/+admin/admin-access-control/group-registry/group-form/subgroup-list/subgroups-list.component.ts +++ b/src/app/+admin/admin-access-control/group-registry/group-form/subgroup-list/subgroups-list.component.ts @@ -123,7 +123,7 @@ export class SubgroupsListComponent implements OnInit, OnDestroy { this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => { if (activeGroup != null) { const response = this.groupDataService.deleteSubGroupFromGroup(activeGroup, subgroup); - this.showNotifications('addSubgroup', response, subgroup.name, activeGroup); + this.showNotifications('deleteSubgroup', response, subgroup.name, activeGroup); this.forceUpdateGroups(activeGroup); } else { this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup')); @@ -139,7 +139,7 @@ export class SubgroupsListComponent implements OnInit, OnDestroy { this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => { if (activeGroup != null) { const response = this.groupDataService.addSubGroupToGroup(activeGroup, subgroup); - this.showNotifications('deleteSubgroup', response, subgroup.name, activeGroup); + this.showNotifications('addSubgroup', response, subgroup.name, activeGroup); this.forceUpdateGroups(activeGroup); } else { this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup')); From a8e0bd28fda24765f0dd216d78958d7a58c73c0c Mon Sep 17 00:00:00 2001 From: Marie Verdonck Date: Wed, 26 Feb 2020 19:20:14 +0100 Subject: [PATCH 08/20] 69110: EPeople admin page - CRUD & search on name, email, md --- resources/i18n/en.json5 | 3 +++ src/app/+admin/admin-routing.module.ts | 2 +- src/app/core/cache/object-cache.service.ts | 2 ++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/resources/i18n/en.json5 b/resources/i18n/en.json5 index 5c311715ca..975e8ec4e6 100644 --- a/resources/i18n/en.json5 +++ b/resources/i18n/en.json5 @@ -2274,6 +2274,9 @@ "administrativeView.search.results.head": "Administrative Search", + "menu.section.admin_search": "Admin Search", + + "uploader.browse": "browse", diff --git a/src/app/+admin/admin-routing.module.ts b/src/app/+admin/admin-routing.module.ts index 3380306217..285ebeb0d1 100644 --- a/src/app/+admin/admin-routing.module.ts +++ b/src/app/+admin/admin-routing.module.ts @@ -1,9 +1,9 @@ import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; import { getAdminModulePath } from '../app-routing.module'; -import { URLCombiner } from '../core/url-combiner/url-combiner'; import { AdminSearchPageComponent } from './admin-search-page/admin-search-page.component'; import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; +import { URLCombiner } from '../core/url-combiner/url-combiner'; const REGISTRIES_MODULE_PATH = 'registries'; const ACCESS_CONTROL_MODULE_PATH = 'access-control'; diff --git a/src/app/core/cache/object-cache.service.ts b/src/app/core/cache/object-cache.service.ts index d82a1f31fe..036c9f0ed8 100644 --- a/src/app/core/cache/object-cache.service.ts +++ b/src/app/core/cache/object-cache.service.ts @@ -276,6 +276,8 @@ export class ObjectCacheService { * list of operations to perform */ 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 AddToSSBAction(selfLink, RestRequestMethod.PATCH)); } From b9401b46b120556a8cea7f88ed0d6c17df1623c4 Mon Sep 17 00:00:00 2001 From: Marie Verdonck Date: Thu, 5 Mar 2020 17:19:15 +0100 Subject: [PATCH 09/20] 69111: Added list of groups that EPerson is member of in Edit EPerson form, linking to edit group page --- resources/i18n/en.json5 | 10 ++++ .../eperson-form/eperson-form.component.html | 40 ++++++++++++++++ .../eperson-form/eperson-form.component.ts | 47 ++++++++++++++++++- .../group-form/group-form.component.html | 2 +- .../group-form/group-form.component.ts | 2 +- src/app/core/eperson/group-data.service.ts | 8 ++++ 6 files changed, 105 insertions(+), 4 deletions(-) diff --git a/resources/i18n/en.json5 b/resources/i18n/en.json5 index 975e8ec4e6..72a9530b54 100644 --- a/resources/i18n/en.json5 +++ b/resources/i18n/en.json5 @@ -226,6 +226,16 @@ "admin.access-control.epeople.form.notification.edited.failure": "Failed to edit EPerson \"{{name}}\"", + "admin.access-control.epeople.form.groupsEPersonIsMemberOf": "Member of these groups:", + + "admin.access-control.epeople.form.table.id": "ID", + + "admin.access-control.epeople.form.table.name": "Name", + + "admin.access-control.epeople.form.memberOfNoGroups": "This EPerson is not a member of any groups", + + "admin.access-control.epeople.form.goToGroups": "Add to groups", + "admin.access-control.epeople.notification.deleted.failure": "Failed to delete EPerson: \"{{name}}\"", "admin.access-control.epeople.notification.deleted.success": "Successfully deleted EPerson: \"{{name}}\"", diff --git a/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.html b/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.html index 6d44dd7fa1..5ab10c94a3 100644 --- a/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.html +++ b/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.html @@ -15,3 +15,43 @@ (cancel)="onCancel()" (submitForm)="onSubmit()"> + +
+
{{messagePrefix + '.groupsEPersonIsMemberOf' | translate}}
+ + + +
+ + + + + + + + + + + + + +
{{messagePrefix + '.table.id' | translate}}{{messagePrefix + '.table.name' | translate}}
{{group.id}}{{group.name}}
+
+ +
+ + +
diff --git a/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.ts b/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.ts index c6cae75ab0..d06bafb3a8 100644 --- a/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.ts +++ b/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.ts @@ -7,17 +7,21 @@ import { DynamicInputModel } from '@ng-dynamic-forms/core'; import { TranslateService } from '@ngx-translate/core'; -import { combineLatest } from 'rxjs/internal/observable/combineLatest'; -import { Subscription } from 'rxjs/internal/Subscription'; +import { Subscription, combineLatest } from 'rxjs'; +import { Observable } from 'rxjs/internal/Observable'; import { take } from 'rxjs/operators'; import { RestResponse } from '../../../../core/cache/response.models'; import { PaginatedList } from '../../../../core/data/paginated-list'; +import { RemoteData } from '../../../../core/data/remote-data'; import { EPersonDataService } from '../../../../core/eperson/eperson-data.service'; +import { GroupDataService } from '../../../../core/eperson/group-data.service'; import { EPerson } from '../../../../core/eperson/models/eperson.model'; +import { Group } from '../../../../core/eperson/models/group.model'; import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators'; import { hasValue } from '../../../../shared/empty.util'; import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service'; import { NotificationsService } from '../../../../shared/notifications/notifications.service'; +import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model'; @Component({ selector: 'ds-eperson-form', @@ -106,12 +110,27 @@ export class EPersonFormComponent implements OnInit, OnDestroy { */ subs: Subscription[] = []; + /** + * A list of all the groups this EPerson is a member of + */ + groups: Observable>>; + + /** + * Pagination config used to display the list of groups + */ + config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { + id: 'groups-ePersonMemberOf-list-pagination', + pageSize: 5, + currentPage: 1 + }); + /** * Try to retrieve initial active eperson, to fill in checkboxes at component creation */ epersonInitial: EPerson; constructor(public epersonService: EPersonDataService, + public groupsDataService: GroupDataService, private formBuilderService: FormBuilderService, private translateService: TranslateService, private notificationsService: NotificationsService,) { @@ -181,6 +200,10 @@ export class EPersonFormComponent implements OnInit, OnDestroy { ]; this.formGroup = this.formBuilderService.createFormGroup(this.formModel); this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => { + this.groups = this.groupsDataService.findAllByHref(eperson._links.groups.href, { + currentPage: 1, + elementsPerPage: this.config.pageSize + }); this.formGroup.patchValue({ firstName: eperson != null ? eperson.firstMetadataValue('eperson.firstname') : '', lastName: eperson != null ? eperson.firstMetadataValue('eperson.lastname') : '', @@ -333,6 +356,26 @@ export class EPersonFormComponent implements OnInit, OnDestroy { }); } + /** + * Event triggered when the user changes page + * @param event + */ + onPageChange(event) { + this.updateGroups({ + currentPage: event, + elementsPerPage: this.config.pageSize + }); + } + + /** + * Update the list of groups by fetching it from the rest api or cache + */ + private updateGroups(options) { + this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => { + this.groups = this.groupsDataService.findAllByHref(eperson._links.groups.href, options); + })); + } + /** * Cancel the current edit when component is destroyed & unsub all subscriptions */ diff --git a/src/app/+admin/admin-access-control/group-registry/group-form/group-form.component.html b/src/app/+admin/admin-access-control/group-registry/group-form/group-form.component.html index 47c61cb024..0cf0c6dd0b 100644 --- a/src/app/+admin/admin-access-control/group-registry/group-form/group-form.component.html +++ b/src/app/+admin/admin-access-control/group-registry/group-form/group-form.component.html @@ -24,7 +24,7 @@
- +
diff --git a/src/app/+admin/admin-access-control/group-registry/group-form/group-form.component.ts b/src/app/+admin/admin-access-control/group-registry/group-form/group-form.component.ts index eb75a1d024..415a0bb12e 100644 --- a/src/app/+admin/admin-access-control/group-registry/group-form/group-form.component.ts +++ b/src/app/+admin/admin-access-control/group-registry/group-form/group-form.component.ts @@ -134,7 +134,7 @@ export class GroupFormComponent implements OnInit, OnDestroy { onCancel() { this.groupDataService.cancelEditGroup(); this.cancelForm.emit(); - this.router.navigate(['/admin/access-control/groups']); + this.router.navigate([this.groupDataService.getGroupRegistryRouterLink()]); } /** diff --git a/src/app/core/eperson/group-data.service.ts b/src/app/core/eperson/group-data.service.ts index dc331d0b2c..17c9829483 100644 --- a/src/app/core/eperson/group-data.service.ts +++ b/src/app/core/eperson/group-data.service.ts @@ -255,4 +255,12 @@ export class GroupDataService extends DataService { }); } + public getGroupRegistryRouterLink(): string { + return '/admin/access-control/groups'; + } + + public getGroupEditPageRouterLink(groupId: string): string { + return '/admin/access-control/groups/' + groupId; + } + } From 5b76ed6d529cd168e89fefa30b6b2c881bd017e0 Mon Sep 17 00:00:00 2001 From: Marie Verdonck Date: Fri, 6 Mar 2020 17:15:38 +0100 Subject: [PATCH 10/20] 69111: Links to edit EPerson page and edit Group page from members list in edit group page --- resources/i18n/en.json5 | 9 +++---- .../epeople-registry.component.ts | 27 ++++++++++++++++--- .../eperson-form/eperson-form.component.html | 3 ++- .../group-form/group-form.component.ts | 1 - .../members-list/members-list.component.html | 24 ++++++++++------- .../members-list/members-list.component.ts | 2 +- .../subgroups-list.component.html | 24 ++++++++++------- .../groups-registry.component.html | 2 +- src/app/core/eperson/eperson-data.service.ts | 23 ++++++++++++++++ src/app/core/eperson/group-data.service.ts | 23 ++++++++++++++-- 10 files changed, 104 insertions(+), 34 deletions(-) diff --git a/resources/i18n/en.json5 b/resources/i18n/en.json5 index 72a9530b54..c955ef737c 100644 --- a/resources/i18n/en.json5 +++ b/resources/i18n/en.json5 @@ -192,9 +192,9 @@ "admin.access-control.epeople.table.edit": "Edit", - "item.access-control.epeople.table.edit.buttons.edit": "Edit", + "admin.access-control.epeople.table.edit.buttons.edit": "Edit", - "item.access-control.epeople.table.edit.buttons.remove": "Remove", + "admin.access-control.epeople.table.edit.buttons.remove": "Remove", "admin.access-control.epeople.no-items": "No EPeople to show.", @@ -241,6 +241,7 @@ "admin.access-control.epeople.notification.deleted.success": "Successfully deleted EPerson: \"{{name}}\"", + "admin.access-control.groups.title": "DSpace Angular :: Groups", "admin.access-control.groups.head": "Groups", @@ -276,8 +277,6 @@ "admin.access-control.groups.form.groupDescription": "Description", - "admin.access-control.groups.form.button.return": "Return", - "admin.access-control.groups.form.notification.created.success": "Successfully created group \"{{name}}\"", "admin.access-control.groups.form.notification.created.failure": "Failed to create group \"{{name}}\"", @@ -350,7 +349,7 @@ "admin.access-control.groups.form.subgroups-list.button.see-all": "Search all", - "admin.access-control.groups.form.return": "Return", + "admin.access-control.groups.form.return": "Return to groups", diff --git a/src/app/+admin/admin-access-control/epeople-registry/epeople-registry.component.ts b/src/app/+admin/admin-access-control/epeople-registry/epeople-registry.component.ts index 4fd663a6b8..591fe272d1 100644 --- a/src/app/+admin/admin-access-control/epeople-registry/epeople-registry.component.ts +++ b/src/app/+admin/admin-access-control/epeople-registry/epeople-registry.component.ts @@ -1,7 +1,8 @@ -import { Component } from '@angular/core'; +import { Component, OnDestroy, OnInit } from '@angular/core'; import { FormBuilder } from '@angular/forms'; import { TranslateService } from '@ngx-translate/core'; import { Observable } from 'rxjs'; +import { Subscription } from 'rxjs/internal/Subscription'; import { map, take } from 'rxjs/operators'; import { PaginatedList } from '../../../core/data/paginated-list'; import { RemoteData } from '../../../core/data/remote-data'; @@ -19,7 +20,7 @@ import { PaginationComponentOptions } from '../../../shared/pagination/paginatio * A component used for managing all existing epeople within the repository. * The admin can create, edit or delete epeople here. */ -export class EPeopleRegistryComponent { +export class EPeopleRegistryComponent implements OnInit, OnDestroy { labelPrefix = 'admin.access-control.epeople.'; @@ -45,15 +46,28 @@ export class EPeopleRegistryComponent { // The search form searchForm; + /** + * List of subscriptions + */ + subs: Subscription[] = []; + constructor(private epersonService: EPersonDataService, private translateService: TranslateService, private notificationsService: NotificationsService, private formBuilder: FormBuilder) { + } + + ngOnInit() { + this.isEPersonFormShown = false; this.updateEPeople({ currentPage: 1, elementsPerPage: this.config.pageSize }); - this.isEPersonFormShown = false; + this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => { + if (eperson != null && eperson.id) { + this.isEPersonFormShown = true; + } + })); this.searchForm = this.formBuilder.group(({ scope: 'metadata', query: '', @@ -151,6 +165,13 @@ export class EPeopleRegistryComponent { } } + /** + * Unsub all subscriptions + */ + ngOnDestroy(): void { + this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe()); + } + scrollToTop() { (function smoothscroll() { const currentScroll = document.documentElement.scrollTop || document.body.scrollTop; diff --git a/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.html b/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.html index 5ab10c94a3..578862b561 100644 --- a/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.html +++ b/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.html @@ -39,7 +39,8 @@ {{group.id}} - {{group.name}} + {{group.name}} diff --git a/src/app/+admin/admin-access-control/group-registry/group-form/group-form.component.ts b/src/app/+admin/admin-access-control/group-registry/group-form/group-form.component.ts index 415a0bb12e..0b8889fb28 100644 --- a/src/app/+admin/admin-access-control/group-registry/group-form/group-form.component.ts +++ b/src/app/+admin/admin-access-control/group-registry/group-form/group-form.component.ts @@ -134,7 +134,6 @@ export class GroupFormComponent implements OnInit, OnDestroy { onCancel() { this.groupDataService.cancelEditGroup(); this.cancelForm.emit(); - this.router.navigate([this.groupDataService.getGroupRegistryRouterLink()]); } /** diff --git a/src/app/+admin/admin-access-control/group-registry/group-form/members-list/members-list.component.html b/src/app/+admin/admin-access-control/group-registry/group-form/members-list/members-list.component.html index 5fb30ecf82..2025419e52 100644 --- a/src/app/+admin/admin-access-control/group-registry/group-form/members-list/members-list.component.html +++ b/src/app/+admin/admin-access-control/group-registry/group-form/members-list/members-list.component.html @@ -22,12 +22,12 @@ + [paginationOptions]="config" + [pageInfoState]="(ePeople | async)?.payload" + [collectionSize]="(ePeople | async)?.payload?.totalElements" + [hideGear]="true" + [hidePagerWhenSinglePage]="true" + (pageChange)="onPageChange($event)">
@@ -41,7 +41,8 @@ - +
{{ePerson.id}}{{ePerson.name}}{{ePerson.name}}
+
-
- -