115215: Fixed advanced workflow options (select reviewer & rating) throwing 404 error

This commit is contained in:
Alexandre Vryghem
2024-05-18 21:02:50 +02:00
parent 659052fcc0
commit 2d957bfa37
10 changed files with 138 additions and 70 deletions

View File

@@ -20,25 +20,33 @@
</tr>
</thead>
<tbody>
<tr *ngFor="let eperson of (ePeopleMembersOfGroup | async)?.page">
<td class="align-middle">{{eperson.id}}</td>
<tr *ngFor="let epersonDTO of (ePeopleMembersOfGroup | async)?.page">
<td class="align-middle">{{epersonDTO.eperson.id}}</td>
<td class="align-middle">
<a [routerLink]="getEPersonEditRoute(eperson.id)">
{{ dsoNameService.getName(eperson) }}
<a [routerLink]="getEPersonEditRoute(epersonDTO.eperson.id)">
{{ dsoNameService.getName(epersonDTO.eperson) }}
</a>
</td>
<td class="align-middle">
{{messagePrefix + '.table.email' | translate}}: {{ eperson.email ? eperson.email : '-' }}<br/>
{{messagePrefix + '.table.netid' | translate}}: {{ eperson.netid ? eperson.netid : '-' }}
{{messagePrefix + '.table.email' | translate}}: {{ epersonDTO.eperson.email ? epersonDTO.eperson.email : '-' }}<br/>
{{messagePrefix + '.table.netid' | translate}}: {{ epersonDTO.eperson.netid ? epersonDTO.eperson.netid : '-' }}
</td>
<td class="align-middle">
<div class="btn-group edit-field">
<button (click)="deleteMemberFromGroup(eperson)"
<button (click)="deleteMemberFromGroup(epersonDTO.eperson)"
*ngIf="epersonDTO.ableToDelete"
[disabled]="actionConfig.remove.disabled"
[ngClass]="['btn btn-sm', actionConfig.remove.css]"
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: { name: dsoNameService.getName(eperson) } }}">
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: { name: dsoNameService.getName(epersonDTO.eperson) } }}">
<i [ngClass]="actionConfig.remove.icon"></i>
</button>
<button *ngIf="!epersonDTO.ableToDelete"
(click)="addMemberToGroup(epersonDTO.eperson)"
[disabled]="actionConfig.add.disabled"
[ngClass]="['btn btn-sm', actionConfig.add.css]"
title="{{messagePrefix + '.table.edit.buttons.add' | translate: { name: dsoNameService.getName(epersonDTO.eperson) } }}">
<i [ngClass]="actionConfig.add.icon"></i>
</button>
</div>
</td>
</tr>

View File

@@ -222,13 +222,13 @@ describe('MembersListComponent', () => {
describe('if first delete button is pressed', () => {
beforeEach(() => {
spyOn(component, 'search').and.callThrough();
const deleteButton: DebugElement = fixture.debugElement.query(By.css('#ePeopleMembersOfGroup tbody .fa-trash-alt'));
deleteButton.nativeElement.click();
fixture.detectChanges();
});
it('then no ePerson remains as a member of the active group.', () => {
const epersonsFound = fixture.debugElement.queryAll(By.css('#ePeopleMembersOfGroup tbody tr'));
expect(epersonsFound.length).toEqual(0);
it('should trigger the search to add the user back to the search table', () => {
expect(component.search).toHaveBeenCalled();
});
});
});
@@ -264,13 +264,13 @@ describe('MembersListComponent', () => {
describe('if first add button is pressed', () => {
beforeEach(() => {
spyOn(component, 'search').and.callThrough();
const addButton: DebugElement = fixture.debugElement.query(By.css('#epersonsSearch tbody .fa-plus'));
addButton.nativeElement.click();
fixture.detectChanges();
});
it('then all (two) ePersons are member of the active group. No non-members left', () => {
epersonsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr'));
expect(epersonsFound.length).toEqual(0);
it('should trigger the search to remove the user from the search table', () => {
expect(component.search).toHaveBeenCalled();
});
});
});

View File

@@ -24,21 +24,29 @@ import {
} from '@ngx-translate/core';
import {
BehaviorSubject,
combineLatest as observableCombineLatest,
Observable,
ObservedValueOf,
of as observableOf,
Subscription,
} from 'rxjs';
import {
defaultIfEmpty,
map,
switchMap,
take,
} from 'rxjs/operators';
import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
import { PaginatedList } from '../../../../core/data/paginated-list.model';
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 { EpersonDtoModel } from '../../../../core/eperson/models/eperson-dto.model';
import { Group } from '../../../../core/eperson/models/group.model';
import { PaginationService } from '../../../../core/pagination/pagination.service';
import {
@@ -137,7 +145,7 @@ export class MembersListComponent implements OnInit, OnDestroy {
/**
* List of EPeople members of currently active group being edited
*/
ePeopleMembersOfGroup: BehaviorSubject<PaginatedList<EPerson>> = new BehaviorSubject<PaginatedList<EPerson>>(undefined);
ePeopleMembersOfGroup: BehaviorSubject<PaginatedList<EpersonDtoModel>> = new BehaviorSubject(undefined);
/**
* Pagination config used to display the list of EPeople that are result of EPeople search
@@ -226,10 +234,35 @@ export class MembersListComponent implements OnInit, OnDestroy {
return rd;
}
}),
getRemoteDataPayload())
.subscribe((paginatedListOfEPersons: PaginatedList<EPerson>) => {
this.ePeopleMembersOfGroup.next(paginatedListOfEPersons);
}));
switchMap((epersonListRD: RemoteData<PaginatedList<EPerson>>) => {
const dtos$ = observableCombineLatest([...epersonListRD.payload.page.map((member: EPerson) => {
const dto$: Observable<EpersonDtoModel> = observableCombineLatest(
this.isMemberOfGroup(member), (isMember: ObservedValueOf<Observable<boolean>>) => {
const epersonDtoModel: EpersonDtoModel = new EpersonDtoModel();
epersonDtoModel.eperson = member;
epersonDtoModel.ableToDelete = isMember;
return epersonDtoModel;
});
return dto$;
})]);
return dtos$.pipe(defaultIfEmpty([]), map((dtos: EpersonDtoModel[]) => {
return buildPaginatedList(epersonListRD.payload.pageInfo, dtos);
}));
}),
).subscribe((paginatedListOfDTOs: PaginatedList<EpersonDtoModel>) => {
this.ePeopleMembersOfGroup.next(paginatedListOfDTOs);
}),
);
}
/**
* We always return true since this is only used by the top section (which represents all the users part of the group
* in {@link MembersListComponent})
*
* @param possibleMember EPerson that is a possible member (being tested) of the group currently being edited
*/
isMemberOfGroup(possibleMember: EPerson): Observable<boolean> {
return observableOf(true);
}
/**

View File

@@ -107,13 +107,17 @@ export class EPersonDataService extends IdentifiableDataService<EPerson> impleme
* @param scope Scope of the EPeople search, default byMetadata
* @param query Query of search
* @param options Options of search request
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
* no valid cached version. Defaults to true
* @param reRequestOnStale Whether or not the request should automatically be re-
* requested after the response becomes stale
*/
public searchByScope(scope: string, query: string, options: FindListOptions = {}, useCachedVersionIfAvailable?: boolean): Observable<RemoteData<PaginatedList<EPerson>>> {
public searchByScope(scope: string, query: string, options: FindListOptions = {}, useCachedVersionIfAvailable?: boolean, reRequestOnStale = true): Observable<RemoteData<PaginatedList<EPerson>>> {
switch (scope) {
case 'metadata':
return this.getEpeopleByMetadata(query.trim(), options, useCachedVersionIfAvailable);
return this.getEpeopleByMetadata(query.trim(), options, useCachedVersionIfAvailable, reRequestOnStale);
case 'email':
return this.getEPersonByEmail(query.trim()).pipe(
return this.getEPersonByEmail(query.trim(), useCachedVersionIfAvailable, reRequestOnStale).pipe(
map((rd: RemoteData<EPerson | NoContent>) => {
if (rd.hasSucceeded) {
// Turn the single EPerson or NoContent in to a PaginatedList<EPerson>
@@ -145,7 +149,7 @@ export class EPersonDataService extends IdentifiableDataService<EPerson> impleme
}),
);
default:
return this.getEpeopleByMetadata(query.trim(), options, useCachedVersionIfAvailable);
return this.getEpeopleByMetadata(query.trim(), options, useCachedVersionIfAvailable, reRequestOnStale);
}
}

View File

@@ -37,6 +37,7 @@ describe('ModifyItemOverviewComponent', () => {
fixture = TestBed.createComponent(ModifyItemOverviewComponent);
comp = fixture.componentInstance;
comp.item = mockItem;
comp.ngOnChanges();
fixture.detectChanges();
});

View File

@@ -5,7 +5,7 @@ import {
import {
Component,
Input,
OnInit,
OnChanges,
} from '@angular/core';
import { TranslateModule } from '@ngx-translate/core';
@@ -21,12 +21,12 @@ import { MetadataMap } from '../../../core/shared/metadata.models';
/**
* Component responsible for rendering a table containing the metadatavalues from the to be edited item
*/
export class ModifyItemOverviewComponent implements OnInit {
export class ModifyItemOverviewComponent implements OnChanges {
@Input() item: Item;
metadata: MetadataMap;
ngOnInit(): void {
this.metadata = this.item.metadata;
ngOnChanges(): void {
this.metadata = this.item?.metadata;
}
}

View File

@@ -1,6 +1,7 @@
import {
Component,
Injector,
Input,
OnDestroy,
} from '@angular/core';
import { Router } from '@angular/router';
@@ -44,7 +45,7 @@ export abstract class ClaimedTaskActionsAbstractComponent extends MyDSpaceReload
/**
* The item object that belonging to the ClaimedTask object
*/
item: Item;
@Input() item: Item;
/**
* Anchor used to reload the pool task.
@@ -56,7 +57,7 @@ export abstract class ClaimedTaskActionsAbstractComponent extends MyDSpaceReload
/**
* The workflowitem object that belonging to the ClaimedTask object
*/
workflowitem: WorkflowItem;
@Input() workflowitem: WorkflowItem;
protected constructor(protected injector: Injector,
protected router: Router,

View File

@@ -1,8 +1,10 @@
import {
ADVANCED_WORKFLOW_ACTION_RATING,
ADVANCED_WORKFLOW_TASK_OPTION_RATING,
AdvancedWorkflowActionRatingComponent,
} from '../../../../workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-rating/advanced-workflow-action-rating.component';
import {
ADVANCED_WORKFLOW_ACTION_SELECT_REVIEWER,
ADVANCED_WORKFLOW_TASK_OPTION_SELECT_REVIEWER,
AdvancedWorkflowActionSelectReviewerComponent,
} from '../../../../workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-select-reviewer/advanced-workflow-action-select-reviewer.component';
@@ -54,8 +56,8 @@ export const WORKFLOW_TASK_OPTION_DECORATOR_MAP = new Map<string, WorkflowTaskOp
]);
export const ADVANCED_WORKFLOW_TASK_OPTION_DECORATOR_MAP = new Map<string, AdvancedWorkflowTaskOptionComponent>([
[ADVANCED_WORKFLOW_TASK_OPTION_RATING, AdvancedWorkflowActionRatingComponent],
[ADVANCED_WORKFLOW_TASK_OPTION_SELECT_REVIEWER, AdvancedWorkflowActionSelectReviewerComponent],
[ADVANCED_WORKFLOW_ACTION_RATING, AdvancedWorkflowActionRatingComponent],
[ADVANCED_WORKFLOW_ACTION_SELECT_REVIEWER, AdvancedWorkflowActionSelectReviewerComponent],
]);
/**

View File

@@ -224,6 +224,38 @@ describe('ReviewersListComponent', () => {
})).not.toBeTruthy();
});
});
it('should replace the value when a new member is added when multipleReviewers is false', () => {
spyOn(component.selectedReviewersUpdated, 'emit');
component.multipleReviewers = false;
component.selectedReviewers = [EPersonMock];
component.addMemberToGroup(EPersonMock2);
expect(component.selectedReviewers).toEqual([EPersonMock2]);
expect(component.selectedReviewersUpdated.emit).toHaveBeenCalledWith([EPersonMock2]);
});
it('should add the value when a new member is added when multipleReviewers is true', () => {
spyOn(component.selectedReviewersUpdated, 'emit');
component.multipleReviewers = true;
component.selectedReviewers = [EPersonMock];
component.addMemberToGroup(EPersonMock2);
expect(component.selectedReviewers).toEqual([EPersonMock, EPersonMock2]);
expect(component.selectedReviewersUpdated.emit).toHaveBeenCalledWith([EPersonMock, EPersonMock2]);
});
it('should delete the member when present', () => {
spyOn(component.selectedReviewersUpdated, 'emit');
component.selectedReviewers = [EPersonMock];
component.deleteMemberFromGroup(EPersonMock);
expect(component.selectedReviewers).toEqual([]);
expect(component.selectedReviewersUpdated.emit).toHaveBeenCalledWith([]);
});
});
describe('when a group is selected', () => {
@@ -245,37 +277,4 @@ describe('ReviewersListComponent', () => {
});
});
it('should replace the value when a new member is added when multipleReviewers is false', () => {
spyOn(component.selectedReviewersUpdated, 'emit');
component.multipleReviewers = false;
component.selectedReviewers = [EPersonMock];
component.addMemberToGroup(EPersonMock2);
expect(component.selectedReviewers).toEqual([EPersonMock2]);
expect(component.selectedReviewersUpdated.emit).toHaveBeenCalledWith([EPersonMock2]);
});
it('should add the value when a new member is added when multipleReviewers is true', () => {
spyOn(component.selectedReviewersUpdated, 'emit');
component.multipleReviewers = true;
component.selectedReviewers = [EPersonMock];
component.addMemberToGroup(EPersonMock2);
expect(component.selectedReviewers).toEqual([EPersonMock, EPersonMock2]);
expect(component.selectedReviewersUpdated.emit).toHaveBeenCalledWith([EPersonMock, EPersonMock2]);
});
it('should delete the member when present', () => {
spyOn(component.selectedReviewersUpdated, 'emit');
component.selectedReviewers = [EPersonMock];
component.deleteMemberFromGroup(EPersonMock);
expect(component.selectedReviewers).toEqual([]);
expect(component.selectedReviewersUpdated.emit).toHaveBeenCalledWith([]);
});
});

View File

@@ -26,6 +26,10 @@ import {
TranslateModule,
TranslateService,
} from '@ngx-translate/core';
import {
Observable,
of as observableOf,
} from 'rxjs';
import {
EPersonListActionConfig,
@@ -36,10 +40,12 @@ import { PaginatedList } from '../../../../core/data/paginated-list.model';
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 { EpersonDtoModel } from '../../../../core/eperson/models/eperson-dto.model';
import { Group } from '../../../../core/eperson/models/group.model';
import { PaginationService } from '../../../../core/pagination/pagination.service';
import { getFirstSucceededRemoteDataPayload } from '../../../../core/shared/operators';
import { ContextHelpDirective } from '../../../../shared/context-help.directive';
import { hasValue } from '../../../../shared/empty.util';
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
import { PaginationComponent } from '../../../../shared/pagination/pagination.component';
@@ -101,7 +107,7 @@ export class ReviewersListComponent extends MembersListComponent implements OnIn
super(groupService, ePersonDataService, translateService, notificationsService, formBuilder, paginationService, router, dsoNameService);
}
ngOnInit() {
override ngOnInit(): void {
this.searchForm = this.formBuilder.group(({
scope: 'metadata',
query: '',
@@ -114,6 +120,7 @@ export class ReviewersListComponent extends MembersListComponent implements OnIn
if (this.groupId === null) {
this.retrieveMembers(this.config.currentPage);
} else {
this.unsubFrom(SubKey.ActiveGroup);
this.subs.set(SubKey.ActiveGroup, this.groupService.findById(this.groupId).pipe(
getFirstSucceededRemoteDataPayload(),
).subscribe((activeGroup: Group) => {
@@ -136,25 +143,37 @@ export class ReviewersListComponent extends MembersListComponent implements OnIn
retrieveMembers(page: number): void {
this.config.currentPage = page;
if (this.groupId === null) {
this.unsubFrom(SubKey.Members);
const paginatedListOfEPersons: PaginatedList<EPerson> = new PaginatedList();
paginatedListOfEPersons.page = this.selectedReviewers;
const paginatedListOfEPersons: PaginatedList<EpersonDtoModel> = new PaginatedList();
paginatedListOfEPersons.page = this.selectedReviewers.map((ePerson: EPerson) => Object.assign(new EpersonDtoModel(), {
eperson: ePerson,
ableToDelete: this.isMemberOfGroup(ePerson),
}));
this.ePeopleMembersOfGroup.next(paginatedListOfEPersons);
} else {
super.retrieveMembers(page);
}
}
/**
* Checks whether the given {@link possibleMember} is part of the {@link selectedReviewers}.
*
* @param possibleMember The {@link EPerson} that needs to be checked
*/
isMemberOfGroup(possibleMember: EPerson): Observable<boolean> {
return observableOf(hasValue(this.selectedReviewers.find((reviewer: EPerson) => reviewer.id === possibleMember.id)));
}
/**
* Removes the {@link eperson} from the {@link selectedReviewers}
*
* @param eperson The {@link EPerson} to remove
*/
deleteMemberFromGroup(eperson: EPerson) {
const index = this.selectedReviewers.indexOf(eperson);
const index = this.selectedReviewers.findIndex((reviewer: EPerson) => reviewer.id === eperson.id);
if (index !== -1) {
this.selectedReviewers.splice(index, 1);
}
this.retrieveMembers(this.config.currentPage);
this.selectedReviewersUpdated.emit(this.selectedReviewers);
}
@@ -169,6 +188,7 @@ export class ReviewersListComponent extends MembersListComponent implements OnIn
this.selectedReviewers = [];
}
this.selectedReviewers.push(eperson);
this.retrieveMembers(this.config.currentPage);
this.selectedReviewersUpdated.emit(this.selectedReviewers);
}