Merge remote-tracking branch 'upstream/main' into retrieve-name-with-dsonameservice-7.4

# Conflicts:
#	src/app/access-control/epeople-registry/epeople-registry.component.html
#	src/app/access-control/epeople-registry/eperson-form/eperson-form.component.ts
#	src/app/access-control/group-registry/group-form/group-form.component.spec.ts
#	src/app/access-control/group-registry/group-form/group-form.component.ts
#	src/app/access-control/group-registry/group-form/members-list/members-list.component.html
#	src/app/access-control/group-registry/group-form/members-list/members-list.component.ts
#	src/app/collection-page/edit-item-template-page/edit-item-template-page.component.html
#	src/app/item-page/full/field-components/file-section/full-file-section.component.ts
#	src/app/item-page/media-viewer/media-viewer-video/media-viewer-video.component.ts
#	src/app/item-page/simple/field-components/file-section/file-section.component.html
#	src/app/item-page/simple/field-components/file-section/file-section.component.ts
#	src/app/item-page/versions/item-versions.component.ts
#	src/app/shared/auth-nav-menu/user-menu/user-menu.component.html
#	src/app/shared/auth-nav-menu/user-menu/user-menu.component.ts
#	src/app/shared/object-collection/shared/mydspace-item-submitter/item-submitter.component.html
This commit is contained in:
Alexandre Vryghem
2023-03-02 21:15:00 +01:00
960 changed files with 41545 additions and 19578 deletions

View File

@@ -0,0 +1,5 @@
<div class="container">
<h2>{{'workflow-item.' + type + '.header' | translate}}</h2>
<ds-advanced-workflow-actions-loader [type]="type">
</ds-advanced-workflow-actions-loader>
</div>

View File

@@ -0,0 +1,42 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AdvancedWorkflowActionPageComponent } from './advanced-workflow-action-page.component';
import { ActivatedRoute } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
describe('AdvancedWorkflowActionPageComponent', () => {
let component: AdvancedWorkflowActionPageComponent;
let fixture: ComponentFixture<AdvancedWorkflowActionPageComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
TranslateModule.forRoot(),
],
declarations: [
AdvancedWorkflowActionPageComponent,
],
providers: [
{
provide: ActivatedRoute,
useValue: {
snapshot: {
queryParams: {
workflow: 'testaction',
},
},
},
},
],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(AdvancedWorkflowActionPageComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,26 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
/**
* The Advanced Workflow page containing the correct {@link AdvancedWorkflowActionComponent}
* based on the route parameters.
*/
@Component({
selector: 'ds-advanced-workflow-action-page',
templateUrl: './advanced-workflow-action-page.component.html',
styleUrls: ['./advanced-workflow-action-page.component.scss']
})
export class AdvancedWorkflowActionPageComponent implements OnInit {
public type: string;
constructor(
protected route: ActivatedRoute,
) {
}
ngOnInit(): void {
this.type = this.route.snapshot.queryParams.workflow;
}
}

View File

@@ -0,0 +1,50 @@
<div *ngVar="getAdvancedInfo(workflowAction$ | async) as advancedInfo">
<p *ngIf="advancedInfo?.descriptionRequired">
{{ 'advanced-workflow-action.rating.description-requiredDescription' | translate }}
</p>
<p *ngIf="!advancedInfo?.descriptionRequired">
{{ 'advanced-workflow-action.rating.description' | translate }}
</p>
<form (ngSubmit)="performAction()" *ngIf="ratingForm" [formGroup]="ratingForm">
<div class="form-group">
<label class="control-label">
<span>{{ 'advanced-workflow-action.rating.form.review.label' | translate }}</span>
<span *ngIf="advancedInfo?.descriptionRequired">*</span>
</label>
<textarea [ngClass]="{ 'is-invalid' : isInvalid('review') }"
[required]="advancedInfo?.descriptionRequired" class="form-control" formControlName="review">
</textarea>
<small *ngIf="isInvalid('review')" class="invalid-feedback d-block">
{{ 'advanced-workflow-action.rating.form.review.error' | translate }}
</small>
</div>
<div class="form-group">
<label class="control-label mb-1">
{{ 'advanced-workflow-action.rating.form.rating.label' | translate }}*
</label>
<div class="d-block">
<ngb-rating [max]="advancedInfo?.maxValue" [ngClass]="{ 'text-danger': isInvalid('rating') }"
formControlName="rating">
</ngb-rating>
</div>
<small *ngIf="isInvalid('rating')" class="invalid-feedback d-block">
{{ 'advanced-workflow-action.rating.form.rating.error' | translate }}
</small>
</div>
</form>
<ds-modify-item-overview *ngIf="item$ | async"
[item]="item$ | async">
</ds-modify-item-overview>
<div class="d-flex flex-wrap justify-content-end">
<button (click)="previousPage()" class="btn btn-default">
{{'workflow-item.' + getType() + '.button.cancel' | translate}}
</button>
<button (click)="performAction()" class="btn btn-primary">
{{'workflow-item.' + getType() + '.button.confirm' | translate}}
</button>
</div>
</div>

View File

@@ -0,0 +1,196 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Location } from '@angular/common';
import {
AdvancedWorkflowActionRatingComponent,
ADVANCED_WORKFLOW_TASK_OPTION_RATING
} from './advanced-workflow-action-rating.component';
import { ActivatedRoute, Router } from '@angular/router';
import { of as observableOf } from 'rxjs';
import { ClaimedTaskDataService } from '../../../core/tasks/claimed-task-data.service';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { RouteService } from '../../../core/services/route.service';
import { routeServiceStub } from '../../../shared/testing/route-service.stub';
import { WorkflowActionDataService } from '../../../core/data/workflow-action-data.service';
import { WorkflowItemDataService } from '../../../core/submission/workflowitem-data.service';
import { ClaimedTaskDataServiceStub } from '../../../shared/testing/claimed-task-data-service.stub';
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
import { WorkflowActionDataServiceStub } from '../../../shared/testing/workflow-action-data-service.stub';
import { WorkflowItemDataServiceStub } from '../../../shared/testing/workflow-item-data-service.stub';
import { RouterStub } from '../../../shared/testing/router.stub';
import { TranslateModule } from '@ngx-translate/core';
import { VarDirective } from '../../../shared/utils/var.directive';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { WorkflowItem } from '../../../core/submission/models/workflowitem.model';
import { createSuccessfulRemoteDataObject$, createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils';
import { Item } from '../../../core/shared/item.model';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { ProcessTaskResponse } from '../../../core/tasks/models/process-task-response';
import { RatingAdvancedWorkflowInfo } from '../../../core/tasks/models/rating-advanced-workflow-info.model';
import { RequestService } from '../../../core/data/request.service';
import { RequestServiceStub } from '../../../shared/testing/request-service.stub';
import { LocationStub } from '../../../shared/testing/location.stub';
const claimedTaskId = '2';
const workflowId = '1';
describe('AdvancedWorkflowActionRatingComponent', () => {
const workflowItem: WorkflowItem = new WorkflowItem();
workflowItem.item = createSuccessfulRemoteDataObject$(new Item());
let component: AdvancedWorkflowActionRatingComponent;
let fixture: ComponentFixture<AdvancedWorkflowActionRatingComponent>;
let claimedTaskDataService: ClaimedTaskDataServiceStub;
let notificationService: NotificationsServiceStub;
let workflowActionDataService: WorkflowItemDataServiceStub;
let workflowItemDataService: WorkflowItemDataServiceStub;
beforeEach(async () => {
claimedTaskDataService = new ClaimedTaskDataServiceStub();
notificationService = new NotificationsServiceStub();
workflowActionDataService = new WorkflowActionDataServiceStub();
workflowItemDataService = new WorkflowItemDataServiceStub();
await TestBed.configureTestingModule({
imports: [
FormsModule,
NgbModule,
ReactiveFormsModule,
TranslateModule.forRoot(),
],
declarations: [
AdvancedWorkflowActionRatingComponent,
VarDirective,
],
providers: [
{
provide: ActivatedRoute,
useValue: {
data: observableOf({
id: workflowId,
wfi: createSuccessfulRemoteDataObject(workflowItem),
}),
snapshot: {
queryParams: {
claimedTask: claimedTaskId,
workflow: 'testaction',
},
},
},
},
{ provide: ClaimedTaskDataService, useValue: claimedTaskDataService },
{ provide: Location, useValue: new LocationStub() },
{ provide: NotificationsService, useValue: notificationService },
{ provide: RouteService, useValue: routeServiceStub },
{ provide: Router, useValue: new RouterStub() },
{ provide: WorkflowActionDataService, useValue: workflowActionDataService },
{ provide: WorkflowItemDataService, useValue: workflowItemDataService },
{ provide: RequestService, useClass: RequestServiceStub },
],
schemas: [NO_ERRORS_SCHEMA],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(AdvancedWorkflowActionRatingComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
afterEach(() => {
fixture.debugElement.nativeElement.remove();
});
describe('performAction', () => {
let ratingAdvancedWorkflowInfo: RatingAdvancedWorkflowInfo;
beforeEach(() => {
ratingAdvancedWorkflowInfo = new RatingAdvancedWorkflowInfo();
ratingAdvancedWorkflowInfo.maxValue = 5;
spyOn(component, 'getAdvancedInfo').and.returnValue(ratingAdvancedWorkflowInfo);
spyOn(component, 'previousPage');
// The form validators are set in the HTML code so the getAdvancedInfo needs to return a value
fixture.detectChanges();
});
describe('with required review', () => {
beforeEach(() => {
ratingAdvancedWorkflowInfo.descriptionRequired = true;
fixture.detectChanges();
});
it('should call the claimedTaskDataService with the rating and the required description when it has been rated and return to the mydspace page', () => {
spyOn(claimedTaskDataService, 'submitTask').and.returnValue(observableOf(new ProcessTaskResponse(true)));
component.ratingForm.setValue({
review: 'Good job!',
rating: 4,
});
component.performAction();
expect(claimedTaskDataService.submitTask).toHaveBeenCalledWith(claimedTaskId, {
[ADVANCED_WORKFLOW_TASK_OPTION_RATING]: true,
review: 'Good job!',
score: 4,
});
expect(notificationService.success).toHaveBeenCalled();
expect(component.previousPage).toHaveBeenCalled();
});
it('should not call the claimedTaskDataService when the required description is empty', () => {
spyOn(claimedTaskDataService, 'submitTask').and.returnValue(observableOf(new ProcessTaskResponse(true)));
component.ratingForm.setValue({
review: '',
rating: 4,
});
component.performAction();
expect(claimedTaskDataService.submitTask).not.toHaveBeenCalled();
expect(notificationService.success).not.toHaveBeenCalled();
expect(component.previousPage).not.toHaveBeenCalled();
});
});
describe('with an optional review', () => {
beforeEach(() => {
ratingAdvancedWorkflowInfo.descriptionRequired = false;
fixture.detectChanges();
});
it('should call the claimedTaskDataService with the optional review when provided and return to the mydspace page', () => {
spyOn(claimedTaskDataService, 'submitTask').and.returnValue(observableOf(new ProcessTaskResponse(true)));
component.ratingForm.setValue({
review: 'Good job!',
rating: 4,
});
component.performAction();
expect(claimedTaskDataService.submitTask).toHaveBeenCalledWith(claimedTaskId, {
[ADVANCED_WORKFLOW_TASK_OPTION_RATING]: true,
review: 'Good job!',
score: 4,
});
expect(notificationService.success).toHaveBeenCalled();
expect(component.previousPage).toHaveBeenCalled();
});
it('should call the claimedTaskDataService when the optional description is empty and return to the mydspace page', () => {
spyOn(claimedTaskDataService, 'submitTask').and.returnValue(observableOf(new ProcessTaskResponse(true)));
component.ratingForm.setValue({
review: '',
rating: 4,
});
component.performAction();
expect(claimedTaskDataService.submitTask).toHaveBeenCalledWith(claimedTaskId, {
[ADVANCED_WORKFLOW_TASK_OPTION_RATING]: true,
score: 4,
});
expect(notificationService.success).toHaveBeenCalled();
expect(component.previousPage).toHaveBeenCalled();
});
});
});
});

View File

@@ -0,0 +1,79 @@
import { Component, OnInit } 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 { FormGroup, FormControl, Validators } from '@angular/forms';
import { WorkflowAction } from '../../../core/tasks/models/workflow-action-object.model';
import { RatingAdvancedWorkflowInfo } from '../../../core/tasks/models/rating-advanced-workflow-info.model';
export const ADVANCED_WORKFLOW_TASK_OPTION_RATING = 'submit_score';
export const ADVANCED_WORKFLOW_ACTION_RATING = 'scorereviewaction';
/**
* The page on which reviewers can rate submitted items.
*/
@rendersAdvancedWorkflowTaskOption(ADVANCED_WORKFLOW_ACTION_RATING)
@Component({
selector: 'ds-advanced-workflow-action-rating-reviewer',
templateUrl: './advanced-workflow-action-rating.component.html',
styleUrls: ['./advanced-workflow-action-rating.component.scss'],
preserveWhitespaces: false,
})
export class AdvancedWorkflowActionRatingComponent extends AdvancedWorkflowActionComponent implements OnInit {
ratingForm: FormGroup;
ngOnInit(): void {
super.ngOnInit();
this.ratingForm = new FormGroup({
review: new FormControl(''),
rating: new FormControl(0, Validators.min(1)),
});
}
/**
* Only run **performAction()** when the form has been correctly filled in
*/
performAction(): void {
this.ratingForm.updateValueAndValidity();
if (this.ratingForm.valid) {
super.performAction();
} else {
this.ratingForm.markAllAsTouched();
}
}
/**
* Returns the task option, the score and the review if one was provided
*/
createBody(): any {
const body = {
[ADVANCED_WORKFLOW_TASK_OPTION_RATING]: true,
score: this.ratingForm.get('rating').value,
};
if (this.ratingForm.get('review').value !== '') {
const review: string = this.ratingForm.get('review').value;
Object.assign(body, { review: review });
}
return body;
}
getType(): string {
return ADVANCED_WORKFLOW_ACTION_RATING;
}
getAdvancedInfo(workflowAction: WorkflowAction | null): RatingAdvancedWorkflowInfo | null {
return workflowAction ? (workflowAction.advancedInfo[0] as RatingAdvancedWorkflowInfo) : null;
}
/**
* Returns whether the field is valid or not.
*
* @param formControlName The input field
*/
isInvalid(formControlName: string): boolean {
return this.ratingForm.get(formControlName).touched && !this.ratingForm.get(formControlName).valid;
}
}

View File

@@ -0,0 +1,29 @@
<div>
<p *ngIf="multipleReviewers">{{ 'advanced-workflow-action.select-reviewer.description-multiple' | translate }}</p>
<p *ngIf="!multipleReviewers">{{ 'advanced-workflow-action.select-reviewer.description-single' | translate }}</p>
<ds-reviewers-list *ngIf="groupId !== undefined"
[actionConfig]="reviewersListActionConfig"
[groupId]="groupId"
[ngClass]="groupId ? 'reviewersListWithGroup' : ''"
[multipleReviewers]="multipleReviewers"
(selectedReviewersUpdated)="selectedReviewers = $event; displayError = false"
messagePrefix="advanced-workflow-action-select-reviewer.groups.form.reviewers-list"
></ds-reviewers-list>
<small *ngIf="displayError" class="invalid-feedback d-block mb-3">
{{ 'advanced-workflow-action.select-reviewer.no-reviewer-selected.error' | translate }}
</small>
<ds-modify-item-overview *ngIf="item$ | async"
[item]="item$ | async">
</ds-modify-item-overview>
<div class="d-flex flex-wrap justify-content-end">
<button class="btn btn-default" (click)="previousPage()">
{{'workflow-item.' + getType() + '.button.cancel' | translate}}
</button>
<button class="btn btn-primary" (click)="performAction()">
{{'workflow-item.' + getType() + '.button.confirm' | translate}}
</button>
</div>
</div>

View File

@@ -0,0 +1,7 @@
:host ::ng-deep {
.reviewersListWithGroup {
#search, #search + form, #search + form + ds-pagination {
display: none !important;
}
}
}

View File

@@ -0,0 +1,165 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Location } from '@angular/common';
import {
AdvancedWorkflowActionSelectReviewerComponent,
ADVANCED_WORKFLOW_TASK_OPTION_SELECT_REVIEWER,
} from './advanced-workflow-action-select-reviewer.component';
import { ActivatedRoute, Router } from '@angular/router';
import { WorkflowItemDataService } from '../../../core/submission/workflowitem-data.service';
import { WorkflowItemDataServiceStub } from '../../../shared/testing/workflow-item-data-service.stub';
import { WorkflowActionDataServiceStub } from '../../../shared/testing/workflow-action-data-service.stub';
import { WorkflowActionDataService } from '../../../core/data/workflow-action-data.service';
import { RouteService } from '../../../core/services/route.service';
import { routeServiceStub } from '../../../shared/testing/route-service.stub';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
import { TranslateModule } from '@ngx-translate/core';
import { ClaimedTaskDataService } from '../../../core/tasks/claimed-task-data.service';
import { ClaimedTaskDataServiceStub } from '../../../shared/testing/claimed-task-data-service.stub';
import { of as observableOf } from 'rxjs';
import { WorkflowItem } from '../../../core/submission/models/workflowitem.model';
import { createSuccessfulRemoteDataObject$, createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils';
import { Item } from '../../../core/shared/item.model';
import { EPersonMock, EPersonMock2 } from '../../../shared/testing/eperson.mock';
import { ProcessTaskResponse } from '../../../core/tasks/models/process-task-response';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { RequestService } from '../../../core/data/request.service';
import { RequestServiceStub } from '../../../shared/testing/request-service.stub';
import { RouterStub } from '../../../shared/testing/router.stub';
import { LocationStub } from '../../../shared/testing/location.stub';
const claimedTaskId = '2';
const workflowId = '1';
describe('AdvancedWorkflowActionSelectReviewerComponent', () => {
const workflowItem: WorkflowItem = new WorkflowItem();
workflowItem.item = createSuccessfulRemoteDataObject$(new Item());
let component: AdvancedWorkflowActionSelectReviewerComponent;
let fixture: ComponentFixture<AdvancedWorkflowActionSelectReviewerComponent>;
let claimedTaskDataService: ClaimedTaskDataServiceStub;
let location: LocationStub;
let notificationService: NotificationsServiceStub;
let router: RouterStub;
let workflowActionDataService: WorkflowItemDataServiceStub;
let workflowItemDataService: WorkflowItemDataServiceStub;
beforeEach(async () => {
claimedTaskDataService = new ClaimedTaskDataServiceStub();
location = new LocationStub();
notificationService = new NotificationsServiceStub();
router = new RouterStub();
workflowActionDataService = new WorkflowActionDataServiceStub();
workflowItemDataService = new WorkflowItemDataServiceStub();
await TestBed.configureTestingModule({
imports: [
TranslateModule.forRoot(),
],
declarations: [
AdvancedWorkflowActionSelectReviewerComponent,
],
providers: [
{
provide: ActivatedRoute,
useValue: {
data: observableOf({
id: workflowId,
wfi: createSuccessfulRemoteDataObject(workflowItem),
}),
snapshot: {
queryParams: {
claimedTask: claimedTaskId,
workflow: 'testaction',
previousSearchQuery: 'Thor%20Love%20and%20Thunder',
},
},
},
},
{ provide: ClaimedTaskDataService, useValue: claimedTaskDataService },
{ provide: Location, useValue: location },
{ provide: NotificationsService, useValue: notificationService },
{ provide: Router, useValue: router },
{ provide: RouteService, useValue: routeServiceStub },
{ provide: WorkflowActionDataService, useValue: workflowActionDataService },
{ provide: WorkflowItemDataService, useValue: workflowItemDataService },
{ provide: RequestService, useClass: RequestServiceStub },
],
schemas: [NO_ERRORS_SCHEMA],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(AdvancedWorkflowActionSelectReviewerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
afterEach(() => {
fixture.debugElement.nativeElement.remove();
});
describe('previousPage', () => {
it('should navigate back to the Workflow tasks page with the previous query', () => {
spyOn(location, 'getState').and.returnValue({
previousQueryParams: {
configuration: 'workflow',
query: 'Thor Love and Thunder',
},
});
component.ngOnInit();
component.previousPage();
expect(router.navigate).toHaveBeenCalledWith(['/mydspace'], {
queryParams: {
configuration: 'workflow',
query: 'Thor Love and Thunder',
},
});
});
});
describe('performAction', () => {
beforeEach(() => {
spyOn(component, 'previousPage');
});
it('should call the claimedTaskDataService with the list of selected ePersons', () => {
spyOn(claimedTaskDataService, 'submitTask').and.returnValue(observableOf(new ProcessTaskResponse(true)));
component.selectedReviewers = [EPersonMock, EPersonMock2];
component.performAction();
expect(claimedTaskDataService.submitTask).toHaveBeenCalledWith(claimedTaskId, {
[ADVANCED_WORKFLOW_TASK_OPTION_SELECT_REVIEWER]: true,
eperson: [EPersonMock.id, EPersonMock2.id],
});
expect(notificationService.success).toHaveBeenCalled();
expect(component.previousPage).toHaveBeenCalled();
});
it('should not call the claimedTaskDataService with the list of selected ePersons when it\'s empty', () => {
spyOn(claimedTaskDataService, 'submitTask').and.returnValue(observableOf(new ProcessTaskResponse(true)));
component.selectedReviewers = [];
component.performAction();
expect(claimedTaskDataService.submitTask).not.toHaveBeenCalled();
});
it('should not call the return to mydspace page when the request failed', () => {
spyOn(claimedTaskDataService, 'submitTask').and.returnValue(observableOf(new ProcessTaskResponse(false)));
component.selectedReviewers = [EPersonMock, EPersonMock2];
component.performAction();
expect(claimedTaskDataService.submitTask).toHaveBeenCalledWith(claimedTaskId, {
[ADVANCED_WORKFLOW_TASK_OPTION_SELECT_REVIEWER]: true,
eperson: [EPersonMock.id, EPersonMock2.id],
});
expect(notificationService.error).toHaveBeenCalled();
expect(component.previousPage).not.toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,152 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Location } from '@angular/common';
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 {
SelectReviewerAdvancedWorkflowInfo
} from '../../../core/tasks/models/select-reviewer-advanced-workflow-info.model';
import {
EPersonListActionConfig
} from '../../../access-control/group-registry/group-form/members-list/members-list.component';
import { Subscription } from 'rxjs';
import { EPerson } from '../../../core/eperson/models/eperson.model';
import { ActivatedRoute, Params, Router } from '@angular/router';
import { WorkflowItemDataService } from '../../../core/submission/workflowitem-data.service';
import { RouteService } from '../../../core/services/route.service';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core';
import { WorkflowActionDataService } from '../../../core/data/workflow-action-data.service';
import { ClaimedTaskDataService } from '../../../core/tasks/claimed-task-data.service';
import { RequestService } from '../../../core/data/request.service';
import { hasValue } from '../../../shared/empty.util';
export const ADVANCED_WORKFLOW_TASK_OPTION_SELECT_REVIEWER = 'submit_select_reviewer';
export const ADVANCED_WORKFLOW_ACTION_SELECT_REVIEWER = 'selectrevieweraction';
/**
* The page on which Review Managers can assign Reviewers to review an item.
*/
@rendersAdvancedWorkflowTaskOption(ADVANCED_WORKFLOW_ACTION_SELECT_REVIEWER)
@Component({
selector: 'ds-advanced-workflow-action-select-reviewer',
templateUrl: './advanced-workflow-action-select-reviewer.component.html',
styleUrls: ['./advanced-workflow-action-select-reviewer.component.scss'],
})
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[] = [];
displayError = false;
constructor(
protected route: ActivatedRoute,
protected workflowItemService: WorkflowItemDataService,
protected router: Router,
protected routeService: RouteService,
protected notificationsService: NotificationsService,
protected translationService: TranslateService,
protected workflowActionService: WorkflowActionDataService,
protected claimedTaskDataService: ClaimedTaskDataService,
protected requestService: RequestService,
protected location: Location,
) {
super(route, workflowItemService, router, routeService, notificationsService, translationService, workflowActionService, claimedTaskDataService, requestService, location);
}
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 SelectReviewerAdvancedWorkflowInfo[])[0].group;
} else {
this.groupId = null;
}
}));
}
getType(): string {
return ADVANCED_WORKFLOW_ACTION_SELECT_REVIEWER;
}
/**
* Only performs the action when some reviewers have been selected.
*/
performAction(): void {
if (this.selectedReviewers.length > 0) {
super.performAction();
} else {
this.displayError = true;
}
}
/**
* Returns the task option and the selected {@link EPerson} id(s)
*/
createBody(): any {
return {
[ADVANCED_WORKFLOW_TASK_OPTION_SELECT_REVIEWER]: true,
eperson: this.selectedReviewers.map((ePerson: EPerson) => ePerson.id),
};
}
/**
* Hardcoded the previous page url because the {@link ReviewersListComponent} changes the previous route when
* switching between the different pages
*/
previousPage(): void {
let queryParams: Params = this.previousQueryParameters;
if (!hasValue(queryParams)) {
queryParams = {
configuration: 'workflow',
};
}
void this.router.navigate(['/mydspace'], { queryParams: queryParams });
}
}

View File

@@ -0,0 +1,252 @@
import { CommonModule } from '@angular/common';
import { NO_ERRORS_SCHEMA, SimpleChange, DebugElement } from '@angular/core';
import { ComponentFixture, fakeAsync, flush, 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';
import { EpersonDtoModel } from '../../../../core/eperson/models/eperson-dto.model';
describe('ReviewersListComponent', () => {
let component: ReviewersListComponent;
let fixture: ComponentFixture<ReviewersListComponent>;
let translateService: TranslateService;
let builderService: FormBuilderService;
let ePersonDataServiceStub: any;
let groupsDataServiceStub: any;
let activeGroup;
let allEPersons;
let allGroups;
let epersonMembers;
let subgroupMembers;
let paginationService;
let ePersonDtoModel1: EpersonDtoModel;
let ePersonDtoModel2: EpersonDtoModel;
beforeEach(waitForAsync(() => {
activeGroup = GroupMock;
epersonMembers = [EPersonMock2];
subgroupMembers = [GroupMock2];
allEPersons = [EPersonMock, EPersonMock2];
allGroups = [GroupMock, GroupMock2];
ePersonDataServiceStub = {
activeGroup: activeGroup,
epersonMembers: epersonMembers,
subgroupMembers: subgroupMembers,
findListByHref(_href: string): Observable<RemoteData<PaginatedList<EPerson>>> {
return createSuccessfulRemoteDataObject$(buildPaginatedList<EPerson>(new PageInfo(), groupsDataServiceStub.getEPersonMembers()));
},
searchByScope(scope: string, query: string): Observable<RemoteData<PaginatedList<EPerson>>> {
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<Group> {
return observableOf(activeGroup);
},
getEPersonMembers() {
return this.epersonMembers;
},
searchGroups(query: string): Observable<RemoteData<PaginatedList<Group>>> {
if (query === '') {
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), this.allGroups));
}
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), []));
},
addMemberToGroup(parentGroup, eperson: EPerson): Observable<RestResponse> {
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<RestResponse> {
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) {
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();
}));
beforeEach(() => {
ePersonDtoModel1 = new EpersonDtoModel();
ePersonDtoModel1.eperson = EPersonMock;
ePersonDtoModel2 = new EpersonDtoModel();
ePersonDtoModel2.eperson = EPersonMock2;
});
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 a 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: DebugElement) => {
return (foundEl.nativeElement.textContent.trim() === ePerson.uuid);
})).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 = [ePersonDtoModel1];
component.addMemberToGroup(ePersonDtoModel2);
expect(component.selectedReviewers).toEqual([ePersonDtoModel2]);
expect(component.selectedReviewersUpdated.emit).toHaveBeenCalledWith([ePersonDtoModel2.eperson]);
});
it('should add the value when a new member is added when multipleReviewers is true', () => {
spyOn(component.selectedReviewersUpdated, 'emit');
component.multipleReviewers = true;
component.selectedReviewers = [ePersonDtoModel1];
component.addMemberToGroup(ePersonDtoModel2);
expect(component.selectedReviewers).toEqual([ePersonDtoModel1, ePersonDtoModel2]);
expect(component.selectedReviewersUpdated.emit).toHaveBeenCalledWith([ePersonDtoModel1.eperson, ePersonDtoModel2.eperson]);
});
it('should delete the member when present', () => {
spyOn(component.selectedReviewersUpdated, 'emit');
ePersonDtoModel1.memberOfGroup = true;
component.selectedReviewers = [ePersonDtoModel1];
component.deleteMemberFromGroup(ePersonDtoModel1);
expect(component.selectedReviewers).toEqual([]);
expect(ePersonDtoModel1.memberOfGroup).toBeFalse();
expect(component.selectedReviewersUpdated.emit).toHaveBeenCalledWith([]);
});
});

View File

@@ -0,0 +1,153 @@
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 { 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';
import {
MembersListComponent,
EPersonListActionConfig,
} from '../../../../access-control/group-registry/group-form/members-list/members-list.component';
import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
/**
* Keys to keep track of specific subscriptions
*/
enum SubKey {
ActiveGroup,
MembersDTO,
SearchResultsDTO,
}
/**
* A custom {@link MembersListComponent} for the advanced SelectReviewer workflow.
*/
@Component({
selector: 'ds-reviewers-list',
// templateUrl: './reviewers-list.component.html',
templateUrl: '../../../../access-control/group-registry/group-form/members-list/members-list.component.html',
})
export class ReviewersListComponent extends MembersListComponent implements OnInit, OnChanges, OnDestroy {
@Input()
groupId: string | null;
@Input()
actionConfig: EPersonListActionConfig;
@Input()
multipleReviewers: boolean;
@Output()
selectedReviewersUpdated: EventEmitter<EPerson[]> = new EventEmitter();
selectedReviewers: EpersonDtoModel[] = [];
constructor(
protected groupService: GroupDataService,
public ePersonDataService: EPersonDataService,
protected translateService: TranslateService,
protected notificationsService: NotificationsService,
protected formBuilder: FormBuilder,
protected paginationService: PaginationService,
protected router: Router,
public dsoNameService: DSONameService,
) {
super(groupService, ePersonDataService, translateService, notificationsService, formBuilder, paginationService, router, dsoNameService);
}
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);
}
}));
}
}
}
/**
* Sets the list of currently selected members, when no group is defined the list of {@link selectedReviewers}
* will be set.
*
* @param page The number of the page to retrieve
*/
retrieveMembers(page: number): void {
this.config.currentPage = page;
if (this.groupId === null) {
this.unsubFrom(SubKey.MembersDTO);
const paginatedListOfDTOs: PaginatedList<EpersonDtoModel> = new PaginatedList();
paginatedListOfDTOs.page = this.selectedReviewers;
this.ePeopleMembersOfGroupDtos.next(paginatedListOfDTOs);
} 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: EpersonDtoModel) => reviewer.eperson.id === possibleMember.id)));
}
/**
* Removes the {@link ePerson} from the {@link selectedReviewers}
*
* @param ePerson The {@link EpersonDtoModel} containg the {@link EPerson} to remove
*/
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));
}
/**
* Adds the {@link ePerson} to the {@link selectedReviewers} (or replaces it when {@link multipleReviewers} is
* `false`). Afterwards it will emit the list.
*
* @param ePerson The {@link EPerson} to add to the list
*/
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));
}
}

View File

@@ -0,0 +1,134 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Location } from '@angular/common';
import { AdvancedWorkflowActionComponent } from './advanced-workflow-action.component';
import { Component } from '@angular/core';
import { MockComponent } from 'ng-mocks';
import { DSOSelectorComponent } from '../../../shared/dso-selector/dso-selector/dso-selector.component';
import { ClaimedTaskDataService } from '../../../core/tasks/claimed-task-data.service';
import { ClaimedTaskDataServiceStub } from '../../../shared/testing/claimed-task-data-service.stub';
import { ActivatedRoute } from '@angular/router';
import { of as observableOf } from 'rxjs';
import { WorkflowItemDataService } from '../../../core/submission/workflowitem-data.service';
import { RouterTestingModule } from '@angular/router/testing';
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
import { WorkflowActionDataService } from '../../../core/data/workflow-action-data.service';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { RouteService } from '../../../core/services/route.service';
import { routeServiceStub } from '../../../shared/testing/route-service.stub';
import { TranslateModule } from '@ngx-translate/core';
import { WorkflowActionDataServiceStub } from '../../../shared/testing/workflow-action-data-service.stub';
import { ProcessTaskResponse } from '../../../core/tasks/models/process-task-response';
import { WorkflowItemDataServiceStub } from '../../../shared/testing/workflow-item-data-service.stub';
import { RequestService } from '../../../core/data/request.service';
import { RequestServiceStub } from '../../../shared/testing/request-service.stub';
import { LocationStub } from '../../../shared/testing/location.stub';
const workflowId = '1';
describe('AdvancedWorkflowActionComponent', () => {
let component: AdvancedWorkflowActionComponent;
let fixture: ComponentFixture<AdvancedWorkflowActionComponent>;
let claimedTaskDataService: ClaimedTaskDataServiceStub;
let location: LocationStub;
let notificationService: NotificationsServiceStub;
let workflowActionDataService: WorkflowActionDataServiceStub;
let workflowItemDataService: WorkflowItemDataServiceStub;
beforeEach(async () => {
claimedTaskDataService = new ClaimedTaskDataServiceStub();
location = new LocationStub();
notificationService = new NotificationsServiceStub();
workflowActionDataService = new WorkflowActionDataServiceStub();
workflowItemDataService = new WorkflowItemDataServiceStub();
await TestBed.configureTestingModule({
imports: [
TranslateModule.forRoot(),
RouterTestingModule,
],
declarations: [
TestComponent,
MockComponent(DSOSelectorComponent),
],
providers: [
{
provide: ActivatedRoute,
useValue: {
data: observableOf({
id: workflowId,
}),
snapshot: {
queryParams: {
workflow: 'testaction',
},
},
},
},
{ provide: ClaimedTaskDataService, useValue: claimedTaskDataService },
{ provide: Location, useValue: location },
{ provide: NotificationsService, useValue: notificationService },
{ provide: RouteService, useValue: routeServiceStub },
{ provide: WorkflowActionDataService, useValue: workflowActionDataService },
{ provide: WorkflowItemDataService, useValue: workflowItemDataService },
{ provide: RequestService, useClass: RequestServiceStub },
],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(TestComponent);
component = fixture.componentInstance;
component.ngOnInit();
fixture.detectChanges();
});
describe('sendRequest', () => {
it('should return true if the request succeeded', () => {
spyOn(claimedTaskDataService, 'submitTask').and.returnValue(observableOf(new ProcessTaskResponse(true, 200)));
spyOn(workflowActionDataService, 'findById');
const result = component.sendRequest(workflowId);
expect(claimedTaskDataService.submitTask).toHaveBeenCalledWith('1', {
'submit_test': true,
});
result.subscribe((value: boolean) => {
expect(value).toBeTrue();
});
});
it('should return false if the request didn\'t succeeded', () => {
spyOn(claimedTaskDataService, 'submitTask').and.returnValue(observableOf(new ProcessTaskResponse(false, 404)));
spyOn(workflowActionDataService, 'findById');
const result = component.sendRequest(workflowId);
expect(claimedTaskDataService.submitTask).toHaveBeenCalledWith('1', {
'submit_test': true,
});
result.subscribe((value: boolean) => {
expect(value).toBeFalse();
});
});
});
});
@Component({
// eslint-disable-next-line @angular-eslint/component-selector
selector: '',
template: ''
})
class TestComponent extends AdvancedWorkflowActionComponent {
createBody(): any {
return {
'submit_test': true,
};
}
getType(): string {
return 'testaction';
}
}

View File

@@ -0,0 +1,88 @@
import { Component, OnInit } from '@angular/core';
import { WorkflowAction } from '../../../core/tasks/models/workflow-action-object.model';
import { WorkflowActionDataService } from '../../../core/data/workflow-action-data.service';
import { ActivatedRoute, Router } from '@angular/router';
import { Observable } from 'rxjs';
import { WorkflowItemActionPageComponent } from '../../workflow-item-action-page.component';
import { WorkflowItemDataService } from '../../../core/submission/workflowitem-data.service';
import { RouteService } from '../../../core/services/route.service';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core';
import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators';
import { ClaimedTaskDataService } from '../../../core/tasks/claimed-task-data.service';
import { map } from 'rxjs/operators';
import { ProcessTaskResponse } from '../../../core/tasks/models/process-task-response';
import { RequestService } from '../../../core/data/request.service';
import { Location } from '@angular/common';
/**
* Abstract component for rendering an advanced claimed task's workflow page
* To create a child-component for a new option:
* - Set the "{@link getType}()" of the component
* - Implement the {@link createBody}, should always contain at least the ADVANCED_WORKFLOW_TASK_OPTION_*
*/
@Component({
selector: 'ds-advanced-workflow-action',
template: '',
})
export abstract class AdvancedWorkflowActionComponent extends WorkflowItemActionPageComponent implements OnInit {
workflowAction$: Observable<WorkflowAction>;
constructor(
protected route: ActivatedRoute,
protected workflowItemService: WorkflowItemDataService,
protected router: Router,
protected routeService: RouteService,
protected notificationsService: NotificationsService,
protected translationService: TranslateService,
protected workflowActionService: WorkflowActionDataService,
protected claimedTaskDataService: ClaimedTaskDataService,
protected requestService: RequestService,
protected location: Location,
) {
super(route, workflowItemService, router, routeService, notificationsService, translationService, requestService, location);
}
ngOnInit(): void {
super.ngOnInit();
this.workflowAction$ = this.workflowActionService.findById(this.route.snapshot.queryParams.workflow).pipe(
getFirstSucceededRemoteDataPayload(),
);
}
/**
* Performs the action and shows a notification based on the outcome of the action
*/
performAction(): void {
this.sendRequest(this.route.snapshot.queryParams.claimedTask).subscribe((successful: boolean) => {
if (successful) {
const title = this.translationService.get('workflow-item.' + this.type + '.notification.success.title');
const content = this.translationService.get('workflow-item.' + this.type + '.notification.success.content');
this.notificationsService.success(title, content);
this.previousPage();
} else {
const title = this.translationService.get('workflow-item.' + this.type + '.notification.error.title');
const content = this.translationService.get('workflow-item.' + this.type + '.notification.error.content');
this.notificationsService.error(title, content);
}
});
}
/**
* Submits the task with the given {@link createBody}.
*
* @param id The task id
*/
sendRequest(id: string): Observable<boolean> {
return this.claimedTaskDataService.submitTask(id, this.createBody()).pipe(
map((processTaskResponse: ProcessTaskResponse) => processTaskResponse.hasSucceeded),
);
}
/**
* The body that needs to be passed to the {@link ClaimedTaskDataService}.submitTask().
*/
abstract createBody(): any;
}

View File

@@ -0,0 +1 @@
<ng-template dsAdvancedWorkflowActions></ng-template>

View File

@@ -0,0 +1,83 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AdvancedWorkflowActionsLoaderComponent } from './advanced-workflow-actions-loader.component';
import { Router } from '@angular/router';
import { RouterStub } from '../../../shared/testing/router.stub';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { AdvancedWorkflowActionsDirective } from './advanced-workflow-actions.directive';
import {
rendersAdvancedWorkflowTaskOption
} from '../../../shared/mydspace-actions/claimed-task/switcher/claimed-task-actions-decorator';
import { By } from '@angular/platform-browser';
import { PAGE_NOT_FOUND_PATH } from '../../../app-routing-paths';
const ADVANCED_WORKFLOW_ACTION_TEST = 'testaction';
describe('AdvancedWorkflowActionsLoaderComponent', () => {
let component: AdvancedWorkflowActionsLoaderComponent;
let fixture: ComponentFixture<AdvancedWorkflowActionsLoaderComponent>;
let router: RouterStub;
beforeEach(async () => {
router = new RouterStub();
await TestBed.configureTestingModule({
declarations: [
AdvancedWorkflowActionsDirective,
AdvancedWorkflowActionsLoaderComponent,
],
providers: [
{ provide: Router, useValue: router },
],
}).overrideComponent(AdvancedWorkflowActionsLoaderComponent, {
set: {
changeDetection: ChangeDetectionStrategy.Default,
entryComponents: [AdvancedWorkflowActionTestComponent],
},
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(AdvancedWorkflowActionsLoaderComponent);
component = fixture.componentInstance;
component.type = ADVANCED_WORKFLOW_ACTION_TEST;
fixture.detectChanges();
});
afterEach(() => {
fixture.debugElement.nativeElement.remove();
});
describe('When the component is rendered', () => {
it('should display the AdvancedWorkflowActionTestComponent when the type has been defined in a rendersAdvancedWorkflowTaskOption', () => {
spyOn(component, 'getComponentByWorkflowTaskOption').and.returnValue(AdvancedWorkflowActionTestComponent);
component.ngOnInit();
fixture.detectChanges();
expect(component.getComponentByWorkflowTaskOption).toHaveBeenCalledWith(ADVANCED_WORKFLOW_ACTION_TEST);
expect(fixture.debugElement.query(By.css('#AdvancedWorkflowActionsLoaderComponent'))).not.toBeNull();
expect(router.navigate).not.toHaveBeenCalled();
});
it('should redirect to page not found when the type has not been defined in a rendersAdvancedWorkflowTaskOption', () => {
spyOn(component, 'getComponentByWorkflowTaskOption').and.returnValue(undefined);
component.type = 'nonexistingaction';
component.ngOnInit();
fixture.detectChanges();
expect(component.getComponentByWorkflowTaskOption).toHaveBeenCalledWith('nonexistingaction');
expect(router.navigate).toHaveBeenCalledWith([PAGE_NOT_FOUND_PATH]);
});
});
});
@rendersAdvancedWorkflowTaskOption(ADVANCED_WORKFLOW_ACTION_TEST)
@Component({
// eslint-disable-next-line @angular-eslint/component-selector
selector: '',
template: '<span id="AdvancedWorkflowActionsLoaderComponent"></span>',
})
class AdvancedWorkflowActionTestComponent {
}

View File

@@ -0,0 +1,57 @@
import { Component, Input, ViewChild, ComponentFactoryResolver, OnInit } from '@angular/core';
import { hasValue } from '../../../shared/empty.util';
import {
getAdvancedComponentByWorkflowTaskOption
} from '../../../shared/mydspace-actions/claimed-task/switcher/claimed-task-actions-decorator';
import { AdvancedWorkflowActionsDirective } from './advanced-workflow-actions.directive';
import { Router } from '@angular/router';
import { PAGE_NOT_FOUND_PATH } from '../../../app-routing-paths';
/**
* Component for loading a {@link AdvancedWorkflowActionComponent} depending on the "{@link type}" input
*/
@Component({
selector: 'ds-advanced-workflow-actions-loader',
templateUrl: './advanced-workflow-actions-loader.component.html',
styleUrls: ['./advanced-workflow-actions-loader.component.scss'],
})
export class AdvancedWorkflowActionsLoaderComponent implements OnInit {
/**
* The name of the type to render
* Passed on to the decorator to fetch the relevant component for this option
*/
@Input() type: string;
/**
* Directive to determine where the dynamic child component is located
*/
@ViewChild(AdvancedWorkflowActionsDirective, { static: true }) claimedTaskActionsDirective: AdvancedWorkflowActionsDirective;
constructor(
private componentFactoryResolver: ComponentFactoryResolver,
private router: Router,
) {
}
/**
* Fetch, create and initialize the relevant component
*/
ngOnInit(): void {
const comp = this.getComponentByWorkflowTaskOption(this.type);
if (hasValue(comp)) {
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(comp);
const viewContainerRef = this.claimedTaskActionsDirective.viewContainerRef;
viewContainerRef.clear();
viewContainerRef.createComponent(componentFactory);
} else {
void this.router.navigate([PAGE_NOT_FOUND_PATH]);
}
}
getComponentByWorkflowTaskOption(type: string): any {
return getAdvancedComponentByWorkflowTaskOption(type);
}
}

View File

@@ -0,0 +1,16 @@
import { Directive, ViewContainerRef } from '@angular/core';
@Directive({
selector: '[dsAdvancedWorkflowActions]',
})
/**
* Directive used as a hook to know where to inject the dynamic Advanced Claimed Task Actions component
*/
export class AdvancedWorkflowActionsDirective {
constructor(
public viewContainerRef: ViewContainerRef,
) {
}
}

View File

@@ -16,6 +16,10 @@ import { TranslateLoaderMock } from '../shared/mocks/translate-loader.mock';
import { ActivatedRouteStub } from '../shared/testing/active-router.stub';
import { RouterStub } from '../shared/testing/router.stub';
import { NotificationsServiceStub } from '../shared/testing/notifications-service.stub';
import { RequestService } from '../core/data/request.service';
import { RequestServiceStub } from '../shared/testing/request-service.stub';
import { Location } from '@angular/common';
import { LocationStub } from '../shared/testing/location.stub';
const type = 'testType';
describe('WorkflowItemActionPageComponent', () => {
@@ -50,8 +54,10 @@ describe('WorkflowItemActionPageComponent', () => {
{ provide: ActivatedRoute, useValue: new ActivatedRouteStub({}, { wfi: createSuccessfulRemoteDataObject(wfi) }) },
{ provide: Router, useClass: RouterStub },
{ provide: RouteService, useValue: {} },
{ provide: Location, useValue: new LocationStub() },
{ provide: NotificationsService, useClass: NotificationsServiceStub },
{ provide: WorkflowItemDataService, useValue: wfiService },
{ provide: RequestService, useClass: RequestServiceStub },
],
schemas: [NO_ERRORS_SCHEMA]
})
@@ -110,8 +116,11 @@ class TestComponent extends WorkflowItemActionPageComponent {
protected router: Router,
protected routeService: RouteService,
protected notificationsService: NotificationsService,
protected translationService: TranslateService) {
super(route, workflowItemService, router, routeService, notificationsService, translationService);
protected translationService: TranslateService,
protected requestService: RequestService,
protected location: Location,
) {
super(route, workflowItemService, router, routeService, notificationsService, translationService, requestService, location);
}
getType(): string {

View File

@@ -1,16 +1,18 @@
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { Location } from '@angular/common';
import { Observable, forkJoin } from 'rxjs';
import { map, switchMap, take } from 'rxjs/operators';
import { TranslateService } from '@ngx-translate/core';
import { WorkflowItem } from '../core/submission/models/workflowitem.model';
import { Item } from '../core/shared/item.model';
import { ActivatedRoute, Data, Router } from '@angular/router';
import { ActivatedRoute, Data, Router, Params } from '@angular/router';
import { WorkflowItemDataService } from '../core/submission/workflowitem-data.service';
import { RouteService } from '../core/services/route.service';
import { NotificationsService } from '../shared/notifications/notifications.service';
import { RemoteData } from '../core/data/remote-data';
import { getAllSucceededRemoteData, getRemoteDataPayload } from '../core/shared/operators';
import { isEmpty } from '../shared/empty.util';
import { RequestService } from '../core/data/request.service';
/**
* Abstract component representing a page to perform an action on a workflow item
@@ -23,13 +25,17 @@ export abstract class WorkflowItemActionPageComponent implements OnInit {
public type;
public wfi$: Observable<WorkflowItem>;
public item$: Observable<Item>;
protected previousQueryParameters?: Params;
constructor(protected route: ActivatedRoute,
protected workflowItemService: WorkflowItemDataService,
protected router: Router,
protected routeService: RouteService,
protected notificationsService: NotificationsService,
protected translationService: TranslateService) {
protected translationService: TranslateService,
protected requestService: RequestService,
protected location: Location,
) {
}
/**
@@ -39,15 +45,16 @@ export abstract class WorkflowItemActionPageComponent implements OnInit {
this.type = this.getType();
this.wfi$ = this.route.data.pipe(map((data: Data) => data.wfi as RemoteData<WorkflowItem>), getRemoteDataPayload());
this.item$ = this.wfi$.pipe(switchMap((wfi: WorkflowItem) => (wfi.item as Observable<RemoteData<Item>>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload())));
this.previousQueryParameters = (this.location.getState() as { [key: string]: any }).previousQueryParams;
}
/**
* Performs the action and shows a notification based on the outcome of the action
*/
performAction() {
this.wfi$.pipe(
forkJoin([this.wfi$, this.requestService.removeByHrefSubstring('/discover')]).pipe(
take(1),
switchMap((wfi: WorkflowItem) => this.sendRequest(wfi.id))
switchMap(([wfi]) => this.sendRequest(wfi.id))
).subscribe((successful: boolean) => {
if (successful) {
const title = this.translationService.get('workflow-item.' + this.type + '.notification.success.title');
@@ -69,10 +76,17 @@ export abstract class WorkflowItemActionPageComponent implements OnInit {
previousPage() {
this.routeService.getPreviousUrl().pipe(take(1))
.subscribe((url) => {
let params: Params = {};
if (isEmpty(url)) {
url = '/mydspace';
params = this.previousQueryParameters;
}
this.router.navigateByUrl(url);
if (url.split('?').length > 1) {
for (const param of url.split('?')[1].split('&')) {
params[param.split('=')[0]] = decodeURIComponent(param.split('=')[1]);
}
}
void this.router.navigate([url.split('?')[0]], { queryParams: params });
}
);
}

View File

@@ -1,5 +1,5 @@
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { Location } from '@angular/common';
import { WorkflowItemDeleteComponent } from './workflow-item-delete.component';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { ActivatedRoute, Router } from '@angular/router';
@@ -17,6 +17,7 @@ import { ActivatedRouteStub } from '../../shared/testing/active-router.stub';
import { RouterStub } from '../../shared/testing/router.stub';
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
import { getMockRequestService } from '../../shared/mocks/request.service.mock';
import { LocationStub } from '../../shared/testing/location.stub';
describe('WorkflowItemDeleteComponent', () => {
let component: WorkflowItemDeleteComponent;
@@ -50,6 +51,7 @@ describe('WorkflowItemDeleteComponent', () => {
{ provide: ActivatedRoute, useValue: new ActivatedRouteStub({}, { wfi: createSuccessfulRemoteDataObject(wfi) }) },
{ provide: Router, useClass: RouterStub },
{ provide: RouteService, useValue: {} },
{ provide: Location, useValue: new LocationStub() },
{ provide: NotificationsService, useClass: NotificationsServiceStub },
{ provide: WorkflowItemDataService, useValue: wfiService },
{ provide: RequestService, useValue: getMockRequestService() },

View File

@@ -11,6 +11,7 @@ import { map } from 'rxjs/operators';
import { RemoteData } from '../../core/data/remote-data';
import { NoContent } from '../../core/shared/NoContent.model';
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
import { Location } from '@angular/common';
@Component({
selector: 'ds-workflow-item-delete',
@@ -26,8 +27,10 @@ export class WorkflowItemDeleteComponent extends WorkflowItemActionPageComponent
protected routeService: RouteService,
protected notificationsService: NotificationsService,
protected translationService: TranslateService,
protected requestService: RequestService) {
super(route, workflowItemService, router, routeService, notificationsService, translationService);
protected requestService: RequestService,
protected location: Location,
) {
super(route, workflowItemService, router, routeService, notificationsService, translationService, requestService, location);
}
/**
@@ -42,7 +45,6 @@ export class WorkflowItemDeleteComponent extends WorkflowItemActionPageComponent
* @param id The id of the WorkflowItem
*/
sendRequest(id: string): Observable<boolean> {
this.requestService.removeByHrefSubstring('/discover');
return this.workflowItemService.delete(id).pipe(
getFirstCompletedRemoteData(),
map((response: RemoteData<NoContent>) => response.hasSucceeded)

View File

@@ -1,5 +1,5 @@
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { Location } from '@angular/common';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { ActivatedRoute, Router } from '@angular/router';
import { RouteService } from '../../core/services/route.service';
@@ -17,6 +17,7 @@ import { RouterStub } from '../../shared/testing/router.stub';
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock';
import { getMockRequestService } from '../../shared/mocks/request.service.mock';
import { LocationStub } from '../../shared/testing/location.stub';
describe('WorkflowItemSendBackComponent', () => {
let component: WorkflowItemSendBackComponent;
@@ -50,6 +51,7 @@ describe('WorkflowItemSendBackComponent', () => {
{ provide: ActivatedRoute, useValue: new ActivatedRouteStub({}, { wfi: createSuccessfulRemoteDataObject(wfi) }) },
{ provide: Router, useClass: RouterStub },
{ provide: RouteService, useValue: {} },
{ provide: Location, useValue: new LocationStub() },
{ provide: NotificationsService, useClass: NotificationsServiceStub },
{ provide: WorkflowItemDataService, useValue: wfiService },
{ provide: RequestService, useValue: getMockRequestService() },

View File

@@ -7,6 +7,7 @@ import { RouteService } from '../../core/services/route.service';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core';
import { RequestService } from '../../core/data/request.service';
import { Location } from '@angular/common';
@Component({
selector: 'ds-workflow-item-send-back',
@@ -22,8 +23,10 @@ export class WorkflowItemSendBackComponent extends WorkflowItemActionPageCompone
protected routeService: RouteService,
protected notificationsService: NotificationsService,
protected translationService: TranslateService,
protected requestService: RequestService) {
super(route, workflowItemService, router, routeService, notificationsService, translationService);
protected requestService: RequestService,
protected location: Location,
) {
super(route, workflowItemService, router, routeService, notificationsService, translationService, requestService, location);
}
/**
@@ -38,7 +41,6 @@ export class WorkflowItemSendBackComponent extends WorkflowItemActionPageCompone
* @param id The id of the WorkflowItem
*/
sendRequest(id: string): Observable<boolean> {
this.requestService.removeByHrefSubstring('/discover');
return this.workflowItemService.sendBack(id);
}
}

View File

@@ -20,7 +20,12 @@ export function getWorkflowItemSendBackRoute(wfiId: string) {
return new URLCombiner(getWorkflowItemModuleRoute(), wfiId, WORKFLOW_ITEM_SEND_BACK_PATH).toString();
}
export function getAdvancedWorkflowRoute(wfiId: string) {
return new URLCombiner(getWorkflowItemModuleRoute(), wfiId, ADVANCED_WORKFLOW_PATH).toString();
}
export const WORKFLOW_ITEM_EDIT_PATH = 'edit';
export const WORKFLOW_ITEM_DELETE_PATH = 'delete';
export const WORKFLOW_ITEM_VIEW_PATH = 'view';
export const WORKFLOW_ITEM_SEND_BACK_PATH = 'sendback';
export const ADVANCED_WORKFLOW_PATH = 'advanced';

View File

@@ -7,7 +7,8 @@ import {
WORKFLOW_ITEM_DELETE_PATH,
WORKFLOW_ITEM_EDIT_PATH,
WORKFLOW_ITEM_SEND_BACK_PATH,
WORKFLOW_ITEM_VIEW_PATH
WORKFLOW_ITEM_VIEW_PATH,
ADVANCED_WORKFLOW_PATH,
} from './workflowitems-edit-page-routing-paths';
import { ThemedSubmissionEditComponent } from '../submission/edit/themed-submission-edit.component';
import { ThemedWorkflowItemDeleteComponent } from './workflow-item-delete/themed-workflow-item-delete.component';
@@ -15,6 +16,9 @@ import { ThemedWorkflowItemSendBackComponent } from './workflow-item-send-back/t
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
import { ItemFromWorkflowResolver } from './item-from-workflow.resolver';
import { ThemedFullItemPageComponent } from '../item-page/full/themed-full-item-page.component';
import {
AdvancedWorkflowActionPageComponent
} from './advanced-workflow-action/advanced-workflow-action-page/advanced-workflow-action-page.component';
@NgModule({
imports: [
@@ -59,7 +63,16 @@ import { ThemedFullItemPageComponent } from '../item-page/full/themed-full-item-
breadcrumb: I18nBreadcrumbResolver
},
data: { title: 'workflow-item.send-back.title', breadcrumbKey: 'workflow-item.edit' }
}
},
{
canActivate: [AuthenticatedGuard],
path: ADVANCED_WORKFLOW_PATH,
component: AdvancedWorkflowActionPageComponent,
resolve: {
breadcrumb: I18nBreadcrumbResolver
},
data: { title: 'workflow-item.advanced.title', breadcrumbKey: 'workflow-item.edit' }
},
]
}]
)

View File

@@ -6,9 +6,32 @@ import { SubmissionModule } from '../submission/submission.module';
import { WorkflowItemDeleteComponent } from './workflow-item-delete/workflow-item-delete.component';
import { WorkflowItemSendBackComponent } from './workflow-item-send-back/workflow-item-send-back.component';
import { ThemedWorkflowItemDeleteComponent } from './workflow-item-delete/themed-workflow-item-delete.component';
import { ThemedWorkflowItemSendBackComponent } from './workflow-item-send-back/themed-workflow-item-send-back.component';
import {
ThemedWorkflowItemSendBackComponent
} from './workflow-item-send-back/themed-workflow-item-send-back.component';
import { StatisticsModule } from '../statistics/statistics.module';
import { ItemPageModule } from '../item-page/item-page.module';
import {
AdvancedWorkflowActionsLoaderComponent
} from './advanced-workflow-action/advanced-workflow-actions-loader/advanced-workflow-actions-loader.component';
import {
AdvancedWorkflowActionRatingComponent
} from './advanced-workflow-action/advanced-workflow-action-rating/advanced-workflow-action-rating.component';
import {
AdvancedWorkflowActionSelectReviewerComponent
} from './advanced-workflow-action/advanced-workflow-action-select-reviewer/advanced-workflow-action-select-reviewer.component';
import {
AdvancedWorkflowActionPageComponent
} from './advanced-workflow-action/advanced-workflow-action-page/advanced-workflow-action-page.component';
import {
AdvancedWorkflowActionsDirective
} from './advanced-workflow-action/advanced-workflow-actions-loader/advanced-workflow-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';
import { FormModule } from '../shared/form/form.module';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
@NgModule({
imports: [
@@ -17,13 +40,22 @@ import { ItemPageModule } from '../item-page/item-page.module';
SharedModule,
SubmissionModule,
StatisticsModule,
ItemPageModule
ItemPageModule,
AccessControlModule,
FormModule,
NgbModule,
],
declarations: [
WorkflowItemDeleteComponent,
ThemedWorkflowItemDeleteComponent,
WorkflowItemSendBackComponent,
ThemedWorkflowItemSendBackComponent
ThemedWorkflowItemSendBackComponent,
AdvancedWorkflowActionsLoaderComponent,
AdvancedWorkflowActionRatingComponent,
AdvancedWorkflowActionSelectReviewerComponent,
AdvancedWorkflowActionPageComponent,
AdvancedWorkflowActionsDirective,
ReviewersListComponent,
]
})
/**