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>>; + }