diff --git a/src/app/access-control/access-control.module.ts b/src/app/access-control/access-control.module.ts index 891238bbed..2c9932d387 100644 --- a/src/app/access-control/access-control.module.ts +++ b/src/app/access-control/access-control.module.ts @@ -10,6 +10,7 @@ import { MembersListComponent } from './group-registry/group-form/members-list/m import { SubgroupsListComponent } from './group-registry/group-form/subgroup-list/subgroups-list.component'; import { GroupsRegistryComponent } from './group-registry/groups-registry.component'; import { FormModule } from '../shared/form/form.module'; +import { EPersonListComponent } from './group-registry/group-form/eperson-list/eperson-list.component'; @NgModule({ imports: [ @@ -17,7 +18,10 @@ import { FormModule } from '../shared/form/form.module'; SharedModule, RouterModule, AccessControlRoutingModule, - FormModule + FormModule, + ], + exports: [ + EPersonListComponent, ], declarations: [ EPeopleRegistryComponent, @@ -25,8 +29,9 @@ import { FormModule } from '../shared/form/form.module'; GroupsRegistryComponent, GroupFormComponent, SubgroupsListComponent, - MembersListComponent - ] + MembersListComponent, + EPersonListComponent, + ], }) /** * This module handles all components related to the access control pages diff --git a/src/app/access-control/group-registry/group-form/eperson-list/eperson-list.component.html b/src/app/access-control/group-registry/group-form/eperson-list/eperson-list.component.html new file mode 100644 index 0000000000..e4a507ae19 --- /dev/null +++ b/src/app/access-control/group-registry/group-form/eperson-list/eperson-list.component.html @@ -0,0 +1,146 @@ + +

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

+ + +
+
+ +
+
+
+ + + + +
+
+
+ +
+
+ + + +
+ + + + + + + + + + + + + + + + + +
{{messagePrefix + '.table.id' | translate}}{{messagePrefix + '.table.name' | translate}}{{messagePrefix + '.table.identity' | translate}}{{messagePrefix + '.table.edit' | translate}}
{{ePerson.eperson.id}}{{ePerson.eperson.name}} + {{messagePrefix + '.table.email' | translate}}: {{ ePerson.eperson.email ? ePerson.eperson.email : '-' }}
+ {{messagePrefix + '.table.netid' | translate}}: {{ ePerson.eperson.netid ? ePerson.eperson.netid : '-' }} +
+
+ + + +
+
+
+ +
+ + + +

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

+ + + +
+ + + + + + + + + + + + + + + + + +
{{messagePrefix + '.table.id' | translate}}{{messagePrefix + '.table.name' | translate}}{{messagePrefix + '.table.identity' | translate}}{{messagePrefix + '.table.edit' | translate}}
{{ePerson.eperson.id}}{{ePerson.eperson.name}} + {{messagePrefix + '.table.email' | translate}}: {{ ePerson.eperson.email ? ePerson.eperson.email : '-' }}
+ {{messagePrefix + '.table.netid' | translate}}: {{ ePerson.eperson.netid ? ePerson.eperson.netid : '-' }} +
+
+ + +
+
+
+ +
+ + + +
diff --git a/src/app/access-control/group-registry/group-form/eperson-list/eperson-list.component.spec.ts b/src/app/access-control/group-registry/group-form/eperson-list/eperson-list.component.spec.ts new file mode 100644 index 0000000000..8077139026 --- /dev/null +++ b/src/app/access-control/group-registry/group-form/eperson-list/eperson-list.component.spec.ts @@ -0,0 +1,247 @@ +import { CommonModule } from '@angular/common'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, fakeAsync, flush, inject, TestBed, tick, waitForAsync } 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, of as observableOf } from 'rxjs'; +import { RestResponse } from '../../../../core/cache/response.models'; +import { buildPaginatedList, PaginatedList } from '../../../../core/data/paginated-list.model'; +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 { NotificationsService } from '../../../../shared/notifications/notifications.service'; +import { GroupMock, GroupMock2 } from '../../../../shared/testing/group-mock'; +import { EPersonListComponent } from './eperson-list.component'; +import { EPersonMock, EPersonMock2 } from '../../../../shared/testing/eperson.mock'; +import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; +import { getMockTranslateService } from '../../../../shared/mocks/translate.service.mock'; +import { getMockFormBuilderService } from '../../../../shared/mocks/form-builder-service.mock'; +import { TranslateLoaderMock } from '../../../../shared/testing/translate-loader.mock'; +import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service.stub'; +import { RouterMock } from '../../../../shared/mocks/router.mock'; +import { PaginationService } from '../../../../core/pagination/pagination.service'; +import { PaginationServiceStub } from '../../../../shared/testing/pagination-service.stub'; + +describe('EPersonListComponent', () => { + let component: EPersonListComponent; + 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; + let paginationService; + + beforeEach(waitForAsync(() => { + 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$(buildPaginatedList(new PageInfo(), groupsDataServiceStub.getEPersonMembers())); + }, + searchByScope(scope: string, query: string): Observable>> { + if (query === '') { + return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), allEPersons)); + } + return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])); + }, + clearEPersonRequests() { + // empty + }, + clearLinkRequests() { + // empty + }, + getEPeoplePageRouterLink(): string { + return '/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$(buildPaginatedList(new PageInfo(), this.allGroups)); + } + return createSuccessfulRemoteDataObject$(buildPaginatedList(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 '/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(); + + paginationService = new PaginationServiceStub(); + TestBed.configureTestingModule({ + imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock + } + }), + ], + declarations: [EPersonListComponent], + providers: [EPersonListComponent, + { provide: EPersonDataService, useValue: ePersonDataServiceStub }, + { provide: GroupDataService, useValue: groupsDataServiceStub }, + { provide: NotificationsService, useValue: new NotificationsServiceStub() }, + { provide: FormBuilderService, useValue: builderService }, + { provide: Router, useValue: new RouterMock() }, + { provide: PaginationService, useValue: paginationService }, + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(EPersonListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + afterEach(fakeAsync(() => { + fixture.destroy(); + flush(); + component = null; + fixture.debugElement.nativeElement.remove(); + })); + + it('should create EpeopleListComponent', inject([EPersonListComponent], (comp: EPersonListComponent) => { + 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/access-control/group-registry/group-form/eperson-list/eperson-list.component.ts b/src/app/access-control/group-registry/group-form/eperson-list/eperson-list.component.ts new file mode 100644 index 0000000000..9eafe10daa --- /dev/null +++ b/src/app/access-control/group-registry/group-form/eperson-list/eperson-list.component.ts @@ -0,0 +1,358 @@ +import { Component, OnDestroy, OnInit, Input } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; +import { Router } from '@angular/router'; +import { TranslateService } from '@ngx-translate/core'; +import { EPersonDataService } from '../../../../core/eperson/eperson-data.service'; +import { GroupDataService } from '../../../../core/eperson/group-data.service'; +import { NotificationsService } from '../../../../shared/notifications/notifications.service'; +import { PaginationService } from '../../../../core/pagination/pagination.service'; +import { Group } from '../../../../core/eperson/models/group.model'; +import { + getAllCompletedRemoteData, + getFirstSucceededRemoteData, + getRemoteDataPayload, + getFirstCompletedRemoteData +} from '../../../../core/shared/operators'; +import { + BehaviorSubject, + Subscription, + combineLatest as observableCombineLatest, + Observable, + ObservedValueOf, + of as observableOf +} from 'rxjs'; +import { PaginatedList, buildPaginatedList } from '../../../../core/data/paginated-list.model'; +import { EpersonDtoModel } from '../../../../core/eperson/models/eperson-dto.model'; +import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model'; +import { switchMap, map, take, mergeMap } from 'rxjs/operators'; +import { RemoteData } from '../../../../core/data/remote-data'; +import { EPerson } from '../../../../core/eperson/models/eperson.model'; + +/** + * Keys to keep track of specific subscriptions + */ +enum SubKey { + ActiveGroup, + MembersDTO, + SearchResultsDTO, +} + +export interface EPersonActionConfig { + css?: string; + disabled: boolean; + icon: string; +} + +export interface EPersonListActionConfig { + add: EPersonActionConfig; + remove: EPersonActionConfig; +} + +@Component({ + selector: 'ds-eperson-list', + templateUrl: './eperson-list.component.html' +}) +export class EPersonListComponent implements OnInit, OnDestroy { + + @Input() + messagePrefix: string; + + @Input() + actionConfig: EPersonListActionConfig = { + add: { + css: 'btn-outline-primary', + disabled: false, + icon: 'fas fa-plus fa-fw', + }, + remove: { + css: 'btn-outline-danger', + disabled: false, + icon: 'fas fa-trash-alt fa-fw' + }, + }; + + /** + * EPeople being displayed in search result, initially all members, after search result of search + */ + ePeopleSearchDtos: BehaviorSubject> = new BehaviorSubject>(undefined); + /** + * List of EPeople members of currently active group being edited + */ + ePeopleMembersOfGroupDtos: BehaviorSubject> = new BehaviorSubject>(undefined); + + /** + * Pagination config used to display the list of EPeople that are result of EPeople search + */ + configSearch: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { + id: 'sml', + 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: 'ml', + pageSize: 5, + currentPage: 1 + }); + + /** + * Map of active subscriptions + */ + subs: Map = new Map(); + + // 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( + protected groupDataService: GroupDataService, + public ePersonDataService: EPersonDataService, + protected translateService: TranslateService, + protected notificationsService: NotificationsService, + protected formBuilder: FormBuilder, + protected paginationService: PaginationService, + private router: Router + ) { + this.currentSearchQuery = ''; + this.currentSearchScope = 'metadata'; + } + + ngOnInit(): void { + this.searchForm = this.formBuilder.group(({ + scope: 'metadata', + query: '', + })); + this.subs.set(SubKey.ActiveGroup, this.groupDataService.getActiveGroup().subscribe((activeGroup: Group) => { + if (activeGroup != null) { + this.groupBeingEdited = activeGroup; + this.retrieveMembers(this.config.currentPage); + } + })); + } + + /** + * Retrieve the EPersons that are members of the group + * + * @param page the number of the page to retrieve + * @private + */ + retrieveMembers(page: number): void { + this.unsubFrom(SubKey.MembersDTO); + this.subs.set(SubKey.MembersDTO, + this.paginationService.getCurrentPagination(this.config.id, this.config).pipe( + switchMap((currentPagination) => { + return this.ePersonDataService.findAllByHref(this.groupBeingEdited._links.epersons.href, { + currentPage: currentPagination.currentPage, + elementsPerPage: currentPagination.pageSize + } + ); + }), + getAllCompletedRemoteData(), + map((rd: RemoteData) => { + if (rd.hasFailed) { + this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure', { cause: rd.errorMessage })); + } else { + return rd; + } + }), + switchMap((epersonListRD: RemoteData>) => { + const dtos$ = observableCombineLatest(...epersonListRD.payload.page.map((member: EPerson) => { + const dto$: Observable = observableCombineLatest( + this.isMemberOfGroup(member), (isMember: ObservedValueOf>) => { + const epersonDtoModel: EpersonDtoModel = new EpersonDtoModel(); + epersonDtoModel.eperson = member; + epersonDtoModel.memberOfGroup = isMember; + return epersonDtoModel; + }); + return dto$; + })); + return dtos$.pipe(map((dtos: EpersonDtoModel[]) => { + return buildPaginatedList(epersonListRD.payload.pageInfo, dtos); + })); + })) + .subscribe((paginatedListOfDTOs: PaginatedList) => { + this.ePeopleMembersOfGroupDtos.next(paginatedListOfDTOs); + })); + } + + /** + * Whether 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: 1, + elementsPerPage: 9999 + }, false) + .pipe( + getFirstSucceededRemoteData(), + getRemoteDataPayload(), + map((listEPeopleInGroup: PaginatedList) => listEPeopleInGroup.page.filter((ePersonInList: EPerson) => ePersonInList.id === possibleMember.id)), + map((epeople: EPerson[]) => epeople.length > 0)); + } else { + return observableOf(false); + } + })); + } + + /** + * Unsubscribe from a subscription if it's still subscribed, and remove it from the map of + * active subscriptions + * + * @param key The key of the subscription to unsubscribe from + * @private + */ + protected unsubFrom(key: SubKey) { + if (this.subs.has(key)) { + this.subs.get(key).unsubscribe(); + this.subs.delete(key); + } + } + + /** + * 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: EpersonDtoModel) { + this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => { + if (activeGroup != null) { + const response = this.groupDataService.deleteMemberFromGroup(activeGroup, ePerson.eperson); + this.showNotifications('deleteMember', response, ePerson.eperson.name, activeGroup); + this.search({ scope: this.currentSearchScope, query: this.currentSearchQuery }); + } 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: EpersonDtoModel) { + ePerson.memberOfGroup = true; + this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => { + if (activeGroup != null) { + const response = this.groupDataService.addMemberToGroup(activeGroup, ePerson.eperson); + this.showNotifications('addMember', response, ePerson.eperson.name, activeGroup); + } else { + this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup')); + } + }); + } + + /** + * Search in the EPeople by name, email or metadata + * @param data Contains scope and query param + */ + search(data: any) { + this.unsubFrom(SubKey.SearchResultsDTO); + this.subs.set(SubKey.SearchResultsDTO, + this.paginationService.getCurrentPagination(this.configSearch.id, this.configSearch).pipe( + switchMap((paginationOptions) => { + + const query: string = data.query; + const scope: string = data.scope; + if (query != null && this.currentSearchQuery !== query && this.groupBeingEdited) { + this.router.navigate([], { + queryParamsHandling: 'merge' + }); + this.currentSearchQuery = query; + this.paginationService.resetPage(this.configSearch.id); + } + if (scope != null && this.currentSearchScope !== scope && this.groupBeingEdited) { + this.router.navigate([], { + queryParamsHandling: 'merge' + }); + this.currentSearchScope = scope; + this.paginationService.resetPage(this.configSearch.id); + } + this.searchDone = true; + + return this.ePersonDataService.searchByScope(this.currentSearchScope, this.currentSearchQuery, { + currentPage: paginationOptions.currentPage, + elementsPerPage: paginationOptions.pageSize + }); + }), + getAllCompletedRemoteData(), + map((rd: RemoteData) => { + if (rd.hasFailed) { + this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure', { cause: rd.errorMessage })); + } else { + return rd; + } + }), + switchMap((epersonListRD: RemoteData>) => { + const dtos$ = observableCombineLatest(...epersonListRD.payload.page.map((member: EPerson) => { + const dto$: Observable = observableCombineLatest( + this.isMemberOfGroup(member), (isMember: ObservedValueOf>) => { + const epersonDtoModel: EpersonDtoModel = new EpersonDtoModel(); + epersonDtoModel.eperson = member; + epersonDtoModel.memberOfGroup = isMember; + return epersonDtoModel; + }); + return dto$; + })); + return dtos$.pipe(map((dtos: EpersonDtoModel[]) => { + return buildPaginatedList(epersonListRD.payload.pageInfo, dtos); + })); + })) + .subscribe((paginatedListOfDTOs: PaginatedList) => { + this.ePeopleSearchDtos.next(paginatedListOfDTOs); + })); + } + + /** + * unsub all subscriptions + */ + ngOnDestroy(): void { + for (const key of this.subs.keys()) { + this.unsubFrom(key); + } + this.paginationService.clearPagination(this.config.id); + this.paginationService.clearPagination(this.configSearch.id); + } + + /** + * 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(getFirstCompletedRemoteData()).subscribe((rd: RemoteData) => { + if (rd.hasSucceeded) { + this.notificationsService.success(this.translateService.get(this.messagePrefix + '.notification.success.' + messageSuffix, { name: nameObject })); + this.ePersonDataService.clearLinkRequests(activeGroup._links.epersons.href); + } 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/access-control/group-registry/group-form/members-list/members-list.component.spec.ts b/src/app/access-control/group-registry/group-form/members-list/members-list.component.spec.ts index 0b19b17100..0fa405a1c9 100644 --- a/src/app/access-control/group-registry/group-form/members-list/members-list.component.spec.ts +++ b/src/app/access-control/group-registry/group-form/members-list/members-list.component.spec.ts @@ -149,6 +149,7 @@ describe('MembersListComponent', () => { fixture.destroy(); flush(); component = null; + fixture.debugElement.nativeElement.remove(); })); it('should create MembersListComponent', inject([MembersListComponent], (comp: MembersListComponent) => { diff --git a/src/app/access-control/group-registry/group-form/members-list/members-list.component.ts b/src/app/access-control/group-registry/group-form/members-list/members-list.component.ts index 54d144da51..8bc540641e 100644 --- a/src/app/access-control/group-registry/group-form/members-list/members-list.component.ts +++ b/src/app/access-control/group-registry/group-form/members-list/members-list.component.ts @@ -94,13 +94,15 @@ export class MembersListComponent implements OnInit, OnDestroy { paginationSub: Subscription; - constructor(private groupDataService: GroupDataService, - public ePersonDataService: EPersonDataService, - private translateService: TranslateService, - private notificationsService: NotificationsService, - private formBuilder: FormBuilder, - private paginationService: PaginationService, - private router: Router) { + constructor( + protected groupDataService: GroupDataService, + public ePersonDataService: EPersonDataService, + private translateService: TranslateService, + private notificationsService: NotificationsService, + protected formBuilder: FormBuilder, + private paginationService: PaginationService, + private router: Router + ) { this.currentSearchQuery = ''; this.currentSearchScope = 'metadata'; } @@ -124,7 +126,7 @@ export class MembersListComponent implements OnInit, OnDestroy { * @param page the number of the page to retrieve * @private */ - private retrieveMembers(page: number) { + protected retrieveMembers(page: number) { this.unsubFrom(SubKey.MembersDTO); this.subs.set(SubKey.MembersDTO, this.paginationService.getCurrentPagination(this.config.id, this.config).pipe( diff --git a/src/app/core/data/request.service.spec.ts b/src/app/core/data/request.service.spec.ts index 7a07f6fe10..fb4edff76f 100644 --- a/src/app/core/data/request.service.spec.ts +++ b/src/app/core/data/request.service.spec.ts @@ -592,6 +592,19 @@ describe('RequestService', () => { 'property1=multiple%0Alines%0Ato%0Asend&property2=sp%26ci%40l%20characters&sp%26ci%40l-chars%20in%20prop=test123' ); }); + + it('should properly encode the body with an array', () => { + const body = { + 'property1': 'multiple\nlines\nto\nsend', + 'property2': 'sp&ci@l characters', + 'sp&ci@l-chars in prop': 'test123', + 'arrayParam': ['arrayValue1', 'arrayValue2'], + }; + const queryParams = service.uriEncodeBody(body); + expect(queryParams).toEqual( + 'property1=multiple%0Alines%0Ato%0Asend&property2=sp%26ci%40l%20characters&sp%26ci%40l-chars%20in%20prop=test123&arrayParam=arrayValue1&arrayParam=arrayValue2' + ); + }); }); }); diff --git a/src/app/core/data/request.service.ts b/src/app/core/data/request.service.ts index 14499b8214..f26b36da08 100644 --- a/src/app/core/data/request.service.ts +++ b/src/app/core/data/request.service.ts @@ -252,8 +252,8 @@ export class RequestService { /** * Convert request Payload to a URL-encoded string * - * e.g. uriEncodeBody({param: value, param1: value1}) - * returns: param=value¶m1=value1 + * e.g. uriEncodeBody({param: value, param1: value1, param2: [value3, value4]}) + * returns: param=value¶m1=value1¶m2=value3¶m2=value4 * * @param body * The request Payload to convert @@ -264,11 +264,19 @@ export class RequestService { let queryParams = ''; if (isNotEmpty(body) && typeof body === 'object') { Object.keys(body) - .forEach((param) => { + .forEach((param: string) => { const encodedParam = encodeURIComponent(param); - const encodedBody = encodeURIComponent(body[param]); - const paramValue = `${encodedParam}=${encodedBody}`; - queryParams = isEmpty(queryParams) ? queryParams.concat(paramValue) : queryParams.concat('&', paramValue); + if (Array.isArray(body[param])) { + for (const element of body[param]) { + const encodedBody = encodeURIComponent(element); + const paramValue = `${encodedParam}=${encodedBody}`; + queryParams = isEmpty(queryParams) ? queryParams.concat(paramValue) : queryParams.concat('&', paramValue); + } + } else { + const encodedBody = encodeURIComponent(body[param]); + const paramValue = `${encodedParam}=${encodedBody}`; + queryParams = isEmpty(queryParams) ? queryParams.concat(paramValue) : queryParams.concat('&', paramValue); + } }); } return queryParams; diff --git a/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-select-reviewer/advanced-workflow-action-select-reviewer.component.html b/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-select-reviewer/advanced-workflow-action-select-reviewer.component.html index 64e5638de6..3009cc0771 100644 --- a/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-select-reviewer/advanced-workflow-action-select-reviewer.component.html +++ b/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-select-reviewer/advanced-workflow-action-select-reviewer.component.html @@ -1,5 +1,15 @@
- advancedInfo: {{ (workflowAction$ | async)?.advancedInfo | json }} +

{{ 'advanced-workflow-action.select-reviewer.description-multiple' | translate }}

+

{{ 'advanced-workflow-action.select-reviewer.description-single' | translate }}

+ + diff --git a/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-select-reviewer/advanced-workflow-action-select-reviewer.component.scss b/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-select-reviewer/advanced-workflow-action-select-reviewer.component.scss index e69de29bb2..65f38247c8 100644 --- a/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-select-reviewer/advanced-workflow-action-select-reviewer.component.scss +++ b/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-select-reviewer/advanced-workflow-action-select-reviewer.component.scss @@ -0,0 +1,7 @@ +:host ::ng-deep { + .reviewersListWithGroup { + #search, #search + form, #search + form + ds-pagination { + display: none !important; + } + } +} diff --git a/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-select-reviewer/advanced-workflow-action-select-reviewer.component.ts b/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-select-reviewer/advanced-workflow-action-select-reviewer.component.ts index df867d5595..29949a0ae3 100644 --- a/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-select-reviewer/advanced-workflow-action-select-reviewer.component.ts +++ b/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-select-reviewer/advanced-workflow-action-select-reviewer.component.ts @@ -1,8 +1,17 @@ -import { Component } from '@angular/core'; +import { Component, OnInit, OnDestroy } from '@angular/core'; import { rendersAdvancedWorkflowTaskOption } from '../../../shared/mydspace-actions/claimed-task/switcher/claimed-task-actions-decorator'; import { AdvancedWorkflowActionComponent } from '../advanced-workflow-action/advanced-workflow-action.component'; +import { WorkflowAction } from '../../../core/tasks/models/workflow-action-object.model'; +import { + SelectReviewerActionAdvancedInfo +} from '../../../core/tasks/models/select-reviewer-action-advanced-info.model'; +import { + EPersonListActionConfig +} from '../../../access-control/group-registry/group-form/eperson-list/eperson-list.component'; +import { Subscription } from 'rxjs'; +import { EPerson } from '../../../core/eperson/models/eperson.model'; export const WORKFLOW_ADVANCED_TASK_OPTION_SELECT_REVIEWER = 'submit_select_reviewer'; export const ADVANCED_WORKFLOW_ACTION_SELECT_REVIEWER = 'selectrevieweraction'; @@ -13,7 +22,63 @@ export const ADVANCED_WORKFLOW_ACTION_SELECT_REVIEWER = 'selectrevieweraction'; templateUrl: './advanced-workflow-action-select-reviewer.component.html', styleUrls: ['./advanced-workflow-action-select-reviewer.component.scss'], }) -export class AdvancedWorkflowActionSelectReviewerComponent extends AdvancedWorkflowActionComponent { +export class AdvancedWorkflowActionSelectReviewerComponent extends AdvancedWorkflowActionComponent implements OnInit, OnDestroy { + + multipleReviewers = true; + + selectedReviewers: EPerson[]; + + reviewersListActionConfig: EPersonListActionConfig; + + /** + * When the component is created the value is `undefined`, afterwards it will be set to either the group id or `null`. + * It needs to be subscribed in the **ngOnInit()** because otherwise some unnecessary request will be made. + */ + groupId?: string | null; + + subs: Subscription[] = []; + + ngOnDestroy(): void { + this.subs.forEach((subscription: Subscription) => subscription.unsubscribe()); + } + + ngOnInit(): void { + super.ngOnInit(); + if (this.multipleReviewers) { + this.reviewersListActionConfig = { + add: { + css: 'btn-outline-primary', + disabled: false, + icon: 'fas fa-plus', + }, + remove: { + css: 'btn-outline-danger', + disabled: false, + icon: 'fas fa-minus' + }, + }; + } else { + this.reviewersListActionConfig = { + add: { + css: 'btn-outline-primary', + disabled: false, + icon: 'fas fa-check', + }, + remove: { + css: 'btn-primary', + disabled: true, + icon: 'fas fa-check' + }, + }; + } + this.subs.push(this.workflowAction$.subscribe((workflowAction: WorkflowAction) => { + if (workflowAction) { + this.groupId = (workflowAction.advancedInfo as SelectReviewerActionAdvancedInfo[])[0].group; + } else { + this.groupId = null; + } + })); + } getType(): string { return ADVANCED_WORKFLOW_ACTION_SELECT_REVIEWER; @@ -22,6 +87,7 @@ export class AdvancedWorkflowActionSelectReviewerComponent extends AdvancedWorkf createBody(): any { return { [WORKFLOW_ADVANCED_TASK_OPTION_SELECT_REVIEWER]: true, + eperson: this.selectedReviewers.map((ePerson: EPerson) => ePerson.id), }; } diff --git a/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-select-reviewer/reviewers-list/reviewers-list.component.html b/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-select-reviewer/reviewers-list/reviewers-list.component.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-select-reviewer/reviewers-list/reviewers-list.component.spec.ts b/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-select-reviewer/reviewers-list/reviewers-list.component.spec.ts new file mode 100644 index 0000000000..bf27b1e79f --- /dev/null +++ b/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-select-reviewer/reviewers-list/reviewers-list.component.spec.ts @@ -0,0 +1,212 @@ +import { CommonModule } from '@angular/common'; +import { NO_ERRORS_SCHEMA, SimpleChange } from '@angular/core'; +import { ComponentFixture, fakeAsync, flush, inject, TestBed, waitForAsync } 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, of as observableOf } from 'rxjs'; +import { RestResponse } from '../../../../core/cache/response.models'; +import { buildPaginatedList, PaginatedList } from '../../../../core/data/paginated-list.model'; +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 { NotificationsService } from '../../../../shared/notifications/notifications.service'; +import { GroupMock, GroupMock2 } from '../../../../shared/testing/group-mock'; +import { ReviewersListComponent } from './reviewers-list.component'; +import { EPersonMock, EPersonMock2 } from '../../../../shared/testing/eperson.mock'; +import { + createSuccessfulRemoteDataObject$, + createNoContentRemoteDataObject$ +} from '../../../../shared/remote-data.utils'; +import { getMockTranslateService } from '../../../../shared/mocks/translate.service.mock'; +import { getMockFormBuilderService } from '../../../../shared/mocks/form-builder-service.mock'; +import { TranslateLoaderMock } from '../../../../shared/testing/translate-loader.mock'; +import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service.stub'; +import { RouterMock } from '../../../../shared/mocks/router.mock'; +import { PaginationService } from '../../../../core/pagination/pagination.service'; +import { PaginationServiceStub } from '../../../../shared/testing/pagination-service.stub'; + +describe('ReviewersListComponent', () => { + let component: ReviewersListComponent; + 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; + let paginationService; + + beforeEach(waitForAsync(() => { + 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$(buildPaginatedList(new PageInfo(), groupsDataServiceStub.getEPersonMembers())); + }, + searchByScope(scope: string, query: string): Observable>> { + if (query === '') { + return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), allEPersons)); + } + return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])); + }, + clearEPersonRequests() { + // empty + }, + clearLinkRequests() { + // empty + }, + getEPeoplePageRouterLink(): string { + return '/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$(buildPaginatedList(new PageInfo(), this.allGroups)); + } + return createSuccessfulRemoteDataObject$(buildPaginatedList(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 '/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')); + }, + findById(id: string) { + for (const group of allGroups) { + if (group.id === id) { + console.log('found', group); + return createSuccessfulRemoteDataObject$(group); + } + } + return createNoContentRemoteDataObject$(); + }, + editGroup() { + // empty + } + }; + builderService = getMockFormBuilderService(); + translateService = getMockTranslateService(); + + paginationService = new PaginationServiceStub(); + TestBed.configureTestingModule({ + imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock + } + }), + ], + declarations: [ReviewersListComponent], + providers: [ReviewersListComponent, + { provide: EPersonDataService, useValue: ePersonDataServiceStub }, + { provide: GroupDataService, useValue: groupsDataServiceStub }, + { provide: NotificationsService, useValue: new NotificationsServiceStub() }, + { provide: FormBuilderService, useValue: builderService }, + { provide: Router, useValue: new RouterMock() }, + { provide: PaginationService, useValue: paginationService }, + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ReviewersListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + afterEach(fakeAsync(() => { + fixture.destroy(); + flush(); + component = null; + fixture.debugElement.nativeElement.remove(); + })); + + it('should create ReviewersListComponent', inject([ReviewersListComponent], (comp: ReviewersListComponent) => { + expect(comp).toBeDefined(); + })); + + describe('when no group is selected', () => { + beforeEach(() => { + component.ngOnChanges({ + groupId: new SimpleChange(undefined, null, true) + }); + fixture.detectChanges(); + }); + + it('should show no epersons because no group is selected', () => { + const epersonIdsFound = fixture.debugElement.queryAll(By.css('#ePeopleMembersOfGroup tr td:first-child')); + expect(epersonIdsFound.length).toEqual(0); + epersonMembers.map((eperson: EPerson) => { + expect(epersonIdsFound.find((foundEl) => { + return (foundEl.nativeElement.textContent.trim() === eperson.uuid); + })).not.toBeTruthy(); + }); + }); + }); + + describe('when group is selected', () => { + beforeEach(() => { + component.ngOnChanges({ + groupId: new SimpleChange(undefined, GroupMock.id, true) + }); + fixture.detectChanges(); + }); + + it('should show all eperson members of 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(); + }); + }); + }); + +}); diff --git a/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-select-reviewer/reviewers-list/reviewers-list.component.ts b/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-select-reviewer/reviewers-list/reviewers-list.component.ts new file mode 100644 index 0000000000..14159450ff --- /dev/null +++ b/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-select-reviewer/reviewers-list/reviewers-list.component.ts @@ -0,0 +1,124 @@ +import { Component, OnDestroy, OnInit, Input, OnChanges, SimpleChanges, EventEmitter, Output } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; +import { Router } from '@angular/router'; +import { TranslateService } from '@ngx-translate/core'; +import { EPersonDataService } from '../../../../core/eperson/eperson-data.service'; +import { GroupDataService } from '../../../../core/eperson/group-data.service'; +import { NotificationsService } from '../../../../shared/notifications/notifications.service'; +import { PaginationService } from '../../../../core/pagination/pagination.service'; +import { Group } from '../../../../core/eperson/models/group.model'; +import { getFirstSucceededRemoteDataPayload } from '../../../../core/shared/operators'; +import { + EPersonListComponent, + EPersonListActionConfig +} from '../../../../access-control/group-registry/group-form/eperson-list/eperson-list.component'; +import { EpersonDtoModel } from '../../../../core/eperson/models/eperson-dto.model'; +import { EPerson } from '../../../../core/eperson/models/eperson.model'; +import { Observable, of as observableOf } from 'rxjs'; +import { hasValue } from '../../../../shared/empty.util'; +import { PaginatedList } from '../../../../core/data/paginated-list.model'; + +/** + * Keys to keep track of specific subscriptions + */ +enum SubKey { + ActiveGroup, + MembersDTO, + SearchResultsDTO, +} + +@Component({ + selector: 'ds-reviewers-list', + // templateUrl: './reviewers-list.component.html', + templateUrl: '../../../../access-control/group-registry/group-form/eperson-list/eperson-list.component.html', +}) +export class ReviewersListComponent extends EPersonListComponent implements OnInit, OnChanges, OnDestroy { + + @Input() + groupId: string | null; + + @Input() + actionConfig: EPersonListActionConfig; + + @Input() + multipleReviewers: boolean; + + @Output() + selectedReviewersUpdated: EventEmitter = new EventEmitter(); + + selectedReviewers: EpersonDtoModel[] = []; + + constructor(protected groupService: GroupDataService, + public ePersonDataService: EPersonDataService, + translateService: TranslateService, + notificationsService: NotificationsService, + formBuilder: FormBuilder, + paginationService: PaginationService, + router: Router) { + super(groupService, ePersonDataService, translateService, notificationsService, formBuilder, paginationService, router); + } + + ngOnInit() { + this.searchForm = this.formBuilder.group(({ + scope: 'metadata', + query: '', + })); + } + + ngOnChanges(changes: SimpleChanges): void { + this.groupId = changes.groupId.currentValue; + if (changes.groupId.currentValue !== changes.groupId.previousValue) { + if (this.groupId === null) { + this.retrieveMembers(this.config.currentPage); + } else { + this.subs.set(SubKey.ActiveGroup, this.groupService.findById(this.groupId).pipe( + getFirstSucceededRemoteDataPayload(), + ).subscribe((activeGroup: Group) => { + if (activeGroup != null) { + this.groupDataService.editGroup(activeGroup); + this.groupBeingEdited = activeGroup; + this.retrieveMembers(this.config.currentPage); + } + })); + } + } + } + + retrieveMembers(page: number): void { + this.config.currentPage = page; + if (this.groupId === null) { + this.unsubFrom(SubKey.MembersDTO); + const paginatedListOfDTOs: PaginatedList = new PaginatedList(); + paginatedListOfDTOs.page = this.selectedReviewers; + this.ePeopleMembersOfGroupDtos.next(paginatedListOfDTOs); + } else { + super.retrieveMembers(page); + } + } + + isMemberOfGroup(possibleMember: EPerson): Observable { + return observableOf(hasValue(this.selectedReviewers.find((reviewer: EpersonDtoModel) => reviewer.eperson.id === possibleMember.id))); + } + + deleteMemberFromGroup(ePerson: EpersonDtoModel) { + ePerson.memberOfGroup = false; + const index = this.selectedReviewers.indexOf(ePerson); + if (index !== -1) { + this.selectedReviewers.splice(index, 1); + } + this.selectedReviewersUpdated.emit(this.selectedReviewers.map((epersonDtoModel: EpersonDtoModel) => epersonDtoModel.eperson)); + } + + addMemberToGroup(ePerson: EpersonDtoModel) { + ePerson.memberOfGroup = true; + if (!this.multipleReviewers) { + for (const selectedReviewer of this.selectedReviewers) { + selectedReviewer.memberOfGroup = false; + } + this.selectedReviewers = []; + } + this.selectedReviewers.push(ePerson); + this.selectedReviewersUpdated.emit(this.selectedReviewers.map((epersonDtoModel: EpersonDtoModel) => epersonDtoModel.eperson)); + } + +} diff --git a/src/app/workflowitems-edit-page/workflowitems-edit-page.module.ts b/src/app/workflowitems-edit-page/workflowitems-edit-page.module.ts index b230ffe65c..080ca96468 100644 --- a/src/app/workflowitems-edit-page/workflowitems-edit-page.module.ts +++ b/src/app/workflowitems-edit-page/workflowitems-edit-page.module.ts @@ -18,16 +18,21 @@ import { AdvancedWorkflowActionPageComponent } from './advanced-workflow-action/ import { AdvancedClaimedTaskActionsDirective } from './advanced-workflow-action/advanced-workflow-actions-loader/advanced-claimed-task-actions.directive'; +import { AccessControlModule } from '../access-control/access-control.module'; +import { + ReviewersListComponent +} from './advanced-workflow-action/advanced-workflow-action-select-reviewer/reviewers-list/reviewers-list.component'; @NgModule({ - imports: [ - WorkflowItemsEditPageRoutingModule, - CommonModule, - SharedModule, - SubmissionModule, - StatisticsModule, - ItemPageModule - ], + imports: [ + WorkflowItemsEditPageRoutingModule, + CommonModule, + SharedModule, + SubmissionModule, + StatisticsModule, + ItemPageModule, + AccessControlModule + ], declarations: [ WorkflowItemDeleteComponent, ThemedWorkflowItemDeleteComponent, @@ -38,6 +43,7 @@ import { AdvancedWorkflowActionSelectReviewerComponent, AdvancedWorkflowActionPageComponent, AdvancedClaimedTaskActionsDirective, + ReviewersListComponent, ] }) /** diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index b410a26ac1..9d455fb422 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -539,6 +539,54 @@ "admin.metadata-import.page.error.addFile": "Select file first!", + "advanced-workflow-action.select-reviewer.description-single": "Please select a single reviewer below before submitting", + + "advanced-workflow-action.select-reviewer.description-multiple": "Please select one or more reviewers below before submitting", + + + "advanced-workflow-action-select-reviewer.groups.form.reviewers-list.head": "EPeople", + + "advanced-workflow-action-select-reviewer.groups.form.reviewers-list.search.head": "Add EPeople", + + "advanced-workflow-action-select-reviewer.groups.form.reviewers-list.button.see-all": "Browse All", + + "advanced-workflow-action-select-reviewer.groups.form.reviewers-list.headMembers": "Current Members", + + "advanced-workflow-action-select-reviewer.groups.form.reviewers-list.search.scope.metadata": "Metadata", + + "advanced-workflow-action-select-reviewer.groups.form.reviewers-list.search.scope.email": "E-mail (exact)", + + "advanced-workflow-action-select-reviewer.groups.form.reviewers-list.search.button": "Search", + + "advanced-workflow-action-select-reviewer.groups.form.reviewers-list.table.id": "ID", + + "advanced-workflow-action-select-reviewer.groups.form.reviewers-list.table.name": "Name", + + "advanced-workflow-action-select-reviewer.groups.form.reviewers-list.table.identity": "Identity", + + "advanced-workflow-action-select-reviewer.groups.form.reviewers-list.table.email": "Email", + + "advanced-workflow-action-select-reviewer.groups.form.reviewers-list.table.netid": "NetID", + + "advanced-workflow-action-select-reviewer.groups.form.reviewers-list.table.edit": "Remove / Add", + + "advanced-workflow-action-select-reviewer.groups.form.reviewers-list.table.edit.buttons.remove": "Remove member with name \"{{name}}\"", + + "advanced-workflow-action-select-reviewer.groups.form.reviewers-list.notification.success.addMember": "Successfully added member: \"{{name}}\"", + + "advanced-workflow-action-select-reviewer.groups.form.reviewers-list.notification.failure.addMember": "Failed to add member: \"{{name}}\"", + + "advanced-workflow-action-select-reviewer.groups.form.reviewers-list.notification.success.deleteMember": "Successfully deleted member: \"{{name}}\"", + + "advanced-workflow-action-select-reviewer.groups.form.reviewers-list.notification.failure.deleteMember": "Failed to delete member: \"{{name}}\"", + + "advanced-workflow-action-select-reviewer.groups.form.reviewers-list.table.edit.buttons.add": "Add member with name \"{{name}}\"", + + "advanced-workflow-action-select-reviewer.groups.form.reviewers-list.notification.failure.noActiveGroup": "No current active group, submit a name first.", + + "advanced-workflow-action-select-reviewer.groups.form.reviewers-list.no-members-yet": "No members in group yet, search and add.", + + "advanced-workflow-action-select-reviewer.groups.form.reviewers-list.no-items": "No EPeople found in that search", "auth.errors.invalid-user": "Invalid email address or password.",