diff --git a/resources/i18n/en.json5 b/resources/i18n/en.json5 index 01a65d9105..ae3176d8b1 100644 --- a/resources/i18n/en.json5 +++ b/resources/i18n/en.json5 @@ -176,12 +176,12 @@ "admin.access-control.epeople.search.head": "Search", - "admin.access-control.epeople.search.scope.name": "Name", - - "admin.access-control.epeople.search.scope.email": "E-mail (exact)", + "admin.access-control.epeople.button.see-all": "Browse All", "admin.access-control.epeople.search.scope.metadata": "Metadata", + "admin.access-control.epeople.search.scope.email": "E-mail (exact)", + "admin.access-control.epeople.search.button": "Search", "admin.access-control.epeople.button.add": "Add EPerson", @@ -190,13 +190,13 @@ "admin.access-control.epeople.table.name": "Name", - "admin.access-control.epeople.table.email": "E-mail", + "admin.access-control.epeople.table.email": "E-mail (exact)", "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 \"{{name}}\"", - "item.access-control.epeople.table.edit.buttons.remove": "Remove", + "admin.access-control.epeople.table.edit.buttons.remove": "Delete \"{{name}}\"", "admin.access-control.epeople.no-items": "No EPeople to show.", @@ -228,12 +228,149 @@ "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}}\"", + "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.button.see-all": "Browse all", + + "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.table.edit.buttons.edit": "Edit \"{{name}}\"", + + "admin.access-control.groups.table.edit.buttons.remove": "Delete \"{{name}}\"", + + "admin.access-control.groups.no-items": "No groups found with this in their name or this as UUID", + + "admin.access-control.groups.notification.deleted.success": "Successfully deleted group \"{{name}}\"", + + "admin.access-control.groups.notification.deleted.failure": "Failed to delete group \"{{name}}\"", + + + "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.groupDescription": "Description", + + "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.notification.created.failure.groupNameInUse": "Failed to create Group with name: \"{{name}}\", make sure the name is not already in use.", + + "admin.access-control.groups.form.members-list.head": "EPeople", + + "admin.access-control.groups.form.members-list.search.head": "Add EPeople", + + "admin.access-control.groups.form.members-list.button.see-all": "Browse All", + + "admin.access-control.groups.form.members-list.headMembers": "Current Members", + + "admin.access-control.groups.form.members-list.search.scope.metadata": "Metadata", + + "admin.access-control.groups.form.members-list.search.scope.email": "E-mail (exact)", + + "admin.access-control.groups.form.members-list.search.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.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.", + + "admin.access-control.groups.form.members-list.no-members-yet": "No members in group yet, search and add.", + + "admin.access-control.groups.form.members-list.no-items": "No EPeople found in that search", + + "admin.access-control.groups.form.subgroups-list.head": "Groups", + + "admin.access-control.groups.form.subgroups-list.search.head": "Add Subgroup", + + "admin.access-control.groups.form.subgroups-list.button.see-all": "Browse All", + + "admin.access-control.groups.form.subgroups-list.headSubgroups": "Current Subgroups", + + "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.table.edit.currentGroup": "Current group", + + "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.notification.failure.subgroupToAddIsActiveGroup": "This is the current group, can't be added.", + + "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.", + + "admin.access-control.groups.form.return": "Return to groups", + + + "admin.search.breadcrumbs": "Administrative Search", "admin.search.collection.edit": "Edit", @@ -2326,6 +2463,9 @@ "administrativeView.search.results.head": "Administrative Search", + "menu.section.admin_search": "Admin Search", + + "uploader.browse": "browse", 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.html b/src/app/+admin/admin-access-control/epeople-registry/epeople-registry.component.html index dd1e8bb62c..20593756c1 100644 --- a/src/app/+admin/admin-access-control/epeople-registry/epeople-registry.component.html +++ b/src/app/+admin/admin-access-control/epeople-registry/epeople-registry.component.html @@ -15,7 +15,10 @@ - +
+ + + +
+
+
+ + + + +
+
+
+ + + +
+ + + + + + + + + + + + + + + +
{{messagePrefix + '.table.id' | translate}}{{messagePrefix + '.table.name' | translate}}{{messagePrefix + '.table.edit' | translate}}
{{ePerson.id}}{{ePerson.name}} +
+ + + +
+
+
+ +
+ + + +

{{messagePrefix + '.headMembers' | 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.spec.ts b/src/app/+admin/admin-access-control/group-registry/group-form/members-list/members-list.component.spec.ts new file mode 100644 index 0000000000..07fab49ca5 --- /dev/null +++ b/src/app/+admin/admin-access-control/group-registry/group-form/members-list/members-list.component.spec.ts @@ -0,0 +1,241 @@ +import { CommonModule } from '@angular/common'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { async, ComponentFixture, fakeAsync, flush, inject, TestBed, tick } from '@angular/core/testing'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { BrowserModule, By } from '@angular/platform-browser'; +import { Router } from '@angular/router'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core'; +import { Observable } from 'rxjs/internal/Observable'; +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 { PageInfo } from '../../../../../core/shared/page-info.model'; +import { FormBuilderService } from '../../../../../shared/form/builder/form-builder.service'; +import { getMockFormBuilderService } from '../../../../../shared/mocks/mock-form-builder-service'; +import { MockRouter } from '../../../../../shared/mocks/mock-router'; +import { getMockTranslateService } from '../../../../../shared/mocks/mock-translate.service'; +import { NotificationsService } from '../../../../../shared/notifications/notifications.service'; +import { EPersonMock, EPersonMock2 } from '../../../../../shared/testing/eperson-mock'; +import { GroupMock, GroupMock2 } from '../../../../../shared/testing/group-mock'; +import { MockTranslateLoader } from '../../../../../shared/testing/mock-translate-loader'; +import { NotificationsServiceStub } from '../../../../../shared/testing/notifications-service-stub'; +import { of as observableOf } from 'rxjs'; +import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/testing/utils'; +import { MembersListComponent } from './members-list.component'; + +describe('MembersListComponent', () => { + let component: MembersListComponent; + let fixture: ComponentFixture; + let translateService: TranslateService; + let builderService: FormBuilderService; + let ePersonDataServiceStub: any; + let groupsDataServiceStub: any; + let activeGroup; + let allEPersons; + let allGroups; + let epersonMembers; + let subgroupMembers; + + beforeEach(async(() => { + activeGroup = GroupMock; + epersonMembers = [EPersonMock2]; + subgroupMembers = [GroupMock2]; + allEPersons = [EPersonMock, EPersonMock2]; + allGroups = [GroupMock, GroupMock2]; + ePersonDataServiceStub = { + activeGroup: activeGroup, + epersonMembers: epersonMembers, + subgroupMembers: subgroupMembers, + findAllByHref(href: string): Observable>> { + return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), groupsDataServiceStub.getEPersonMembers())) + }, + searchByScope(scope: string, query: string): Observable>> { + if (query === '') { + return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), allEPersons)) + } + return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])) + }, + clearEPersonRequests() { + // empty + }, + clearLinkRequests() { + // empty + }, + getEPeoplePageRouterLink(): string { + return '/admin/access-control/epeople'; + } + }; + groupsDataServiceStub = { + activeGroup: activeGroup, + epersonMembers: epersonMembers, + subgroupMembers: subgroupMembers, + allGroups: allGroups, + getActiveGroup(): Observable { + return observableOf(activeGroup); + }, + getEPersonMembers() { + return this.epersonMembers; + }, + searchGroups(query: string): Observable>> { + if (query === '') { + return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), this.allGroups)) + } + return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])) + }, + addMemberToGroup(parentGroup, eperson: EPerson): Observable { + this.epersonMembers = [...this.epersonMembers, eperson]; + return observableOf(new RestResponse(true, 200, 'Success')); + }, + clearGroupsRequests() { + // empty + }, + clearGroupLinkRequests() { + // empty + }, + getGroupEditPageRouterLink(group: Group): string { + return '/admin/access-control/groups/' + group.id; + }, + deleteMemberFromGroup(parentGroup, epersonToDelete: EPerson): Observable { + this.epersonMembers = this.epersonMembers.find((eperson: EPerson) => { + if (eperson.id !== epersonToDelete.id) { + return eperson; + } + }); + if (this.epersonMembers === undefined) { + this.epersonMembers = [] + } + return observableOf(new RestResponse(true, 200, 'Success')); + } + }; + builderService = getMockFormBuilderService(); + translateService = getMockTranslateService(); + TestBed.configureTestingModule({ + imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: MockTranslateLoader + } + }), + ], + declarations: [MembersListComponent], + providers: [MembersListComponent, + { provide: EPersonDataService, useValue: ePersonDataServiceStub }, + { provide: GroupDataService, useValue: groupsDataServiceStub }, + { provide: NotificationsService, useValue: new NotificationsServiceStub() }, + { provide: FormBuilderService, useValue: builderService }, + { provide: Router, useValue: new MockRouter() }, + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(MembersListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + afterEach(fakeAsync(() => { + fixture.destroy(); + flush(); + component = null; + })); + + it('should create MembersListComponent', inject([MembersListComponent], (comp: MembersListComponent) => { + expect(comp).toBeDefined(); + })); + + it('should show list of eperson members of current active group', () => { + const epersonIdsFound = fixture.debugElement.queryAll(By.css('#ePeopleMembersOfGroup tr td:first-child')); + expect(epersonIdsFound.length).toEqual(1); + epersonMembers.map((eperson: EPerson) => { + expect(epersonIdsFound.find((foundEl) => { + return (foundEl.nativeElement.textContent.trim() === eperson.uuid); + })).toBeTruthy(); + }); + }); + + describe('search', () => { + describe('when searching without query', () => { + let epersonsFound; + beforeEach(fakeAsync(() => { + component.search({ scope: 'metadata', query: '' }); + tick(); + fixture.detectChanges(); + epersonsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr')); + })); + + it('should display all epersons', () => { + expect(epersonsFound.length).toEqual(2); + }); + + describe('if eperson is already a eperson', () => { + it('should have delete button, else it should have add button', () => { + activeGroup.epersons.map((eperson: EPerson) => { + epersonsFound.map((foundEPersonRowElement) => { + if (foundEPersonRowElement.debugElement !== undefined) { + const epersonId = foundEPersonRowElement.debugElement.query(By.css('td:first-child')); + const addButton = foundEPersonRowElement.debugElement.query(By.css('td:last-child .fa-plus')); + const deleteButton = foundEPersonRowElement.debugElement.query(By.css('td:last-child .fa-trash-alt')); + if (epersonId.nativeElement.textContent === eperson.id) { + expect(addButton).toBeUndefined(); + expect(deleteButton).toBeDefined(); + } else { + expect(deleteButton).toBeUndefined(); + expect(addButton).toBeDefined(); + } + } + }) + }) + }); + }); + + describe('if first add button is pressed', () => { + beforeEach(fakeAsync(() => { + const addButton = fixture.debugElement.query(By.css('#epersonsSearch tbody .fa-plus')); + addButton.nativeElement.click(); + tick(); + fixture.detectChanges(); + })); + it('all groups in search member of selected group', () => { + epersonsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr')); + expect(epersonsFound.length).toEqual(2); + epersonsFound.map((foundEPersonRowElement) => { + if (foundEPersonRowElement.debugElement !== undefined) { + const addButton = foundEPersonRowElement.debugElement.query(By.css('td:last-child .fa-plus')); + const deleteButton = foundEPersonRowElement.debugElement.query(By.css('td:last-child .fa-trash-alt')); + expect(addButton).toBeUndefined(); + expect(deleteButton).toBeDefined(); + } + }) + }); + }); + + describe('if first delete button is pressed', () => { + beforeEach(fakeAsync(() => { + const addButton = fixture.debugElement.query(By.css('#epersonsSearch tbody .fa-trash-alt')); + addButton.nativeElement.click(); + tick(); + fixture.detectChanges(); + })); + it('first eperson in search delete button, because now member', () => { + epersonsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr')); + epersonsFound.map((foundEPersonRowElement) => { + if (foundEPersonRowElement.debugElement !== undefined) { + const addButton = foundEPersonRowElement.debugElement.query(By.css('td:last-child .fa-plus')); + const deleteButton = foundEPersonRowElement.debugElement.query(By.css('td:last-child .fa-trash-alt')); + expect(deleteButton).toBeUndefined(); + expect(addButton).toBeDefined(); + } + }) + }); + }); + }); + }); + +}); 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..b2e9ea75d6 --- /dev/null +++ b/src/app/+admin/admin-access-control/group-registry/group-form/members-list/members-list.component.ts @@ -0,0 +1,247 @@ +import { Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; +import { Router } from '@angular/router'; +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'; +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 being displayed in search result, initially all members, after search result of search + */ + ePeopleSearch: Observable>>; + /** + * List of EPeople members of currently active group being edited + */ + ePeopleMembersOfGroup: Observable>>; + + /** + * Pagination config used to display the list of EPeople that are result of EPeople search + */ + configSearch: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { + id: 'search-members-list-pagination', + pageSize: 5, + currentPage: 1 + }); + /** + * Pagination config used to display the list of EPerson Membes of active group being edited + */ + config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { + id: 'members-list-pagination', + pageSize: 5, + currentPage: 1 + }); + + /** + * List of subscriptions + */ + subs: Subscription[] = []; + + // The search form + searchForm; + + // Current search in edit group - epeople search form + currentSearchQuery: string; + currentSearchScope: string; + + // Whether or not user has done a EPeople search yet + searchDone: boolean; + + // current active group being edited + groupBeingEdited: Group; + + constructor(private groupDataService: GroupDataService, + public ePersonDataService: EPersonDataService, + private translateService: TranslateService, + private notificationsService: NotificationsService, + private formBuilder: FormBuilder, + private router: Router) { + this.currentSearchQuery = ''; + this.currentSearchScope = 'metadata'; + } + + ngOnInit() { + this.searchForm = this.formBuilder.group(({ + scope: 'metadata', + query: '', + })); + this.subs.push(this.groupDataService.getActiveGroup().subscribe((activeGroup: Group) => { + if (activeGroup != null) { + this.groupBeingEdited = activeGroup; + this.forceUpdateEPeople(activeGroup); + } + })); + } + + /** + * Event triggered when the user changes page on search result + * @param event + */ + onPageChangeSearch(event) { + this.configSearch.currentPage = event; + this.search({ scope: this.currentSearchScope, query: this.currentSearchQuery }); + } + + /** + * Event triggered when the user changes page on EPerson embers of active group + * @param event + */ + onPageChange(event) { + this.ePeopleMembersOfGroup = this.ePersonDataService.findAllByHref(this.groupBeingEdited._links.epersons.href, { + currentPage: event, + elementsPerPage: this.config.pageSize + }) + } + + /** + * 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) { + this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => { + if (activeGroup != null) { + 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')); + } + }); + } + + /** + * 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) { + this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => { + if (activeGroup != null) { + const response = this.groupDataService.addMemberToGroup(activeGroup, ePerson); + this.showNotifications('addMember', response, ePerson.name, activeGroup); + } else { + this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup')); + } + }); + this.forceUpdateEPeople(this.groupBeingEdited, ePerson); + } + + /** + * 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.ePersonDataService.findAllByHref(group._links.epersons.href, { + currentPage: 0, + elementsPerPage: Number.MAX_SAFE_INTEGER + }) + .pipe( + getSucceededRemoteData(), + getRemoteDataPayload(), + map((listEPeopleInGroup: PaginatedList) => listEPeopleInGroup.page.filter((ePersonInList: EPerson) => ePersonInList.id === possibleMember.id)), + map((epeople: EPerson[]) => epeople.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) { + const query: string = data.query; + const scope: string = data.scope; + if (query != null && this.currentSearchQuery !== query && this.groupBeingEdited) { + this.router.navigateByUrl(this.groupDataService.getGroupEditPageRouterLink(this.groupBeingEdited)); + this.currentSearchQuery = query; + this.configSearch.currentPage = 1; + } + if (scope != null && this.currentSearchScope !== scope && this.groupBeingEdited) { + this.router.navigateByUrl(this.groupDataService.getGroupEditPageRouterLink(this.groupBeingEdited)); + this.currentSearchScope = scope; + this.configSearch.currentPage = 1; + } + this.searchDone = true; + this.ePeopleSearch = this.ePersonDataService.searchByScope(this.currentSearchScope, this.currentSearchQuery, { + currentPage: this.configSearch.currentPage, + elementsPerPage: this.configSearch.pageSize + }); + } + + /** + * 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(activeGroup: Group, ePersonToUpdate?: EPerson) { + if (ePersonToUpdate != null) { + this.ePersonDataService.clearLinkRequests(ePersonToUpdate._links.groups.href); + } + this.ePersonDataService.clearLinkRequests(activeGroup._links.epersons.href); + this.router.navigateByUrl(this.groupDataService.getGroupEditPageRouterLink(activeGroup)); + this.ePeopleMembersOfGroup = this.ePersonDataService.findAllByHref(activeGroup._links.epersons.href, { + currentPage: this.configSearch.currentPage, + elementsPerPage: this.configSearch.pageSize + }) + } + + /** + * unsub all subscriptions + */ + 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 })); + } + }) + } + + /** + * Reset all input-fields to be empty and search all search + */ + clearFormAndResetResult() { + this.searchForm.patchValue({ + query: '', + }); + this.search({ query: '' }); + } +} 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..9558b9da98 --- /dev/null +++ b/src/app/+admin/admin-access-control/group-registry/group-form/subgroup-list/subgroups-list.component.html @@ -0,0 +1,117 @@ + +

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

+ + +
+
+
+ + + + +
+
+
+ + + +
+ + + + + + + + + + + + + + + +
{{messagePrefix + '.table.id' | translate}}{{messagePrefix + '.table.name' | translate}}{{messagePrefix + '.table.edit' | translate}}
{{group.id}}{{group.name}} +
+ + +

{{ messagePrefix + '.table.edit.currentGroup' | translate }}

+ + +
+
+
+
+ + + +

{{messagePrefix + '.headSubgroups' | 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.spec.ts b/src/app/+admin/admin-access-control/group-registry/group-form/subgroup-list/subgroups-list.component.spec.ts new file mode 100644 index 0000000000..4bd088b513 --- /dev/null +++ b/src/app/+admin/admin-access-control/group-registry/group-form/subgroup-list/subgroups-list.component.spec.ts @@ -0,0 +1,208 @@ +import { CommonModule } from '@angular/common'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { async, ComponentFixture, fakeAsync, flush, inject, TestBed, tick } from '@angular/core/testing'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { BrowserModule, By } from '@angular/platform-browser'; +import { Router } from '@angular/router'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core'; +import { Observable } from 'rxjs/internal/Observable'; +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'; +import { Group } from '../../../../../core/eperson/models/group.model'; +import { PageInfo } from '../../../../../core/shared/page-info.model'; +import { FormBuilderService } from '../../../../../shared/form/builder/form-builder.service'; +import { getMockFormBuilderService } from '../../../../../shared/mocks/mock-form-builder-service'; +import { MockRouter } from '../../../../../shared/mocks/mock-router'; +import { getMockTranslateService } from '../../../../../shared/mocks/mock-translate.service'; +import { NotificationsService } from '../../../../../shared/notifications/notifications.service'; +import { GroupMock, GroupMock2 } from '../../../../../shared/testing/group-mock'; +import { MockTranslateLoader } from '../../../../../shared/testing/mock-translate-loader'; +import { NotificationsServiceStub } from '../../../../../shared/testing/notifications-service-stub'; +import { of as observableOf } from 'rxjs'; +import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/testing/utils'; +import { SubgroupsListComponent } from './subgroups-list.component'; + +describe('SubgroupsListComponent', () => { + let component: SubgroupsListComponent; + let fixture: ComponentFixture; + let translateService: TranslateService; + let builderService: FormBuilderService; + let ePersonDataServiceStub: any; + let groupsDataServiceStub: any; + let activeGroup; + let subgroups; + let allGroups; + let routerStub; + + beforeEach(async(() => { + activeGroup = GroupMock; + subgroups = [GroupMock2]; + allGroups = [GroupMock, GroupMock2]; + ePersonDataServiceStub = {}; + groupsDataServiceStub = { + activeGroup: activeGroup, + subgroups: subgroups, + getActiveGroup(): Observable { + return observableOf(this.activeGroup); + }, + getSubgroups(): Group { + return this.activeGroup; + }, + findAllByHref(href: string): Observable>> { + return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), this.subgroups)) + }, + getGroupEditPageRouterLink(group: Group): string { + return '/admin/access-control/groups/' + group.id; + }, + searchGroups(query: string): Observable>> { + if (query === '') { + return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), allGroups)) + } + return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])) + }, + addSubGroupToGroup(parentGroup, subgroup: Group): Observable { + this.subgroups = [...this.subgroups, subgroup]; + return observableOf(new RestResponse(true, 200, 'Success')); + }, + clearGroupsRequests() { + // empty + }, + clearGroupLinkRequests() { + // empty + }, + deleteSubGroupFromGroup(parentGroup, subgroup: Group): Observable { + this.subgroups = this.subgroups.find((group: Group) => { + if (group.id !== subgroup.id) { + return group; + } + }); + return observableOf(new RestResponse(true, 200, 'Success')); + } + }; + routerStub = new MockRouter(); + builderService = getMockFormBuilderService(); + translateService = getMockTranslateService(); + TestBed.configureTestingModule({ + imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: MockTranslateLoader + } + }), + ], + declarations: [SubgroupsListComponent], + providers: [SubgroupsListComponent, + { provide: GroupDataService, useValue: groupsDataServiceStub }, + { provide: NotificationsService, useValue: new NotificationsServiceStub() }, + { provide: FormBuilderService, useValue: builderService }, + { provide: Router, useValue: routerStub }, + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SubgroupsListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + afterEach(fakeAsync(() => { + fixture.destroy(); + flush(); + component = null; + })); + + it('should create SubgroupsListComponent', inject([SubgroupsListComponent], (comp: SubgroupsListComponent) => { + expect(comp).toBeDefined(); + })); + + it('should show list of subgroups of current active group', () => { + const groupIdsFound = fixture.debugElement.queryAll(By.css('#subgroupsOfGroup tr td:first-child')); + expect(groupIdsFound.length).toEqual(1); + activeGroup.subgroups.map((group: Group) => { + expect(groupIdsFound.find((foundEl) => { + return (foundEl.nativeElement.textContent.trim() === group.uuid); + })).toBeTruthy(); + }) + }); + + describe('if first group delete button is pressed', () => { + let groupsFound; + beforeEach(fakeAsync(() => { + const addButton = fixture.debugElement.query(By.css('#subgroupsOfGroup tbody .deleteButton')); + addButton.triggerEventHandler('click', { + preventDefault: () => {/**/ + } + }); + tick(); + fixture.detectChanges(); + })); + it('one less subgroup in list from 1 to 0 (of 2 total groups)', () => { + groupsFound = fixture.debugElement.queryAll(By.css('#subgroupsOfGroup tbody tr')); + expect(groupsFound.length).toEqual(0); + }); + }); + + describe('search', () => { + describe('when searching with empty query', () => { + let groupsFound; + beforeEach(fakeAsync(() => { + component.search({ query: '' }); + groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr')); + })); + + it('should display all groups', () => { + fixture.detectChanges(); + groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr')); + expect(groupsFound.length).toEqual(2); + groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr')); + const groupIdsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr td:first-child')); + allGroups.map((group: Group) => { + expect(groupIdsFound.find((foundEl) => { + return (foundEl.nativeElement.textContent.trim() === group.uuid); + })).toBeTruthy(); + }) + }); + + describe('if group is already a subgroup', () => { + it('should have delete button, else it should have add button', () => { + fixture.detectChanges(); + groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr')); + const getSubgroups = groupsDataServiceStub.getSubgroups().subgroups; + if (getSubgroups !== undefined && getSubgroups.length > 0) { + groupsFound.map((foundGroupRowElement) => { + if (foundGroupRowElement.debugElement !== undefined) { + const addButton = foundGroupRowElement.debugElement.query(By.css('td:last-child .fa-plus')); + const deleteButton = foundGroupRowElement.debugElement.query(By.css('td:last-child .fa-trash-alt')); + expect(addButton).toBeUndefined(); + expect(deleteButton).toBeDefined(); + } + }) + } else { + getSubgroups.map((group: Group) => { + groupsFound.map((foundGroupRowElement) => { + if (foundGroupRowElement.debugElement !== undefined) { + const groupId = foundGroupRowElement.debugElement.query(By.css('td:first-child')); + const addButton = foundGroupRowElement.debugElement.query(By.css('td:last-child .fa-plus')); + const deleteButton = foundGroupRowElement.debugElement.query(By.css('td:last-child .fa-trash-alt')); + if (groupId.nativeElement.textContent === group.id) { + expect(addButton).toBeUndefined(); + expect(deleteButton).toBeDefined(); + } else { + expect(deleteButton).toBeUndefined(); + expect(addButton).toBeDefined(); + } + } + }) + }) + } + }); + }); + }); + }); + +}); 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..62927b74aa --- /dev/null +++ b/src/app/+admin/admin-access-control/group-registry/group-form/subgroup-list/subgroups-list.component.ts @@ -0,0 +1,253 @@ +import { Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; +import { Router } from '@angular/router'; +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'; +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-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; + + /** + * Result of search groups, initially all groups + */ + groupsSearch: Observable>>; + /** + * List of all subgroups of group being edited + */ + subgroupsOfGroup: Observable>>; + + /** + * List of subscriptions + */ + subs: Subscription[] = []; + + /** + * Pagination config used to display the list of groups that are result of groups search + */ + configSearch: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { + id: 'search-subgroups-list-pagination', + pageSize: 5, + currentPage: 1 + }); + /** + * Pagination config used to display the list of subgroups of currently active group being edited + */ + config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { + id: 'subgroups-list-pagination', + pageSize: 5, + currentPage: 1 + }); + + // The search form + searchForm; + + // Current search in edit group - groups search form + currentSearchQuery: string; + + // Whether or not user has done a Groups search yet + searchDone: boolean; + + // current active group being edited + groupBeingEdited: Group; + + constructor(public groupDataService: GroupDataService, + private translateService: TranslateService, + private notificationsService: NotificationsService, + private formBuilder: FormBuilder, + private router: Router) { + this.currentSearchQuery = ''; + } + + ngOnInit() { + this.searchForm = this.formBuilder.group(({ + query: '', + })); + this.subs.push(this.groupDataService.getActiveGroup().subscribe((activeGroup: Group) => { + if (activeGroup != null) { + this.groupBeingEdited = activeGroup; + this.forceUpdateGroups(activeGroup); + } + })); + } + + /** + * Event triggered when the user changes page on search result + * @param event + */ + onPageChangeSearch(event) { + this.configSearch.currentPage = event; + this.search({ query: this.currentSearchQuery }); + } + + /** + * Event triggered when the user changes page on subgroups of active group + * @param event + */ + onPageChange(event) { + this.subgroupsOfGroup = this.groupDataService.findAllByHref(this.groupBeingEdited._links.subgroups.href, { + currentPage: event, + elementsPerPage: this.config.pageSize + }); + } + + /** + * 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((activeGroup: Group) => { + if (activeGroup != null) { + if (activeGroup.uuid === possibleSubgroup.uuid) { + return observableOf(false); + } else { + return this.groupDataService.findAllByHref(activeGroup._links.subgroups.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); + } + })); + } + + /** + * Whether or not the given group is the current group being edited + * @param group Group that is possibly the current group being edited + */ + isActiveGroup(group: Group): Observable { + return this.groupDataService.getActiveGroup().pipe(take(1), + mergeMap((activeGroup: Group) => { + if (activeGroup != null && activeGroup.uuid === group.uuid) { + return observableOf(true); + } + return observableOf(false); + })); + } + + /** + * 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) { + const response = this.groupDataService.deleteSubGroupFromGroup(activeGroup, subgroup); + this.showNotifications('deleteSubgroup', response, subgroup.name, activeGroup); + this.forceUpdateGroups(activeGroup); + } else { + this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup')); + } + }); + } + + /** + * 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) { + if (activeGroup.uuid !== subgroup.uuid) { + const response = this.groupDataService.addSubGroupToGroup(activeGroup, subgroup); + this.showNotifications('addSubgroup', response, subgroup.name, activeGroup); + } else { + this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.subgroupToAddIsActiveGroup')); + } + } else { + this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup')); + } + }); + this.forceUpdateGroups(this.groupBeingEdited); + } + + /** + * 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; + if (query != null && this.currentSearchQuery !== query) { + this.router.navigateByUrl(this.groupDataService.getGroupEditPageRouterLink(this.groupBeingEdited)); + this.currentSearchQuery = query; + this.configSearch.currentPage = 1; + } + this.searchDone = true; + this.groupsSearch = this.groupDataService.searchGroups(this.currentSearchQuery, { + currentPage: this.configSearch.currentPage, + elementsPerPage: this.configSearch.pageSize + }); + } + + /** + * Force-update the list of groups by first clearing the cache of results of this active groups' subgroups, then performing a new REST call + * @param activeGroup Group currently being edited + */ + public forceUpdateGroups(activeGroup: Group) { + this.groupDataService.clearGroupLinkRequests(activeGroup._links.subgroups.href); + this.router.navigateByUrl(this.groupDataService.getGroupEditPageRouterLink(activeGroup)); + this.subgroupsOfGroup = this.groupDataService.findAllByHref(activeGroup._links.subgroups.href, { + currentPage: this.config.currentPage, + elementsPerPage: this.config.pageSize + }); + } + + /** + * unsub all subscriptions + */ + 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 })); + } + }) + } + + /** + * Reset all input-fields to be empty and search all search + */ + clearFormAndResetResult() { + this.searchForm.patchValue({ + query: '', + }); + this.search({ query: '' }); + } +} 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.spec.ts b/src/app/+admin/admin-access-control/group-registry/group-registry.reducers.spec.ts new file mode 100644 index 0000000000..6c9f9d327a --- /dev/null +++ b/src/app/+admin/admin-access-control/group-registry/group-registry.reducers.spec.ts @@ -0,0 +1,54 @@ +import { GroupMock } from '../../../shared/testing/group-mock'; +import { GroupRegistryCancelGroupAction, GroupRegistryEditGroupAction } from './group-registry.actions'; +import { groupRegistryReducer, GroupRegistryState } from './group-registry.reducers'; + +const initialState: GroupRegistryState = { + editGroup: null, +}; + +const editState: GroupRegistryState = { + editGroup: GroupMock, +}; + +class NullAction extends GroupRegistryEditGroupAction { + type = null; + + constructor() { + super(undefined); + } +} + +describe('groupRegistryReducer', () => { + + it('should return the current state when no valid actions have been made', () => { + const state = initialState; + const action = new NullAction(); + const newState = groupRegistryReducer(state, action); + + expect(newState).toEqual(state); + }); + + it('should start with an initial state', () => { + const state = initialState; + const action = new NullAction(); + const initState = groupRegistryReducer(undefined, action); + + expect(initState).toEqual(state); + }); + + it('should update the current state to change the editGroup to a new group when GroupRegistryEditGroupAction is dispatched', () => { + const state = editState; + const action = new GroupRegistryEditGroupAction(GroupMock); + const newState = groupRegistryReducer(state, action); + + expect(newState.editGroup).toEqual(GroupMock); + }); + + it('should update the current state to remove the editGroup from the state when GroupRegistryCancelGroupAction is dispatched', () => { + const state = editState; + const action = new GroupRegistryCancelGroupAction(); + const newState = groupRegistryReducer(state, action); + + expect(newState.editGroup).toEqual(null); + }); +}); 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..eca6c282a7 --- /dev/null +++ b/src/app/+admin/admin-access-control/group-registry/group-registry.reducers.ts @@ -0,0 +1,43 @@ +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; +} + +/** + * The initial state. + */ +const initialState: GroupRegistryState = { + editGroup: null, +}; + +/** + * 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..3bd7d7ac4f --- /dev/null +++ b/src/app/+admin/admin-access-control/group-registry/groups-registry.component.html @@ -0,0 +1,84 @@ +
+
+
+ + + +
+ +
+ + +
+
+
+ + + + +
+
+
+ + + +
+ + + + + + + + + + + + + + + + + + + +
{{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 + (getSubgroups(group) | async)?.payload?.totalElements}} +
+ + +
+
+
+
+ + + +
+
+
diff --git a/src/app/+admin/admin-access-control/group-registry/groups-registry.component.spec.ts b/src/app/+admin/admin-access-control/group-registry/groups-registry.component.spec.ts new file mode 100644 index 0000000000..7dff4f9bd1 --- /dev/null +++ b/src/app/+admin/admin-access-control/group-registry/groups-registry.component.spec.ts @@ -0,0 +1,139 @@ +import { CommonModule } from '@angular/common'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { async, ComponentFixture, fakeAsync, inject, TestBed, tick } from '@angular/core/testing'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { BrowserModule, By } from '@angular/platform-browser'; +import { Router } from '@angular/router'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { Observable } from 'rxjs/internal/Observable'; +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 { RouteService } from '../../../core/services/route.service'; +import { PageInfo } from '../../../core/shared/page-info.model'; +import { MockRouter } from '../../../shared/mocks/mock-router'; +import { MockTranslateLoader } from '../../../shared/mocks/mock-translate-loader'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { EPersonMock, EPersonMock2 } from '../../../shared/testing/eperson-mock'; +import { GroupMock, GroupMock2 } from '../../../shared/testing/group-mock'; +import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub'; +import { routeServiceStub } from '../../../shared/testing/route-service-stub'; +import { createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils'; +import { GroupsRegistryComponent } from './groups-registry.component'; + +describe('GroupRegistryComponent', () => { + let component: GroupsRegistryComponent; + let fixture: ComponentFixture; + let ePersonDataServiceStub: any; + let groupsDataServiceStub: any; + + let mockGroups; + let mockEPeople; + + beforeEach(async(() => { + mockGroups = [GroupMock, GroupMock2]; + mockEPeople = [EPersonMock, EPersonMock2]; + ePersonDataServiceStub = { + findAllByHref(href: string): Observable>> { + switch (href) { + case 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid2/epersons': + return createSuccessfulRemoteDataObject$(new PaginatedList(null, [])); + case 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid/epersons': + return createSuccessfulRemoteDataObject$(new PaginatedList(null, [EPersonMock])); + default: + return createSuccessfulRemoteDataObject$(new PaginatedList(null, [])); + } + } + }; + groupsDataServiceStub = { + allGroups: mockGroups, + findAllByHref(href: string): Observable>> { + switch (href) { + case 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid2/groups': + return createSuccessfulRemoteDataObject$(new PaginatedList(null, [])); + case 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid/groups': + return createSuccessfulRemoteDataObject$(new PaginatedList(null, [GroupMock2])); + default: + return createSuccessfulRemoteDataObject$(new PaginatedList(null, [])); + } + }, + getGroupEditPageRouterLink(group: Group): string { + return '/admin/access-control/groups/' + group.id; + }, + getGroupRegistryRouterLink(): string { + return '/admin/access-control/groups'; + }, + searchGroups(query: string): Observable>> { + if (query === '') { + return createSuccessfulRemoteDataObject$(new PaginatedList(null, this.allGroups)); + } + const result = this.allGroups.find((group: Group) => { + return (group.id.includes(query)) + }); + return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [result])); + } + }; + TestBed.configureTestingModule({ + imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: MockTranslateLoader + } + }), + ], + declarations: [GroupsRegistryComponent], + providers: [GroupsRegistryComponent, + { provide: EPersonDataService, useValue: ePersonDataServiceStub }, + { provide: GroupDataService, useValue: groupsDataServiceStub }, + { provide: NotificationsService, useValue: new NotificationsServiceStub() }, + { provide: RouteService, useValue: routeServiceStub }, + { provide: Router, useValue: new MockRouter() }, + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(GroupsRegistryComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create GroupRegistryComponent', inject([GroupsRegistryComponent], (comp: GroupsRegistryComponent) => { + expect(comp).toBeDefined(); + })); + + it('should display list of groups', () => { + const groupIdsFound = fixture.debugElement.queryAll(By.css('#groups tr td:first-child')); + expect(groupIdsFound.length).toEqual(2); + mockGroups.map((group: Group) => { + expect(groupIdsFound.find((foundEl) => { + return (foundEl.nativeElement.textContent.trim() === group.uuid); + })).toBeTruthy(); + }) + }); + + describe('search', () => { + describe('when searching with query', () => { + let groupIdsFound; + beforeEach(fakeAsync(() => { + component.search({ query: GroupMock2.id }); + tick(); + fixture.detectChanges(); + groupIdsFound = fixture.debugElement.queryAll(By.css('#groups tr td:first-child')); + })); + + it('should display search result', () => { + expect(groupIdsFound.length).toEqual(1); + expect(groupIdsFound.find((foundEl) => { + return (foundEl.nativeElement.textContent.trim() === GroupMock2.uuid); + })).toBeTruthy(); + }); + }); + }); +}); 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..c8ab102d30 --- /dev/null +++ b/src/app/+admin/admin-access-control/group-registry/groups-registry.component.ts @@ -0,0 +1,154 @@ +import { Component, OnInit } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; +import { Router } from '@angular/router'; +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 { RouteService } from '../../../core/services/route.service'; +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-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 implements OnInit { + + 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; + + // Current search in groups registry + currentSearchQuery: string; + + constructor(private groupService: GroupDataService, + private ePersonDataService: EPersonDataService, + private translateService: TranslateService, + private notificationsService: NotificationsService, + private formBuilder: FormBuilder, + protected routeService: RouteService, + private router: Router) { + this.currentSearchQuery = ''; + this.searchForm = this.formBuilder.group(({ + query: this.currentSearchQuery, + })); + } + + ngOnInit() { + this.search({ query: this.currentSearchQuery }); + } + + /** + * Event triggered when the user changes page + * @param event + */ + onPageChange(event) { + this.config.currentPage = event; + this.search({ query: this.currentSearchQuery }) + } + + /** + * 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; + if (query != null && this.currentSearchQuery !== query) { + this.router.navigateByUrl(this.groupService.getGroupRegistryRouterLink()); + this.currentSearchQuery = query; + this.config.currentPage = 1; + } + this.groups = this.groupService.searchGroups(this.currentSearchQuery.trim(), { + currentPage: this.config.currentPage, + elementsPerPage: this.config.pageSize + }); + } + + /** + * 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) { + 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: this.currentSearchQuery }) + } + + /** + * 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.subgroups.href); + } + + /** + * Reset all input-fields to be empty and search all search + */ + clearFormAndResetResult() { + this.searchForm.patchValue({ + query: '', + }); + this.search({ query: '' }); + } + + /** + * Extract optional UUID from a group name => To be resolved to community or collection with link + * (Or will be resolved in backend and added to group object, tbd) //TODO + * @param groupName + */ + getOptionalComColFromName(groupName: string): string { + return this.groupService.getUUIDFromString(groupName); + } +} diff --git a/src/app/+admin/admin-routing.module.ts b/src/app/+admin/admin-routing.module.ts index b4a68d692a..285ebeb0d1 100644 --- a/src/app/+admin/admin-routing.module.ts +++ b/src/app/+admin/admin-routing.module.ts @@ -1,9 +1,9 @@ -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 { 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'; @@ -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/+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/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'; diff --git a/src/app/core/eperson/eperson-data.service.spec.ts b/src/app/core/eperson/eperson-data.service.spec.ts index 1831386321..cd7bc72884 100644 --- a/src/app/core/eperson/eperson-data.service.spec.ts +++ b/src/app/core/eperson/eperson-data.service.spec.ts @@ -19,7 +19,6 @@ import { EPersonMock, EPersonMock2 } from '../../shared/testing/eperson-mock'; import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub'; import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; import { SearchParam } from '../cache/models/search-param.model'; -import { ObjectCacheService } from '../cache/object-cache.service'; import { CoreState } from '../core.reducers'; import { ChangeAnalyzer } from '../data/change-analyzer'; import { PaginatedList } from '../data/paginated-list'; @@ -39,43 +38,15 @@ describe('EPersonDataService', () => { let requestService: RequestService; let scheduler: TestScheduler; - const epeople = [EPersonMock, EPersonMock2]; + let epeople; - const restEndpointURL = 'https://dspace.4science.it/dspace-spring-rest/api/eperson'; - const epersonsEndpoint = `${restEndpointURL}/epersons`; - let halService: any = new HALEndpointServiceStub(restEndpointURL); - const epeople$ = createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [epeople])); - const rdbService = getMockRemoteDataBuildServiceHrefMap(undefined, { 'https://dspace.4science.it/dspace-spring-rest/api/eperson/epersons': epeople$ }); - const objectCache = Object.assign({ - /* tslint:disable:no-empty */ - remove: () => { - }, - hasBySelfLinkObservable: () => observableOf(false) - /* tslint:enable:no-empty */ - }) as ObjectCacheService; + let restEndpointURL; + let epersonsEndpoint; + let halService: any; + let epeople$; + let rdbService; - TestBed.configureTestingModule({ - imports: [ - CommonModule, - StoreModule.forRoot({}), - TranslateModule.forRoot({ - loader: { - provide: TranslateLoader, - useClass: MockTranslateLoader - } - }), - ], - declarations: [], - providers: [], - schemas: [CUSTOM_ELEMENTS_SCHEMA] - }); - - const getRequestEntry$ = (successful: boolean) => { - return observableOf({ - completed: true, - response: { isSuccessful: successful, payload: epeople } as any - } as RequestEntry) - }; + let getRequestEntry$; function initTestService() { return new EPersonDataService( @@ -90,7 +61,39 @@ describe('EPersonDataService', () => { ); } + function init() { + getRequestEntry$ = (successful: boolean) => { + return observableOf({ + completed: true, + response: { isSuccessful: successful, payload: epeople } as any + } as RequestEntry) + }; + restEndpointURL = 'https://dspace.4science.it/dspace-spring-rest/api/eperson'; + epersonsEndpoint = `${restEndpointURL}/epersons`; + epeople = [EPersonMock, EPersonMock2]; + epeople$ = createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [epeople])); + rdbService = getMockRemoteDataBuildServiceHrefMap(undefined, { 'https://dspace.4science.it/dspace-spring-rest/api/eperson/epersons': epeople$ }); + halService = new HALEndpointServiceStub(restEndpointURL); + + TestBed.configureTestingModule({ + imports: [ + CommonModule, + StoreModule.forRoot({}), + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: MockTranslateLoader + } + }), + ], + declarations: [], + providers: [], + schemas: [CUSTOM_ELEMENTS_SCHEMA] + }); + } + beforeEach(() => { + init(); requestService = getMockRequestService(getRequestEntry$(true)); store = new Store(undefined, undefined, undefined); service = initTestService(); diff --git a/src/app/core/eperson/eperson-data.service.ts b/src/app/core/eperson/eperson-data.service.ts index ec8b96d1cd..a8cee6f1de 100644 --- a/src/app/core/eperson/eperson-data.service.ts +++ b/src/app/core/eperson/eperson-data.service.ts @@ -181,7 +181,7 @@ export class EPersonDataService extends DataService { } /** - * Method that clears a cached EPerson request and returns its REST url + * Method that clears a cached EPerson request */ public clearEPersonRequests(): void { this.getBrowseEndpoint().pipe(take(1)).subscribe((link: string) => { @@ -189,6 +189,13 @@ export class EPersonDataService extends DataService { }); } + /** + * Method that clears a link's requests in cache + */ + public clearLinkRequests(href: string): void { + this.requestService.removeByHrefSubstring(href); + } + /** * Method to retrieve the eperson that is currently being edited */ @@ -219,4 +226,27 @@ export class EPersonDataService extends DataService { return this.delete(ePerson.id); } + /** + * Change which ePerson is being edited and return the link for EPeople edit page + * @param ePerson New EPerson to edit + */ + public startEditingNewEPerson(ePerson: EPerson): string { + this.getActiveEPerson().pipe(take(1)).subscribe((activeEPerson: EPerson) => { + if (ePerson === activeEPerson) { + this.cancelEditEPerson(); + } else { + this.editEPerson(ePerson); + } + }); + return '/admin/access-control/epeople'; + } + + /** + * Get EPeople admin page + * @param ePerson New EPerson to edit + */ + public getEPeoplePageRouterLink(): string { + return '/admin/access-control/epeople'; + } + } diff --git a/src/app/core/eperson/group-data.service.spec.ts b/src/app/core/eperson/group-data.service.spec.ts new file mode 100644 index 0000000000..b4a15a46d2 --- /dev/null +++ b/src/app/core/eperson/group-data.service.spec.ts @@ -0,0 +1,197 @@ +import { CommonModule } from '@angular/common'; +import { HttpHeaders } from '@angular/common/http'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { Store, StoreModule } from '@ngrx/store'; +import { of as observableOf } from 'rxjs'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { compare, Operation } from 'fast-json-patch'; +import { + GroupRegistryCancelGroupAction, + GroupRegistryEditGroupAction +} from '../../+admin/admin-access-control/group-registry/group-registry.actions'; +import { getMockRemoteDataBuildServiceHrefMap } from '../../shared/mocks/mock-remote-data-build.service'; +import { getMockRequestService } from '../../shared/mocks/mock-request.service'; +import { MockTranslateLoader } from '../../shared/mocks/mock-translate-loader'; +import { EPersonMock, EPersonMock2 } from '../../shared/testing/eperson-mock'; +import { GroupMock, GroupMock2 } from '../../shared/testing/group-mock'; +import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; +import { SearchParam } from '../cache/models/search-param.model'; +import { CoreState } from '../core.reducers'; +import { ChangeAnalyzer } from '../data/change-analyzer'; +import { PaginatedList } from '../data/paginated-list'; +import { DeleteByIDRequest, DeleteRequest, FindListOptions, PostRequest } from '../data/request.models'; +import { RequestEntry } from '../data/request.reducer'; +import { RequestService } from '../data/request.service'; +import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; +import { Item } from '../shared/item.model'; +import { PageInfo } from '../shared/page-info.model'; +import { GroupDataService } from './group-data.service'; + +describe('GroupDataService', () => { + let service: GroupDataService; + let store: Store; + let requestService: RequestService; + + let restEndpointURL; + let groupsEndpoint; + let groups; + let groups$; + let halService; + let rdbService; + + let getRequestEntry$; + + function init() { + getRequestEntry$ = (successful: boolean) => { + return observableOf({ + completed: true, + response: { isSuccessful: successful, payload: groups } as any + } as RequestEntry) + }; + restEndpointURL = 'https://dspace.4science.it/dspace-spring-rest/api/eperson'; + groupsEndpoint = `${restEndpointURL}/groups`; + groups = [GroupMock, GroupMock2]; + groups$ = createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), groups)); + rdbService = getMockRemoteDataBuildServiceHrefMap(undefined, { 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups': groups$ }); + halService = new HALEndpointServiceStub(restEndpointURL); + TestBed.configureTestingModule({ + imports: [ + CommonModule, + StoreModule.forRoot({}), + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: MockTranslateLoader + } + }), + ], + declarations: [], + providers: [], + schemas: [CUSTOM_ELEMENTS_SCHEMA] + }); + } + + function initTestService() { + return new GroupDataService( + new DummyChangeAnalyzer() as any, + null, + null, + requestService, + rdbService, + store, + null, + halService + ); + }; + + beforeEach(() => { + init(); + requestService = getMockRequestService(getRequestEntry$(true)); + store = new Store(undefined, undefined, undefined); + service = initTestService(); + spyOn(store, 'dispatch'); + }); + + describe('searchGroups', () => { + beforeEach(() => { + spyOn(service, 'searchBy'); + }); + + it('search with empty query', () => { + service.searchGroups(''); + const options = Object.assign(new FindListOptions(), { + searchParams: [Object.assign(new SearchParam('query', ''))] + }); + expect(service.searchBy).toHaveBeenCalledWith('byMetadata', options); + }); + + it('search with query', () => { + service.searchGroups('test'); + const options = Object.assign(new FindListOptions(), { + searchParams: [Object.assign(new SearchParam('query', 'test'))] + }); + expect(service.searchBy).toHaveBeenCalledWith('byMetadata', options); + }); + }); + + describe('deleteGroup', () => { + beforeEach(() => { + service.deleteGroup(GroupMock2).subscribe(); + }); + + it('should send DeleteRequest', () => { + const expected = new DeleteByIDRequest(requestService.generateRequestId(), groupsEndpoint + '/' + GroupMock2.uuid, GroupMock2.uuid); + expect(requestService.configure).toHaveBeenCalledWith(expected); + }); + }); + + describe('addSubGroupToGroup', () => { + beforeEach(() => { + service.addSubGroupToGroup(GroupMock, GroupMock2).subscribe(); + }); + it('should send PostRequest to eperson/groups/group-id/subgroups endpoint with new subgroup link in body', () => { + let headers = new HttpHeaders(); + const options: HttpOptions = Object.create({}); + headers = headers.append('Content-Type', 'text/uri-list'); + options.headers = headers; + const expected = new PostRequest(requestService.generateRequestId(), GroupMock.self + '/' + service.subgroupsEndpoint, GroupMock2.self, options); + expect(requestService.configure).toHaveBeenCalledWith(expected); + }); + }); + + describe('deleteSubGroupFromGroup', () => { + beforeEach(() => { + service.deleteSubGroupFromGroup(GroupMock, GroupMock2).subscribe(); + }); + it('should send DeleteRequest to eperson/groups/group-id/subgroups/group-id endpoint', () => { + const expected = new DeleteRequest(requestService.generateRequestId(), GroupMock.self + '/' + service.subgroupsEndpoint + '/' + GroupMock2.id); + expect(requestService.configure).toHaveBeenCalledWith(expected); + }); + }); + + describe('addMemberToGroup', () => { + beforeEach(() => { + service.addMemberToGroup(GroupMock, EPersonMock2).subscribe(); + }); + it('should send PostRequest to eperson/groups/group-id/epersons endpoint with new eperson member in body', () => { + let headers = new HttpHeaders(); + const options: HttpOptions = Object.create({}); + headers = headers.append('Content-Type', 'text/uri-list'); + options.headers = headers; + const expected = new PostRequest(requestService.generateRequestId(), GroupMock.self + '/' + service.ePersonsEndpoint, EPersonMock2.self, options); + expect(requestService.configure).toHaveBeenCalledWith(expected); + }); + }); + + describe('deleteMemberFromGroup', () => { + beforeEach(() => { + service.deleteMemberFromGroup(GroupMock, EPersonMock).subscribe(); + }); + it('should send DeleteRequest to eperson/groups/group-id/epersons/eperson-id endpoint', () => { + const expected = new DeleteRequest(requestService.generateRequestId(), GroupMock.self + '/' + service.ePersonsEndpoint + '/' + EPersonMock.id); + expect(requestService.configure).toHaveBeenCalledWith(expected); + }); + }); + + describe('editGroup', () => { + it('should dispatch a EDIT_GROUP action with the groupp to start editing', () => { + service.editGroup(GroupMock); + expect(store.dispatch).toHaveBeenCalledWith(new GroupRegistryEditGroupAction(GroupMock)); + }); + }); + + describe('cancelEditGroup', () => { + it('should dispatch a CANCEL_EDIT_GROUP action', () => { + service.cancelEditGroup(); + expect(store.dispatch).toHaveBeenCalledWith(new GroupRegistryCancelGroupAction()); + }); + }); +}); + +class DummyChangeAnalyzer implements ChangeAnalyzer { + diff(object1: Item, object2: Item): Operation[] { + return compare((object1 as any).metadata, (object2 as any).metadata); + } +} diff --git a/src/app/core/eperson/group-data.service.ts b/src/app/core/eperson/group-data.service.ts index 532f42323a..2dd7939547 100644 --- a/src/app/core/eperson/group-data.service.ts +++ b/src/app/core/eperson/group-data.service.ts @@ -1,28 +1,42 @@ +import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -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 { 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, RestRequest } from '../data/request.models'; import { RequestService } from '../data/request.service'; -import { FindListOptions } from '../data/request.models'; +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 { 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' @@ -31,6 +45,8 @@ import { GROUP } from './models/group.resource-type'; export class GroupDataService extends DataService { protected linkPath = 'groups'; protected browseEndpoint = ''; + public ePersonsEndpoint = 'epersons'; + public subgroupsEndpoint = 'subgroups'; constructor( protected comparator: DSOChangeAnalyzer, @@ -38,13 +54,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 +113,200 @@ 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.id); + } + + /** + * Create or Update a group + * If the group contains an id, it is assumed the eperson already exists and is updated instead + * @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 { + return this.create(group, null); + } + } + + /** + * // TODO + * @param {DSpaceObject} ePerson The given object + */ + updateGroup(group: Group): Observable> { + // 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): Observable { + 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); + + return this.fetchResponse(requestId); + } + + /** + * 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): 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); + } + + /** + * 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): Observable { + 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); + + return this.fetchResponse(requestId); + } + + /** + * 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): 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; + }) + ); + } + + /** + * 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 + */ + public clearGroupsRequests(): void { + this.getBrowseEndpoint().pipe(take(1)).subscribe((link: string) => { + this.requestService.removeByHrefSubstring(link); + }); + } + + /** + * Method that clears a cached get subgroups of certain group request + */ + public clearGroupLinkRequests(href: string): void { + this.requestService.removeByHrefSubstring(href); + } + + public getGroupRegistryRouterLink(): string { + return '/admin/access-control/groups'; + } + + /** + * Change which group is being edited and return the link for the edit page of the new group being edited + * @param newGroup New group to edit + */ + public startEditingNewGroup(newGroup: Group): string { + this.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => { + if (newGroup === activeGroup) { + this.cancelEditGroup() + } else { + this.editGroup(newGroup) + } + }); + return this.getGroupEditPageRouterLinkWithID(newGroup.id) + } + + /** + * Get Edit page of group + * @param group Group we want edit page for + */ + public getGroupEditPageRouterLink(group: Group): string { + return this.getGroupEditPageRouterLinkWithID(group.id); + } + + /** + * Get Edit page of group + * @param groupID Group ID we want edit page for + */ + public getGroupEditPageRouterLinkWithID(groupId: string): string { + return '/admin/access-control/groups/' + groupId; + } + + /** + * Extract optional UUID from a string + * @param stringWithUUID String with possible UUID + */ + public getUUIDFromString(stringWithUUID: string): string { + let foundUUID = ''; + const uuidMatches = stringWithUUID.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) { + foundUUID = uuidMatches[0]; + } + return foundUUID; } } diff --git a/src/app/core/eperson/models/group.model.ts b/src/app/core/eperson/models/group.model.ts index 5d531800b8..e496babddc 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 */ @@ -31,7 +39,8 @@ export class Group extends DSpaceObject { @deserialize _links: { self: HALLink; - groups: HALLink; + subgroups: HALLink; + epersons: HALLink; }; /** @@ -39,6 +48,13 @@ export class Group extends DSpaceObject { * Will be undefined unless the groups {@link HALLink} has been resolved. */ @link(GROUP, true) - public groups?: Observable>>; + public subgroups?: 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>>; } diff --git a/src/app/shared/testing/eperson-mock.ts b/src/app/shared/testing/eperson-mock.ts index 81a5a48a2c..61cf9e8b33 100644 --- a/src/app/shared/testing/eperson-mock.ts +++ b/src/app/shared/testing/eperson-mock.ts @@ -3,7 +3,7 @@ import { GroupMock } from './group-mock'; export const EPersonMock: EPerson = Object.assign(new EPerson(), { handle: null, - groups: [], + groups: [GroupMock], netid: 'test@test.com', lastActive: '2018-05-14T12:25:42.411+0000', canLogIn: true, @@ -49,7 +49,7 @@ export const EPersonMock: EPerson = Object.assign(new EPerson(), { export const EPersonMock2: EPerson = Object.assign(new EPerson(), { handle: null, - groups: [GroupMock], + groups: [], netid: 'test2@test.com', lastActive: '2019-05-14T12:25:42.411+0000', canLogIn: false, diff --git a/src/app/shared/testing/group-mock.ts b/src/app/shared/testing/group-mock.ts index 0c9abb4b7d..00068a5eea 100644 --- a/src/app/shared/testing/group-mock.ts +++ b/src/app/shared/testing/group-mock.ts @@ -1,16 +1,38 @@ import { Group } from '../../core/eperson/models/group.model'; +import { EPersonMock } from './eperson-mock'; + +export const GroupMock2: Group = Object.assign(new Group(), { + handle: null, + subgroups: [], + epersons: [], + permanent: true, + selfRegistered: false, + _links: { + self: { + href: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid2', + }, + subgroups: { href: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid2/subgroups' }, + epersons: { href: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid2/epersons' } + }, + id: 'testgroupid2', + uuid: 'testgroupid2', + type: 'group', +}); export const GroupMock: Group = Object.assign(new Group(), { - handle: null, - groups: [], - selfRegistered: false, - _links: { - self: { - href: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid', + handle: null, + subgroups: [GroupMock2], + epersons: [EPersonMock], + selfRegistered: false, + permanent: false, + _links: { + self: { + href: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid', + }, + subgroups: { href: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid/subgroups' }, + epersons: { href: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid/epersons' } }, - groups: { href: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid/groups' } - }, - id: 'testgroupid', - uuid: 'testgroupid', - type: 'group', + id: 'testgroupid', + uuid: 'testgroupid', + type: 'group', }); diff --git a/src/app/submission/sections/upload/section-upload.component.ts b/src/app/submission/sections/upload/section-upload.component.ts index f8f096d4bd..0bdb1a58f5 100644 --- a/src/app/submission/sections/upload/section-upload.component.ts +++ b/src/app/submission/sections/upload/section-upload.component.ts @@ -2,7 +2,6 @@ import { ChangeDetectorRef, Component, Inject } from '@angular/core'; import { BehaviorSubject, combineLatest as observableCombineLatest, Observable, Subscription} from 'rxjs'; import { distinctUntilChanged, filter, find, flatMap, map, reduce, take, tap } from 'rxjs/operators'; -import { followLink } from '../../../shared/utils/follow-link-config.model'; import { SectionModelComponent } from '../models/section.model'; import { hasValue, isNotEmpty, isNotUndefined, isUndefined } from '../../../shared/empty.util'; @@ -23,7 +22,6 @@ import { Group } from '../../../core/eperson/models/group.model'; import { SectionsService } from '../sections.service'; import { SubmissionService } from '../../submission.service'; import { Collection } from '../../../core/shared/collection.model'; -import { ResourcePolicy } from '../../../core/shared/resource-policy.model'; import { AccessConditionOption } from '../../../core/config/models/config-access-condition-option.model'; import { PaginatedList } from '../../../core/data/paginated-list'; @@ -205,7 +203,7 @@ export class SubmissionSectionUploadComponent extends SectionModelComponent { mapGroups$.push( this.groupService.findById(accessCondition.selectGroupUUID).pipe( find((rd: RemoteData) => !rd.isResponsePending && rd.hasSucceeded), - flatMap((group: RemoteData) => group.payload.groups), + flatMap((group: RemoteData) => group.payload.subgroups), find((rd: RemoteData>) => !rd.isResponsePending && rd.hasSucceeded), map((rd: RemoteData>) => ({ accessCondition: accessCondition.name,