Merge branch 'w2p-98211_advanced-workflow-actions-7.2' into w2p-98211_advanced-workflow-actions-main

# Conflicts:
#	src/app/core/core.module.ts
#	src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-select-reviewer/reviewers-list/reviewers-list.component.spec.ts
This commit is contained in:
Alexandre Vryghem
2023-01-18 13:46:38 +01:00
28 changed files with 471 additions and 169 deletions

View File

@@ -157,6 +157,9 @@ import { SequenceService } from './shared/sequence.service';
import { CoreState } from './core-state.model';
import { GroupDataService } from './eperson/group-data.service';
import { SubmissionAccessesModel } from './config/models/config-submission-accesses.model';
import { RatingAdvancedWorkflowInfo } from './tasks/models/rating-advanced-workflow-info.model';
import { AdvancedWorkflowInfo } from './tasks/models/advanced-workflow-info.model';
import { SelectReviewerAdvancedWorkflowInfo } from './tasks/models/select-reviewer-advanced-workflow-info.model';
import { AccessStatusObject } from '../shared/object-list/access-status-badge/access-status.model';
import { AccessStatusDataService } from './data/access-status-data.service';
import { LinkHeadService } from './services/link-head.service';
@@ -170,9 +173,6 @@ import { OrcidHistory } from './orcid/model/orcid-history.model';
import { OrcidAuthService } from './orcid/orcid-auth.service';
import { VocabularyDataService } from './submission/vocabularies/vocabulary.data.service';
import { VocabularyEntryDetailsDataService } from './submission/vocabularies/vocabulary-entry-details.data.service';
import { RatingReviewerActionAdvancedInfo } from './tasks/models/rating-reviewer-action-advanced-info.model';
import { ReviewerActionAdvancedInfo } from './tasks/models/reviewer-action-advanced-info.model';
import { SelectReviewerActionAdvancedInfo } from './tasks/models/select-reviewer-action-advanced-info.model';
/**
* When not in production, endpoint responses can be mocked for testing purposes
@@ -341,9 +341,9 @@ export const models =
Version,
VersionHistory,
WorkflowAction,
ReviewerActionAdvancedInfo,
RatingReviewerActionAdvancedInfo,
SelectReviewerActionAdvancedInfo,
AdvancedWorkflowInfo,
RatingAdvancedWorkflowInfo,
SelectReviewerAdvancedWorkflowInfo,
TemplateItem,
Feature,
Authorization,

View File

@@ -0,0 +1,11 @@
import { autoserialize } from 'cerialize';
/**
* An abstract model class for a {@link AdvancedWorkflowInfo}
*/
export abstract class AdvancedWorkflowInfo {
@autoserialize
id: string;
}

View File

@@ -0,0 +1,17 @@
import { ResourceType } from '../../shared/resource-type';
/**
* The resource type for {@link RatingAdvancedWorkflowInfo}
*
* Needs to be in a separate file to prevent circular
* dependencies in webpack.
*/
export const RATING_ADVANCED_WORKFLOW_INFO = new ResourceType('ratingrevieweraction');
/**
* The resource type for {@link SelectReviewerAdvancedWorkflowInfo}
*
* Needs to be in a separate file to prevent circular
* dependencies in webpack.
*/
export const SELECT_REVIEWER_ADVANCED_WORKFLOW_INFO = new ResourceType('selectrevieweraction');

View File

@@ -0,0 +1,28 @@
import { typedObject } from '../../cache/builders/build-decorators';
import { inheritSerialization, autoserialize } from 'cerialize';
import { RATING_ADVANCED_WORKFLOW_INFO } from './advanced-workflow-info.resource-type';
import { AdvancedWorkflowInfo } from './advanced-workflow-info.model';
import { ResourceType } from '../../shared/resource-type';
/**
* A model class for a {@link RatingAdvancedWorkflowInfo}
*/
@typedObject
@inheritSerialization(AdvancedWorkflowInfo)
export class RatingAdvancedWorkflowInfo extends AdvancedWorkflowInfo {
static type: ResourceType = RATING_ADVANCED_WORKFLOW_INFO;
/**
* Whether the description is required.
*/
@autoserialize
descriptionRequired: boolean;
/**
* The maximum value.
*/
@autoserialize
maxValue: number;
}

View File

@@ -1,27 +0,0 @@
import { typedObject } from '../../cache/builders/build-decorators';
import { inheritSerialization, autoserialize } from 'cerialize';
import { RATING_REVIEWER_ACTION_ADVANCED_INFO } from './reviewer-action-advanced-info.resource-type';
import { ReviewerActionAdvancedInfo } from './reviewer-action-advanced-info.model';
/**
* A model class for a {@link RatingReviewerActionAdvancedInfo}
*/
@typedObject
@inheritSerialization(ReviewerActionAdvancedInfo)
export class RatingReviewerActionAdvancedInfo extends ReviewerActionAdvancedInfo {
static type = RATING_REVIEWER_ACTION_ADVANCED_INFO;
/**
* Whether the description is required.
*/
@autoserialize
descriptionRequired: boolean;
/**
* The maximum value.
*/
@autoserialize
maxValue: number;
}

View File

@@ -1,16 +0,0 @@
import { typedObject } from '../../cache/builders/build-decorators';
import { autoserialize } from 'cerialize';
import { REVIEWER_ACTION_ADVANCED_INFO } from './reviewer-action-advanced-info.resource-type';
/**
* A model class for a {@link ReviewerActionAdvancedInfo}
*/
@typedObject
export class ReviewerActionAdvancedInfo {
static type = REVIEWER_ACTION_ADVANCED_INFO;
@autoserialize
id: string;
}

View File

@@ -1,25 +0,0 @@
import { ResourceType } from '../../shared/resource-type';
/**
* The resource type for {@link ReviewerActionAdvancedInfo}
*
* Needs to be in a separate file to prevent circular
* dependencies in webpack.
*/
export const REVIEWER_ACTION_ADVANCED_INFO = new ResourceType('revieweraction');
/**
* The resource type for {@link RatingReviewerActionAdvancedInfo}
*
* Needs to be in a separate file to prevent circular
* dependencies in webpack.
*/
export const RATING_REVIEWER_ACTION_ADVANCED_INFO = new ResourceType('ratingrevieweraction');
/**
* The resource type for {@link SelectReviewerActionAdvancedInfo}
*
* Needs to be in a separate file to prevent circular
* dependencies in webpack.
*/
export const SELECT_REVIEWER_ACTION_ADVANCED_INFO = new ResourceType('selectrevieweraction');

View File

@@ -1,18 +0,0 @@
import { typedObject } from '../../cache/builders/build-decorators';
import { inheritSerialization, autoserialize } from 'cerialize';
import { SELECT_REVIEWER_ACTION_ADVANCED_INFO } from './reviewer-action-advanced-info.resource-type';
import { ReviewerActionAdvancedInfo } from './reviewer-action-advanced-info.model';
/**
* A model class for a {@link SelectReviewerActionAdvancedInfo}
*/
@typedObject
@inheritSerialization(ReviewerActionAdvancedInfo)
export class SelectReviewerActionAdvancedInfo extends ReviewerActionAdvancedInfo {
static type = SELECT_REVIEWER_ACTION_ADVANCED_INFO;
@autoserialize
group: string;
}

View File

@@ -0,0 +1,19 @@
import { typedObject } from '../../cache/builders/build-decorators';
import { inheritSerialization, autoserialize } from 'cerialize';
import { SELECT_REVIEWER_ADVANCED_WORKFLOW_INFO } from './advanced-workflow-info.resource-type';
import { AdvancedWorkflowInfo } from './advanced-workflow-info.model';
import { ResourceType } from '../../shared/resource-type';
/**
* A model class for a {@link SelectReviewerAdvancedWorkflowInfo}
*/
@typedObject
@inheritSerialization(AdvancedWorkflowInfo)
export class SelectReviewerAdvancedWorkflowInfo extends AdvancedWorkflowInfo {
static type: ResourceType = SELECT_REVIEWER_ADVANCED_WORKFLOW_INFO;
@autoserialize
group: string;
}

View File

@@ -2,7 +2,7 @@ import { inheritSerialization, autoserialize } from 'cerialize';
import { typedObject } from '../../cache/builders/build-decorators';
import { DSpaceObject } from '../../shared/dspace-object.model';
import { WORKFLOW_ACTION } from './workflow-action-object.resource-type';
import { ReviewerActionAdvancedInfo } from './reviewer-action-advanced-info.model';
import { AdvancedWorkflowInfo } from './advanced-workflow-info.model';
/**
* A model class for a WorkflowAction
@@ -40,6 +40,6 @@ export class WorkflowAction extends DSpaceObject {
* The advanced info required by the advanced options
*/
@autoserialize
advancedInfo: ReviewerActionAdvancedInfo[];
advancedInfo: AdvancedWorkflowInfo[];
}

View File

@@ -7,9 +7,8 @@ import { getAdvancedWorkflowRoute } from '../../../../workflowitems-edit-page/wo
/**
* Abstract component for rendering an advanced claimed task's action
* To create a child-component for a new option:
* - Set the "option" of the component
* - Set the "option" and "workflowType" of the component
* - Add a @rendersWorkflowTaskOption annotation to your component providing the same enum value
* - Optionally overwrite createBody if the request body requires more than just the option
*/
@Component({
selector: 'ds-advanced-claimed-task-action-abstract',
@@ -17,7 +16,10 @@ import { getAdvancedWorkflowRoute } from '../../../../workflowitems-edit-page/wo
})
export abstract class AdvancedClaimedTaskActionsAbstractComponent extends ClaimedTaskActionsAbstractComponent implements OnInit {
workflowType: string;
/**
* The {@link WorkflowAction} id of the advanced workflow that needs to be opened.
*/
abstract workflowType: string;
/**
* Route to the workflow's task page
@@ -40,6 +42,9 @@ export abstract class AdvancedClaimedTaskActionsAbstractComponent extends Claime
}));
}
/**
* Navigates to the advanced workflow page based on the {@link workflow}.
*/
openAdvancedClaimedTaskTab(): void {
void this.router.navigate([this.workflowTaskPageRoute], {
queryParams: {

View File

@@ -9,11 +9,14 @@ import {
} from '../abstract/advanced-claimed-task-actions-abstract.component';
import {
ADVANCED_WORKFLOW_ACTION_RATING,
WORKFLOW_ADVANCED_TASK_OPTION_RATING,
ADVANCED_WORKFLOW_TASK_OPTION_RATING,
} from '../../../../workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-rating/advanced-workflow-action-rating.component';
import { rendersWorkflowTaskOption } from '../switcher/claimed-task-actions-decorator';
@rendersWorkflowTaskOption(WORKFLOW_ADVANCED_TASK_OPTION_RATING)
/**
* Advanced Workflow button that redirect to the {@link AdvancedWorkflowActionRatingComponent}
*/
@rendersWorkflowTaskOption(ADVANCED_WORKFLOW_TASK_OPTION_RATING)
@Component({
selector: 'ds-advanced-claimed-task-action-rating-reviewer',
templateUrl: './advanced-claimed-task-action-rating.component.html',
@@ -24,7 +27,7 @@ export class AdvancedClaimedTaskActionRatingComponent extends AdvancedClaimedTas
/**
* This component represents the advanced select option
*/
option = WORKFLOW_ADVANCED_TASK_OPTION_RATING;
option = ADVANCED_WORKFLOW_TASK_OPTION_RATING;
workflowType = ADVANCED_WORKFLOW_ACTION_RATING;

View File

@@ -10,10 +10,13 @@ import { SearchService } from '../../../../core/shared/search/search.service';
import { RequestService } from '../../../../core/data/request.service';
import {
ADVANCED_WORKFLOW_ACTION_SELECT_REVIEWER,
WORKFLOW_ADVANCED_TASK_OPTION_SELECT_REVIEWER
ADVANCED_WORKFLOW_TASK_OPTION_SELECT_REVIEWER
} from '../../../../workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-select-reviewer/advanced-workflow-action-select-reviewer.component';
@rendersWorkflowTaskOption(WORKFLOW_ADVANCED_TASK_OPTION_SELECT_REVIEWER)
/**
* Advanced Workflow button that redirect to the {@link AdvancedWorkflowActionSelectReviewerComponent}
*/
@rendersWorkflowTaskOption(ADVANCED_WORKFLOW_TASK_OPTION_SELECT_REVIEWER)
@Component({
selector: 'ds-advanced-claimed-task-action-select-reviewer',
templateUrl: './advanced-claimed-task-action-select-reviewer.component.html',
@@ -24,7 +27,7 @@ export class AdvancedClaimedTaskActionSelectReviewerComponent extends AdvancedCl
/**
* This component represents the advanced select option
*/
option = WORKFLOW_ADVANCED_TASK_OPTION_SELECT_REVIEWER;
option = ADVANCED_WORKFLOW_TASK_OPTION_SELECT_REVIEWER;
workflowType = ADVANCED_WORKFLOW_ACTION_SELECT_REVIEWER;

View File

@@ -1,6 +1,6 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AdvancedWorkflowActionPageComponent } from './advanced-workflow-action-page.component';
import { ActivatedRoute, convertToParamMap } from '@angular/router';
import { ActivatedRoute } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
describe('AdvancedWorkflowActionPageComponent', () => {
@@ -20,9 +20,9 @@ describe('AdvancedWorkflowActionPageComponent', () => {
provide: ActivatedRoute,
useValue: {
snapshot: {
queryParams: convertToParamMap({
queryParams: {
workflow: 'testaction',
}),
},
},
},
},

View File

@@ -1,6 +1,10 @@
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',

View File

@@ -1,6 +1,9 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AdvancedWorkflowActionRatingComponent } from './advanced-workflow-action-rating.component';
import { ActivatedRoute, convertToParamMap, Router } from '@angular/router';
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';
@@ -17,10 +20,19 @@ import { TranslateModule } from '@ngx-translate/core';
import { VarDirective } from '../../../shared/utils/var.directive';
import { RatingModule } from 'ngx-bootstrap/rating';
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';
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>;
@@ -52,11 +64,13 @@ describe('AdvancedWorkflowActionRatingComponent', () => {
useValue: {
data: observableOf({
id: workflowId,
wfi: createSuccessfulRemoteDataObject(workflowItem),
}),
snapshot: {
queryParams: convertToParamMap({
queryParams: {
claimedTask: claimedTaskId,
workflow: 'testaction',
}),
},
},
},
},
@@ -67,6 +81,7 @@ describe('AdvancedWorkflowActionRatingComponent', () => {
{ provide: WorkflowActionDataService, useValue: workflowActionDataService },
{ provide: WorkflowItemDataService, useValue: workflowItemDataService },
],
schemas: [NO_ERRORS_SCHEMA],
}).compileComponents();
});
@@ -80,7 +95,96 @@ describe('AdvancedWorkflowActionRatingComponent', () => {
fixture.debugElement.nativeElement.remove();
});
it('should create', () => {
expect(component).toBeTruthy();
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

@@ -6,12 +6,15 @@ import { AdvancedWorkflowActionComponent } from '../advanced-workflow-action/adv
import { FormGroup, FormControl } from '@angular/forms';
import { WorkflowAction } from '../../../core/tasks/models/workflow-action-object.model';
import {
RatingReviewerActionAdvancedInfo
} from '../../../core/tasks/models/rating-reviewer-action-advanced-info.model';
RatingAdvancedWorkflowInfo
} from '../../../core/tasks/models/rating-advanced-workflow-info.model';
export const WORKFLOW_ADVANCED_TASK_OPTION_RATING = 'submit_score';
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',
@@ -23,7 +26,7 @@ export class AdvancedWorkflowActionRatingComponent extends AdvancedWorkflowActio
ratingForm: FormGroup;
ngOnInit() {
ngOnInit(): void {
super.ngOnInit();
this.ratingForm = new FormGroup({
review: new FormControl(''),
@@ -43,9 +46,12 @@ export class AdvancedWorkflowActionRatingComponent extends AdvancedWorkflowActio
}
}
/**
* Returns the task option, the score and the review if one was provided
*/
createBody(): any {
const body = {
[WORKFLOW_ADVANCED_TASK_OPTION_RATING]: true,
[ADVANCED_WORKFLOW_TASK_OPTION_RATING]: true,
score: this.ratingForm.get('rating').value,
};
if (this.ratingForm.get('review').value !== '') {
@@ -59,8 +65,8 @@ export class AdvancedWorkflowActionRatingComponent extends AdvancedWorkflowActio
return ADVANCED_WORKFLOW_ACTION_RATING;
}
getAdvancedInfo(workflowAction: WorkflowAction | null): RatingReviewerActionAdvancedInfo | null {
return workflowAction ? (workflowAction.advancedInfo[0] as RatingReviewerActionAdvancedInfo) : null;
getAdvancedInfo(workflowAction: WorkflowAction | null): RatingAdvancedWorkflowInfo | null {
return workflowAction ? (workflowAction.advancedInfo[0] as RatingAdvancedWorkflowInfo) : null;
}
}

View File

@@ -1,6 +1,9 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AdvancedWorkflowActionSelectReviewerComponent } from './advanced-workflow-action-select-reviewer.component';
import { ActivatedRoute, convertToParamMap } from '@angular/router';
import {
AdvancedWorkflowActionSelectReviewerComponent,
ADVANCED_WORKFLOW_TASK_OPTION_SELECT_REVIEWER
} from './advanced-workflow-action-select-reviewer.component';
import { ActivatedRoute } from '@angular/router';
import { WorkflowItemDataService } from '../../../core/submission/workflowitem-data.service';
import { WorkflowItemDataServiceStub } from '../../../shared/testing/workflow-item-data-service.stub';
import { RouterTestingModule } from '@angular/router/testing';
@@ -14,10 +17,19 @@ 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';
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>;
@@ -46,11 +58,13 @@ describe('AdvancedWorkflowActionSelectReviewerComponent', () => {
useValue: {
data: observableOf({
id: workflowId,
wfi: createSuccessfulRemoteDataObject(workflowItem),
}),
snapshot: {
queryParams: convertToParamMap({
queryParams: {
claimedTask: claimedTaskId,
workflow: 'testaction',
}),
},
},
},
},
@@ -60,6 +74,7 @@ describe('AdvancedWorkflowActionSelectReviewerComponent', () => {
{ provide: WorkflowActionDataService, useValue: workflowActionDataService },
{ provide: WorkflowItemDataService, useValue: workflowItemDataService },
],
schemas: [NO_ERRORS_SCHEMA],
}).compileComponents();
});
@@ -67,9 +82,49 @@ describe('AdvancedWorkflowActionSelectReviewerComponent', () => {
fixture = TestBed.createComponent(AdvancedWorkflowActionSelectReviewerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
spyOn(component, 'previousPage');
});
it('should create', () => {
expect(component).toBeTruthy();
afterEach(() => {
fixture.debugElement.nativeElement.remove();
});
describe('performAction', () => {
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

@@ -5,17 +5,20 @@ import {
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';
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';
export const WORKFLOW_ADVANCED_TASK_OPTION_SELECT_REVIEWER = 'submit_select_reviewer';
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',
@@ -75,7 +78,7 @@ export class AdvancedWorkflowActionSelectReviewerComponent extends AdvancedWorkf
}
this.subs.push(this.workflowAction$.subscribe((workflowAction: WorkflowAction) => {
if (workflowAction) {
this.groupId = (workflowAction.advancedInfo as SelectReviewerActionAdvancedInfo[])[0].group;
this.groupId = (workflowAction.advancedInfo as SelectReviewerAdvancedWorkflowInfo[])[0].group;
} else {
this.groupId = null;
}
@@ -86,18 +89,23 @@ export class AdvancedWorkflowActionSelectReviewerComponent extends AdvancedWorkf
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;
}
console.log(this.displayError);
}
/**
* Returns the task option and the selected {@link EPerson} id(s)
*/
createBody(): any {
return {
[WORKFLOW_ADVANCED_TASK_OPTION_SELECT_REVIEWER]: true,
[ADVANCED_WORKFLOW_TASK_OPTION_SELECT_REVIEWER]: true,
eperson: this.selectedReviewers.map((ePerson: EPerson) => ePerson.id),
};
}

View File

@@ -1,6 +1,6 @@
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 { 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';
@@ -31,6 +31,7 @@ import { NotificationsServiceStub } from '../../../../shared/testing/notificatio
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;
@@ -45,6 +46,8 @@ describe('ReviewersListComponent', () => {
let epersonMembers;
let subgroupMembers;
let paginationService;
let ePersonDtoModel1: EpersonDtoModel;
let ePersonDtoModel2: EpersonDtoModel;
beforeEach(waitForAsync(() => {
activeGroup = GroupMock;
@@ -119,7 +122,6 @@ describe('ReviewersListComponent', () => {
findById(id: string) {
for (const group of allGroups) {
if (group.id === id) {
console.log('found', group);
return createSuccessfulRemoteDataObject$(group);
}
}
@@ -167,9 +169,12 @@ describe('ReviewersListComponent', () => {
fixture.debugElement.nativeElement.remove();
}));
it('should create ReviewersListComponent', inject([ReviewersListComponent], (comp: ReviewersListComponent) => {
expect(comp).toBeDefined();
}));
beforeEach(() => {
ePersonDtoModel1 = new EpersonDtoModel();
ePersonDtoModel1.eperson = EPersonMock;
ePersonDtoModel2 = new EpersonDtoModel();
ePersonDtoModel2.eperson = EPersonMock2;
});
describe('when no group is selected', () => {
beforeEach(() => {
@@ -179,18 +184,18 @@ describe('ReviewersListComponent', () => {
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);
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', () => {
describe('when a group is selected', () => {
beforeEach(() => {
component.ngOnChanges({
groupId: new SimpleChange(undefined, GroupMock.id, true)
@@ -198,15 +203,50 @@ describe('ReviewersListComponent', () => {
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);
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

@@ -14,7 +14,8 @@ import { Observable, of as observableOf } from 'rxjs';
import { hasValue } from '../../../../shared/empty.util';
import { PaginatedList } from '../../../../core/data/paginated-list.model';
import {
MembersListComponent, EPersonListActionConfig
MembersListComponent,
EPersonListActionConfig,
} from '../../../../access-control/group-registry/group-form/members-list/members-list.component';
/**
@@ -26,6 +27,9 @@ enum SubKey {
SearchResultsDTO,
}
/**
* A custom {@link MembersListComponent} for the advanced SelectReviewer workflow.
*/
@Component({
selector: 'ds-reviewers-list',
// templateUrl: './reviewers-list.component.html',
@@ -83,6 +87,12 @@ export class ReviewersListComponent extends MembersListComponent implements OnIn
}
}
/**
* 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) {
@@ -95,19 +105,35 @@ export class ReviewersListComponent extends MembersListComponent implements OnIn
}
}
/**
* 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));
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) {

View File

@@ -5,7 +5,7 @@ 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, convertToParamMap } from '@angular/router';
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';
@@ -54,9 +54,9 @@ describe('AdvancedWorkflowActionComponent', () => {
id: workflowId,
}),
snapshot: {
queryParams: convertToParamMap({
queryParams: {
workflow: 'testaction',
}),
},
},
},
},
@@ -108,7 +108,8 @@ describe('AdvancedWorkflowActionComponent', () => {
});
@Component({
selector: 'ds-test-cmp',
// eslint-disable-next-line @angular-eslint/component-selector
selector: '',
template: ''
})
class TestComponent extends AdvancedWorkflowActionComponent {

View File

@@ -13,6 +13,12 @@ import { ClaimedTaskDataService } from '../../../core/tasks/claimed-task-data.se
import { map } from 'rxjs/operators';
import { ProcessTaskResponse } from '../../../core/tasks/models/process-task-response';
/**
* Abstract component for rendering an advanced claimed task's workflow page
* To create a child-component for a new option:
* - Set the "getType()" of the component
* - Implement the createBody, should always contain at least the ADVANCED_WORKFLOW_TASK_OPTION
*/
@Component({
selector: 'ds-advanced-workflow-action',
template: '',
@@ -62,7 +68,7 @@ export abstract class AdvancedWorkflowActionComponent extends WorkflowItemAction
/**
* Submits the task with the given {@link createBody}.
*
* @param id
* @param id The task id
*/
sendRequest(id: string): Observable<boolean> {
return this.claimedTaskDataService.submitTask(id, this.createBody()).pipe(

View File

@@ -1 +1 @@
<ng-template dsAdvancedClaimedTaskActions></ng-template>
<ng-template dsAdvancedWorkflowActions></ng-template>

View File

@@ -2,6 +2,15 @@ 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;
@@ -14,21 +23,61 @@ describe('AdvancedWorkflowActionsLoaderComponent', () => {
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();
});
it('should create', () => {
expect(component).toBeTruthy();
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

@@ -3,10 +3,13 @@ import { hasValue } from '../../../shared/empty.util';
import {
getAdvancedComponentByWorkflowTaskOption
} from '../../../shared/mydspace-actions/claimed-task/switcher/claimed-task-actions-decorator';
import { AdvancedClaimedTaskActionsDirective } from './advanced-claimed-task-actions.directive';
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',
@@ -23,7 +26,7 @@ export class AdvancedWorkflowActionsLoaderComponent implements OnInit {
/**
* Directive to determine where the dynamic child component is located
*/
@ViewChild(AdvancedClaimedTaskActionsDirective, { static: true }) claimedTaskActionsDirective: AdvancedClaimedTaskActionsDirective;
@ViewChild(AdvancedWorkflowActionsDirective, { static: true }) claimedTaskActionsDirective: AdvancedWorkflowActionsDirective;
constructor(
private componentFactoryResolver: ComponentFactoryResolver,
@@ -47,8 +50,8 @@ export class AdvancedWorkflowActionsLoaderComponent implements OnInit {
}
}
getComponentByWorkflowTaskOption(option: string) {
return getAdvancedComponentByWorkflowTaskOption(option);
getComponentByWorkflowTaskOption(type: string): any {
return getAdvancedComponentByWorkflowTaskOption(type);
}
}

View File

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

View File

@@ -24,8 +24,8 @@ import {
AdvancedWorkflowActionPageComponent
} from './advanced-workflow-action/advanced-workflow-action-page/advanced-workflow-action-page.component';
import {
AdvancedClaimedTaskActionsDirective
} from './advanced-workflow-action/advanced-workflow-actions-loader/advanced-claimed-task-actions.directive';
AdvancedWorkflowActionsDirective
} from './advanced-workflow-action/advanced-workflow-actions-loader/advanced-workflow-actions.directive';
import { AccessControlModule } from '../access-control/access-control.module';
import {
ReviewersListComponent
@@ -54,7 +54,7 @@ import { RatingModule } from 'ngx-bootstrap/rating';
AdvancedWorkflowActionRatingComponent,
AdvancedWorkflowActionSelectReviewerComponent,
AdvancedWorkflowActionPageComponent,
AdvancedClaimedTaskActionsDirective,
AdvancedWorkflowActionsDirective,
ReviewersListComponent,
]
})