mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
Merge branch 'main' into CST-7755-refactoring
# Conflicts: # src/app/shared/shared.module.ts
This commit is contained in:
@@ -27,7 +27,10 @@ export const ValidateEmailErrorStateMatcher: DynamicErrorMessagesMatcher =
|
||||
SharedModule,
|
||||
RouterModule,
|
||||
AccessControlRoutingModule,
|
||||
FormModule
|
||||
FormModule,
|
||||
],
|
||||
exports: [
|
||||
MembersListComponent,
|
||||
],
|
||||
declarations: [
|
||||
EPeopleRegistryComponent,
|
||||
@@ -35,7 +38,7 @@ export const ValidateEmailErrorStateMatcher: DynamicErrorMessagesMatcher =
|
||||
GroupsRegistryComponent,
|
||||
GroupFormComponent,
|
||||
SubgroupsListComponent,
|
||||
MembersListComponent
|
||||
MembersListComponent,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
|
@@ -65,18 +65,20 @@
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
<div class="btn-group edit-field">
|
||||
<button *ngIf="(ePerson.memberOfGroup)"
|
||||
<button *ngIf="ePerson.memberOfGroup"
|
||||
(click)="deleteMemberFromGroup(ePerson)"
|
||||
class="btn btn-outline-danger btn-sm"
|
||||
[disabled]="actionConfig.remove.disabled"
|
||||
[ngClass]="['btn btn-sm', actionConfig.remove.css]"
|
||||
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: {name: ePerson.eperson.name} }}">
|
||||
<i class="fas fa-trash-alt fa-fw"></i>
|
||||
<i [ngClass]="actionConfig.remove.icon"></i>
|
||||
</button>
|
||||
|
||||
<button *ngIf="!(ePerson.memberOfGroup)"
|
||||
<button *ngIf="!ePerson.memberOfGroup"
|
||||
(click)="addMemberToGroup(ePerson)"
|
||||
class="btn btn-outline-primary btn-sm"
|
||||
[disabled]="actionConfig.add.disabled"
|
||||
[ngClass]="['btn btn-sm', actionConfig.add.css]"
|
||||
title="{{messagePrefix + '.table.edit.buttons.add' | translate: {name: ePerson.eperson.name} }}">
|
||||
<i class="fas fa-plus fa-fw"></i>
|
||||
<i [ngClass]="actionConfig.add.icon"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
@@ -123,10 +125,19 @@
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
<div class="btn-group edit-field">
|
||||
<button (click)="deleteMemberFromGroup(ePerson)"
|
||||
class="btn btn-outline-danger btn-sm"
|
||||
<button *ngIf="ePerson.memberOfGroup"
|
||||
(click)="deleteMemberFromGroup(ePerson)"
|
||||
[disabled]="actionConfig.remove.disabled"
|
||||
[ngClass]="['btn btn-sm', actionConfig.remove.css]"
|
||||
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: {name: ePerson.eperson.name} }}">
|
||||
<i class="fas fa-trash-alt fa-fw"></i>
|
||||
<i [ngClass]="actionConfig.remove.icon"></i>
|
||||
</button>
|
||||
<button *ngIf="!ePerson.memberOfGroup"
|
||||
(click)="addMemberToGroup(ePerson)"
|
||||
[disabled]="actionConfig.add.disabled"
|
||||
[ngClass]="['btn btn-sm', actionConfig.add.css]"
|
||||
title="{{messagePrefix + '.table.edit.buttons.add' | translate: {name: ePerson.eperson.name} }}">
|
||||
<i [ngClass]="actionConfig.add.icon"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
|
@@ -149,6 +149,7 @@ describe('MembersListComponent', () => {
|
||||
fixture.destroy();
|
||||
flush();
|
||||
component = null;
|
||||
fixture.debugElement.nativeElement.remove();
|
||||
}));
|
||||
|
||||
it('should create MembersListComponent', inject([MembersListComponent], (comp: MembersListComponent) => {
|
||||
|
@@ -11,7 +11,7 @@ import {
|
||||
ObservedValueOf,
|
||||
} from 'rxjs';
|
||||
import { defaultIfEmpty, map, mergeMap, switchMap, take } from 'rxjs/operators';
|
||||
import {buildPaginatedList, PaginatedList} from '../../../../core/data/paginated-list.model';
|
||||
import { buildPaginatedList, PaginatedList } from '../../../../core/data/paginated-list.model';
|
||||
import { RemoteData } from '../../../../core/data/remote-data';
|
||||
import { EPersonDataService } from '../../../../core/eperson/eperson-data.service';
|
||||
import { GroupDataService } from '../../../../core/eperson/group-data.service';
|
||||
@@ -19,11 +19,13 @@ import { EPerson } from '../../../../core/eperson/models/eperson.model';
|
||||
import { Group } from '../../../../core/eperson/models/group.model';
|
||||
import {
|
||||
getFirstSucceededRemoteData,
|
||||
getFirstCompletedRemoteData, getAllCompletedRemoteData, getRemoteDataPayload
|
||||
getFirstCompletedRemoteData,
|
||||
getAllCompletedRemoteData,
|
||||
getRemoteDataPayload
|
||||
} from '../../../../core/shared/operators';
|
||||
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
||||
import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model';
|
||||
import {EpersonDtoModel} from '../../../../core/eperson/models/eperson-dto.model';
|
||||
import { EpersonDtoModel } from '../../../../core/eperson/models/eperson-dto.model';
|
||||
import { PaginationService } from '../../../../core/pagination/pagination.service';
|
||||
|
||||
/**
|
||||
@@ -35,6 +37,35 @@ enum SubKey {
|
||||
SearchResultsDTO,
|
||||
}
|
||||
|
||||
/**
|
||||
* The layout config of the buttons in the last column
|
||||
*/
|
||||
export interface EPersonActionConfig {
|
||||
/**
|
||||
* The css classes that should be added to the button
|
||||
*/
|
||||
css?: string;
|
||||
/**
|
||||
* Whether the button should be disabled
|
||||
*/
|
||||
disabled: boolean;
|
||||
/**
|
||||
* The Font Awesome icon that should be used
|
||||
*/
|
||||
icon: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link EPersonActionConfig} that should be used to display the button. The remove config will be used when the
|
||||
* {@link EPerson} is already a member of the {@link Group} and the remove config will be used otherwise.
|
||||
*
|
||||
* *See {@link actionConfig} for an example*
|
||||
*/
|
||||
export interface EPersonListActionConfig {
|
||||
add: EPersonActionConfig;
|
||||
remove: EPersonActionConfig;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'ds-members-list',
|
||||
templateUrl: './members-list.component.html'
|
||||
@@ -47,6 +78,20 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
||||
@Input()
|
||||
messagePrefix: string;
|
||||
|
||||
@Input()
|
||||
actionConfig: EPersonListActionConfig = {
|
||||
add: {
|
||||
css: 'btn-outline-primary',
|
||||
disabled: false,
|
||||
icon: 'fas fa-plus fa-fw',
|
||||
},
|
||||
remove: {
|
||||
css: 'btn-outline-danger',
|
||||
disabled: false,
|
||||
icon: 'fas fa-trash-alt fa-fw'
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* EPeople being displayed in search result, initially all members, after search result of search
|
||||
*/
|
||||
@@ -91,21 +136,20 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
||||
// current active group being edited
|
||||
groupBeingEdited: Group;
|
||||
|
||||
paginationSub: Subscription;
|
||||
|
||||
|
||||
constructor(private groupDataService: GroupDataService,
|
||||
public ePersonDataService: EPersonDataService,
|
||||
private translateService: TranslateService,
|
||||
private notificationsService: NotificationsService,
|
||||
private formBuilder: FormBuilder,
|
||||
private paginationService: PaginationService,
|
||||
private router: Router) {
|
||||
constructor(
|
||||
protected groupDataService: GroupDataService,
|
||||
public ePersonDataService: EPersonDataService,
|
||||
protected translateService: TranslateService,
|
||||
protected notificationsService: NotificationsService,
|
||||
protected formBuilder: FormBuilder,
|
||||
protected paginationService: PaginationService,
|
||||
private router: Router
|
||||
) {
|
||||
this.currentSearchQuery = '';
|
||||
this.currentSearchScope = 'metadata';
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
ngOnInit(): void {
|
||||
this.searchForm = this.formBuilder.group(({
|
||||
scope: 'metadata',
|
||||
query: '',
|
||||
@@ -124,7 +168,7 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
||||
* @param page the number of the page to retrieve
|
||||
* @private
|
||||
*/
|
||||
private retrieveMembers(page: number) {
|
||||
retrieveMembers(page: number): void {
|
||||
this.unsubFrom(SubKey.MembersDTO);
|
||||
this.subs.set(SubKey.MembersDTO,
|
||||
this.paginationService.getCurrentPagination(this.config.id, this.config).pipe(
|
||||
@@ -135,36 +179,36 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
);
|
||||
}),
|
||||
getAllCompletedRemoteData(),
|
||||
map((rd: RemoteData<any>) => {
|
||||
if (rd.hasFailed) {
|
||||
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure', {cause: rd.errorMessage}));
|
||||
} else {
|
||||
return rd;
|
||||
}
|
||||
}),
|
||||
switchMap((epersonListRD: RemoteData<PaginatedList<EPerson>>) => {
|
||||
const dtos$ = observableCombineLatest([...epersonListRD.payload.page.map((member: EPerson) => {
|
||||
const dto$: Observable<EpersonDtoModel> = observableCombineLatest(
|
||||
this.isMemberOfGroup(member), (isMember: ObservedValueOf<Observable<boolean>>) => {
|
||||
const epersonDtoModel: EpersonDtoModel = new EpersonDtoModel();
|
||||
epersonDtoModel.eperson = member;
|
||||
epersonDtoModel.memberOfGroup = isMember;
|
||||
return epersonDtoModel;
|
||||
});
|
||||
return dto$;
|
||||
})]);
|
||||
return dtos$.pipe(defaultIfEmpty([]), map((dtos: EpersonDtoModel[]) => {
|
||||
return buildPaginatedList(epersonListRD.payload.pageInfo, dtos);
|
||||
getAllCompletedRemoteData(),
|
||||
map((rd: RemoteData<any>) => {
|
||||
if (rd.hasFailed) {
|
||||
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure', { cause: rd.errorMessage }));
|
||||
} else {
|
||||
return rd;
|
||||
}
|
||||
}),
|
||||
switchMap((epersonListRD: RemoteData<PaginatedList<EPerson>>) => {
|
||||
const dtos$ = observableCombineLatest([...epersonListRD.payload.page.map((member: EPerson) => {
|
||||
const dto$: Observable<EpersonDtoModel> = observableCombineLatest(
|
||||
this.isMemberOfGroup(member), (isMember: ObservedValueOf<Observable<boolean>>) => {
|
||||
const epersonDtoModel: EpersonDtoModel = new EpersonDtoModel();
|
||||
epersonDtoModel.eperson = member;
|
||||
epersonDtoModel.memberOfGroup = isMember;
|
||||
return epersonDtoModel;
|
||||
});
|
||||
return dto$;
|
||||
})]);
|
||||
return dtos$.pipe(defaultIfEmpty([]), map((dtos: EpersonDtoModel[]) => {
|
||||
return buildPaginatedList(epersonListRD.payload.pageInfo, dtos);
|
||||
}));
|
||||
}))
|
||||
.subscribe((paginatedListOfDTOs: PaginatedList<EpersonDtoModel>) => {
|
||||
this.ePeopleMembersOfGroupDtos.next(paginatedListOfDTOs);
|
||||
}));
|
||||
}))
|
||||
.subscribe((paginatedListOfDTOs: PaginatedList<EpersonDtoModel>) => {
|
||||
this.ePeopleMembersOfGroupDtos.next(paginatedListOfDTOs);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the given ePerson is a member of the group currently being edited
|
||||
* Whether the given ePerson is a member of the group currently being edited
|
||||
* @param possibleMember EPerson that is a possible member (being tested) of the group currently being edited
|
||||
*/
|
||||
isMemberOfGroup(possibleMember: EPerson): Observable<boolean> {
|
||||
@@ -193,7 +237,7 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
||||
* @param key The key of the subscription to unsubscribe from
|
||||
* @private
|
||||
*/
|
||||
private unsubFrom(key: SubKey) {
|
||||
protected unsubFrom(key: SubKey) {
|
||||
if (this.subs.has(key)) {
|
||||
this.subs.get(key).unsubscribe();
|
||||
this.subs.delete(key);
|
||||
@@ -267,7 +311,7 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
||||
getAllCompletedRemoteData(),
|
||||
map((rd: RemoteData<any>) => {
|
||||
if (rd.hasFailed) {
|
||||
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure', {cause: rd.errorMessage}));
|
||||
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure', { cause: rd.errorMessage }));
|
||||
} else {
|
||||
return rd;
|
||||
}
|
||||
|
@@ -6,6 +6,7 @@ import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { Collection } from '../../../core/shared/collection.model';
|
||||
import { getRemoteDataPayload, getFirstSucceededRemoteData } from '../../../core/shared/operators';
|
||||
import { HALLink } from '../../../core/shared/hal-link.model';
|
||||
import { hasValue } from '../../../shared/empty.util';
|
||||
|
||||
/**
|
||||
* Component for managing a collection's roles
|
||||
@@ -45,25 +46,31 @@ export class CollectionRolesComponent implements OnInit {
|
||||
);
|
||||
|
||||
this.comcolRoles$ = this.collection$.pipe(
|
||||
map((collection) => [
|
||||
{
|
||||
name: 'collection-admin',
|
||||
href: collection._links.adminGroup.href,
|
||||
},
|
||||
{
|
||||
name: 'submitters',
|
||||
href: collection._links.submittersGroup.href,
|
||||
},
|
||||
{
|
||||
name: 'item_read',
|
||||
href: collection._links.itemReadGroup.href,
|
||||
},
|
||||
{
|
||||
name: 'bitstream_read',
|
||||
href: collection._links.bitstreamReadGroup.href,
|
||||
},
|
||||
...collection._links.workflowGroups,
|
||||
]),
|
||||
map((collection) => {
|
||||
let workflowGroups: HALLink[] | HALLink = hasValue(collection._links.workflowGroups) ? collection._links.workflowGroups : [];
|
||||
if (!Array.isArray(workflowGroups)) {
|
||||
workflowGroups = [workflowGroups];
|
||||
}
|
||||
return [
|
||||
{
|
||||
name: 'collection-admin',
|
||||
href: collection._links.adminGroup.href,
|
||||
},
|
||||
{
|
||||
name: 'submitters',
|
||||
href: collection._links.submittersGroup.href,
|
||||
},
|
||||
{
|
||||
name: 'item_read',
|
||||
href: collection._links.itemReadGroup.href,
|
||||
},
|
||||
{
|
||||
name: 'bitstream_read',
|
||||
href: collection._links.bitstreamReadGroup.href,
|
||||
},
|
||||
...workflowGroups,
|
||||
];
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -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';
|
||||
@@ -341,6 +344,9 @@ export const models =
|
||||
Version,
|
||||
VersionHistory,
|
||||
WorkflowAction,
|
||||
AdvancedWorkflowInfo,
|
||||
RatingAdvancedWorkflowInfo,
|
||||
SelectReviewerAdvancedWorkflowInfo,
|
||||
TemplateItem,
|
||||
Feature,
|
||||
Authorization,
|
||||
|
@@ -594,6 +594,19 @@ describe('RequestService', () => {
|
||||
'property1=multiple%0Alines%0Ato%0Asend&property2=sp%26ci%40l%20characters&sp%26ci%40l-chars%20in%20prop=test123'
|
||||
);
|
||||
});
|
||||
|
||||
it('should properly encode the body with an array', () => {
|
||||
const body = {
|
||||
'property1': 'multiple\nlines\nto\nsend',
|
||||
'property2': 'sp&ci@l characters',
|
||||
'sp&ci@l-chars in prop': 'test123',
|
||||
'arrayParam': ['arrayValue1', 'arrayValue2'],
|
||||
};
|
||||
const queryParams = service.uriEncodeBody(body);
|
||||
expect(queryParams).toEqual(
|
||||
'property1=multiple%0Alines%0Ato%0Asend&property2=sp%26ci%40l%20characters&sp%26ci%40l-chars%20in%20prop=test123&arrayParam=arrayValue1&arrayParam=arrayValue2'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setStaleByUUID', () => {
|
||||
|
@@ -255,8 +255,8 @@ export class RequestService {
|
||||
/**
|
||||
* Convert request Payload to a URL-encoded string
|
||||
*
|
||||
* e.g. uriEncodeBody({param: value, param1: value1})
|
||||
* returns: param=value¶m1=value1
|
||||
* e.g. uriEncodeBody({param: value, param1: value1, param2: [value3, value4]})
|
||||
* returns: param=value¶m1=value1¶m2=value3¶m2=value4
|
||||
*
|
||||
* @param body
|
||||
* The request Payload to convert
|
||||
@@ -267,11 +267,19 @@ export class RequestService {
|
||||
let queryParams = '';
|
||||
if (isNotEmpty(body) && typeof body === 'object') {
|
||||
Object.keys(body)
|
||||
.forEach((param) => {
|
||||
.forEach((param: string) => {
|
||||
const encodedParam = encodeURIComponent(param);
|
||||
const encodedBody = encodeURIComponent(body[param]);
|
||||
const paramValue = `${encodedParam}=${encodedBody}`;
|
||||
queryParams = isEmpty(queryParams) ? queryParams.concat(paramValue) : queryParams.concat('&', paramValue);
|
||||
if (Array.isArray(body[param])) {
|
||||
for (const element of body[param]) {
|
||||
const encodedBody = encodeURIComponent(element);
|
||||
const paramValue = `${encodedParam}=${encodedBody}`;
|
||||
queryParams = isEmpty(queryParams) ? queryParams.concat(paramValue) : queryParams.concat('&', paramValue);
|
||||
}
|
||||
} else {
|
||||
const encodedBody = encodeURIComponent(body[param]);
|
||||
const paramValue = `${encodedParam}=${encodedBody}`;
|
||||
queryParams = isEmpty(queryParams) ? queryParams.concat(paramValue) : queryParams.concat('&', paramValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
return queryParams;
|
||||
|
@@ -5,15 +5,15 @@ import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { WORKFLOW_ACTION } from '../tasks/models/workflow-action-object.resource-type';
|
||||
import { BaseDataService } from './base/base-data.service';
|
||||
import { dataService } from './base/data-service.decorator';
|
||||
import { IdentifiableDataService } from './base/identifiable-data.service';
|
||||
|
||||
/**
|
||||
* A service responsible for fetching/sending data from/to the REST API on the workflowactions endpoint
|
||||
*/
|
||||
@Injectable()
|
||||
@dataService(WORKFLOW_ACTION)
|
||||
export class WorkflowActionDataService extends BaseDataService<WorkflowAction> {
|
||||
export class WorkflowActionDataService extends IdentifiableDataService<WorkflowAction> {
|
||||
protected linkPath = 'workflowactions';
|
||||
|
||||
constructor(
|
||||
|
11
src/app/core/tasks/models/advanced-workflow-info.model.ts
Normal file
11
src/app/core/tasks/models/advanced-workflow-info.model.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { autoserialize } from 'cerialize';
|
||||
|
||||
/**
|
||||
* An abstract model class for a {@link AdvancedWorkflowInfo}
|
||||
*/
|
||||
export abstract class AdvancedWorkflowInfo {
|
||||
|
||||
@autoserialize
|
||||
id: string;
|
||||
|
||||
}
|
@@ -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');
|
@@ -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;
|
||||
|
||||
}
|
@@ -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;
|
||||
|
||||
}
|
@@ -2,6 +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 { AdvancedWorkflowInfo } from './advanced-workflow-info.model';
|
||||
|
||||
/**
|
||||
* A model class for a WorkflowAction
|
||||
@@ -22,4 +23,23 @@ export class WorkflowAction extends DSpaceObject {
|
||||
*/
|
||||
@autoserialize
|
||||
options: string[];
|
||||
|
||||
/**
|
||||
* Whether this action has advanced options
|
||||
*/
|
||||
@autoserialize
|
||||
advanced: boolean;
|
||||
|
||||
/**
|
||||
* The advanced options that the user can select at this action
|
||||
*/
|
||||
@autoserialize
|
||||
advancedOptions: string[];
|
||||
|
||||
/**
|
||||
* The advanced info required by the advanced options
|
||||
*/
|
||||
@autoserialize
|
||||
advancedInfo: AdvancedWorkflowInfo[];
|
||||
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
<table id="metadata" class="table table-striped table-hover">
|
||||
<table id="metadata" class="table table-striped table-hover table-responsive">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{'item.edit.modify.overview.field'| translate}}</th>
|
||||
|
@@ -25,6 +25,7 @@ import { ThemedItemListPreviewComponent } from '../shared/object-list/my-dspace-
|
||||
import { MyDSpaceItemStatusComponent } from '../shared/object-collection/shared/mydspace-item-status/my-dspace-item-status.component';
|
||||
import { JournalEntitiesModule } from '../entity-groups/journal-entities/journal-entities.module';
|
||||
import { MyDSpaceActionsModule } from '../shared/mydspace-actions/mydspace-actions.module';
|
||||
import { ClaimedDeclinedTaskSearchResultListElementComponent } from '../shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-declined-task-search-result/claimed-declined-task-search-result-list-element.component';
|
||||
|
||||
const ENTRY_COMPONENTS = [
|
||||
WorkspaceItemSearchResultListElementComponent,
|
||||
@@ -32,6 +33,7 @@ const ENTRY_COMPONENTS = [
|
||||
ClaimedSearchResultListElementComponent,
|
||||
ClaimedApprovedSearchResultListElementComponent,
|
||||
ClaimedDeclinedSearchResultListElementComponent,
|
||||
ClaimedDeclinedTaskSearchResultListElementComponent,
|
||||
PoolSearchResultListElementComponent,
|
||||
ItemSearchResultDetailElementComponent,
|
||||
WorkspaceItemSearchResultDetailElementComponent,
|
||||
|
@@ -0,0 +1,86 @@
|
||||
import { Component, OnInit, Injector } from '@angular/core';
|
||||
import { ClaimedTaskActionsAbstractComponent } from './claimed-task-actions-abstract.component';
|
||||
import { getFirstSucceededRemoteDataPayload } from '../../../../core/shared/operators';
|
||||
import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model';
|
||||
import { getAdvancedWorkflowRoute } from '../../../../workflowitems-edit-page/workflowitems-edit-page-routing-paths';
|
||||
import { Params, Router, ActivatedRoute, NavigationExtras } from '@angular/router';
|
||||
import { NotificationsService } from '../../../notifications/notifications.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { SearchService } from '../../../../core/shared/search/search.service';
|
||||
import { RequestService } from '../../../../core/data/request.service';
|
||||
|
||||
/**
|
||||
* Abstract component for rendering an advanced claimed task's action
|
||||
* To create a child-component for a new option:
|
||||
* - Set the "{@link option}" and "{@link workflowType}" of the component
|
||||
* - Add a @{@link rendersWorkflowTaskOption} annotation to your component providing the same enum value
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-advanced-claimed-task-action-abstract',
|
||||
template: ''
|
||||
})
|
||||
export abstract class AdvancedClaimedTaskActionsAbstractComponent extends ClaimedTaskActionsAbstractComponent implements OnInit {
|
||||
|
||||
/**
|
||||
* The {@link WorkflowAction} id of the advanced workflow that needs to be opened.
|
||||
*/
|
||||
abstract workflowType: string;
|
||||
|
||||
/**
|
||||
* Route to the workflow's task page
|
||||
*/
|
||||
workflowTaskPageRoute: string;
|
||||
|
||||
constructor(
|
||||
protected injector: Injector,
|
||||
protected router: Router,
|
||||
protected notificationsService: NotificationsService,
|
||||
protected translate: TranslateService,
|
||||
protected searchService: SearchService,
|
||||
protected requestService: RequestService,
|
||||
protected route: ActivatedRoute,
|
||||
) {
|
||||
super(injector, router, notificationsService, translate, searchService, requestService);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
super.ngOnInit();
|
||||
this.initPageRoute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise the route to the advanced workflow page
|
||||
*/
|
||||
initPageRoute() {
|
||||
this.subs.push(this.object.workflowitem.pipe(
|
||||
getFirstSucceededRemoteDataPayload()
|
||||
).subscribe((workflowItem: WorkflowItem) => {
|
||||
this.workflowTaskPageRoute = getAdvancedWorkflowRoute(workflowItem.id);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates to the advanced workflow page based on the {@link workflow}.
|
||||
*/
|
||||
openAdvancedClaimedTaskTab(): void {
|
||||
const navigationExtras: NavigationExtras = {
|
||||
queryParams: this.getQueryParams(),
|
||||
};
|
||||
if (Object.keys(this.route.snapshot.queryParams).length > 0) {
|
||||
navigationExtras.state = {};
|
||||
navigationExtras.state.previousQueryParams = this.route.snapshot.queryParams;
|
||||
}
|
||||
void this.router.navigate([this.workflowTaskPageRoute], navigationExtras);
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link Params} that need to be given to the workflow page.
|
||||
*/
|
||||
getQueryParams(): Params {
|
||||
return {
|
||||
workflow: this.workflowType,
|
||||
claimedTask: this.object.id,
|
||||
};
|
||||
}
|
||||
|
||||
}
|
@@ -12,12 +12,5 @@
|
||||
[routerLink]="[getWorkflowItemViewRoute(workflowitem)]">
|
||||
<i class="fa fa-info-circle"></i> {{"submission.workflow.generic.view" | translate}}
|
||||
</button>
|
||||
|
||||
<ds-claimed-task-actions-loader [item]="item"
|
||||
[option]="returnToPoolOption"
|
||||
[object]="object"
|
||||
[workflowitem]="workflowitem"
|
||||
(processCompleted)="this.processCompleted.emit($event)">
|
||||
</ds-claimed-task-actions-loader>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
@@ -14,7 +14,6 @@ import { RequestService } from '../../../core/data/request.service';
|
||||
import { SearchService } from '../../../core/shared/search/search.service';
|
||||
import { WorkflowAction } from '../../../core/tasks/models/workflow-action-object.model';
|
||||
import { WorkflowActionDataService } from '../../../core/data/workflow-action-data.service';
|
||||
import { WORKFLOW_TASK_OPTION_RETURN_TO_POOL } from './return-to-pool/claimed-task-actions-return-to-pool.component';
|
||||
import { getWorkflowItemViewRoute } from '../../../workflowitems-edit-page/workflowitems-edit-page-routing-paths';
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
|
||||
@@ -48,12 +47,6 @@ export class ClaimedTaskActionsComponent extends MyDSpaceActionsComponent<Claime
|
||||
*/
|
||||
public actionRD$: Observable<RemoteData<WorkflowAction>>;
|
||||
|
||||
/**
|
||||
* The option used to render the "return to pool" component
|
||||
* Every claimed task contains this option
|
||||
*/
|
||||
public returnToPoolOption = WORKFLOW_TASK_OPTION_RETURN_TO_POOL;
|
||||
|
||||
/**
|
||||
* Initialize instance variables
|
||||
*
|
||||
|
@@ -0,0 +1,12 @@
|
||||
<button (click)="submitTask()"
|
||||
[disabled]="processing$ | async"
|
||||
class="declineTaskAction btn btn-warning"
|
||||
ngbTooltip="{{'submission.workflow.tasks.claimed.decline_help' | translate}}"
|
||||
type="button">
|
||||
<span *ngIf="processing$ | async">
|
||||
<i class='fas fa-circle-notch fa-spin'></i> {{'submission.workflow.tasks.generic.processing' | translate}}
|
||||
</span>
|
||||
<span *ngIf="!(processing$ | async)">
|
||||
<i class="fa fa-ban"></i> {{'submission.workflow.tasks.claimed.decline' | translate}}
|
||||
</span>
|
||||
</button>
|
@@ -0,0 +1,90 @@
|
||||
import { ChangeDetectionStrategy, Injector, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||
import { TranslateLoaderMock } from '../../../mocks/translate-loader.mock';
|
||||
import { ClaimedTaskDataService } from '../../../../core/tasks/claimed-task-data.service';
|
||||
import { ClaimedTask } from '../../../../core/tasks/models/claimed-task-object.model';
|
||||
import { getMockSearchService } from '../../../mocks/search-service.mock';
|
||||
import { getMockRequestService } from '../../../mocks/request.service.mock';
|
||||
import { PoolTaskDataService } from '../../../../core/tasks/pool-task-data.service';
|
||||
import { NotificationsService } from '../../../notifications/notifications.service';
|
||||
import { NotificationsServiceStub } from '../../../testing/notifications-service.stub';
|
||||
import { Router } from '@angular/router';
|
||||
import { RouterStub } from '../../../testing/router.stub';
|
||||
import { SearchService } from '../../../../core/shared/search/search.service';
|
||||
import { RequestService } from '../../../../core/data/request.service';
|
||||
import { ClaimedTaskActionsDeclineTaskComponent } from './claimed-task-actions-decline-task.component';
|
||||
import { ClaimedTaskDataServiceStub } from '../../../testing/claimed-task-data-service.stub';
|
||||
|
||||
let component: ClaimedTaskActionsDeclineTaskComponent;
|
||||
let fixture: ComponentFixture<ClaimedTaskActionsDeclineTaskComponent>;
|
||||
|
||||
const searchService = getMockSearchService();
|
||||
|
||||
const requestService = getMockRequestService();
|
||||
|
||||
let mockPoolTaskDataService: PoolTaskDataService;
|
||||
|
||||
describe('ClaimedTaskActionsDeclineTaskComponent', () => {
|
||||
const object = Object.assign(new ClaimedTask(), { id: 'claimed-task-1' });
|
||||
|
||||
let claimedTaskService: ClaimedTaskDataServiceStub;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
claimedTaskService = new ClaimedTaskDataServiceStub();
|
||||
|
||||
mockPoolTaskDataService = new PoolTaskDataService(null, null, null, null);
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: TranslateLoaderMock
|
||||
}
|
||||
})
|
||||
],
|
||||
providers: [
|
||||
{ provide: ClaimedTaskDataService, useValue: claimedTaskService },
|
||||
{ provide: Injector, useValue: {} },
|
||||
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
|
||||
{ provide: Router, useValue: new RouterStub() },
|
||||
{ provide: SearchService, useValue: searchService },
|
||||
{ provide: RequestService, useValue: requestService },
|
||||
{ provide: PoolTaskDataService, useValue: mockPoolTaskDataService },
|
||||
],
|
||||
declarations: [ClaimedTaskActionsDeclineTaskComponent],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).overrideComponent(ClaimedTaskActionsDeclineTaskComponent, {
|
||||
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ClaimedTaskActionsDeclineTaskComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.object = object;
|
||||
spyOn(component, 'initReloadAnchor').and.returnValue(undefined);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fixture.debugElement.nativeElement.remove();
|
||||
});
|
||||
|
||||
it('should display decline button', () => {
|
||||
const btn = fixture.debugElement.query(By.css('.declineTaskAction'));
|
||||
|
||||
expect(btn).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should display spin icon when decline is pending', () => {
|
||||
component.processing$.next(true);
|
||||
fixture.detectChanges();
|
||||
|
||||
const span = fixture.debugElement.query(By.css('.declineTaskAction .fa-spin'));
|
||||
|
||||
expect(span).not.toBeNull();
|
||||
});
|
||||
|
||||
});
|
@@ -0,0 +1,50 @@
|
||||
import { Component, Injector } from '@angular/core';
|
||||
import { ClaimedTaskActionsAbstractComponent } from '../abstract/claimed-task-actions-abstract.component';
|
||||
import { rendersWorkflowTaskOption } from '../switcher/claimed-task-actions-decorator';
|
||||
import { Router } from '@angular/router';
|
||||
import { NotificationsService } from '../../../notifications/notifications.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { SearchService } from '../../../../core/shared/search/search.service';
|
||||
import { RequestService } from '../../../../core/data/request.service';
|
||||
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
|
||||
import {
|
||||
ClaimedDeclinedTaskTaskSearchResult
|
||||
} from '../../../object-collection/shared/claimed-declined-task-task-search-result.model';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
import { RemoteData } from 'src/app/core/data/remote-data';
|
||||
|
||||
export const WORKFLOW_TASK_OPTION_DECLINE_TASK = 'submit_decline_task';
|
||||
|
||||
@rendersWorkflowTaskOption(WORKFLOW_TASK_OPTION_DECLINE_TASK)
|
||||
@Component({
|
||||
selector: 'ds-claimed-task-actions-decline-task',
|
||||
templateUrl: './claimed-task-actions-decline-task.component.html',
|
||||
styleUrls: ['./claimed-task-actions-decline-task.component.scss']
|
||||
})
|
||||
/**
|
||||
* Component for displaying and processing the decline task action on a workflow task item
|
||||
*/
|
||||
export class ClaimedTaskActionsDeclineTaskComponent extends ClaimedTaskActionsAbstractComponent {
|
||||
|
||||
option = WORKFLOW_TASK_OPTION_DECLINE_TASK;
|
||||
|
||||
constructor(protected injector: Injector,
|
||||
protected router: Router,
|
||||
protected notificationsService: NotificationsService,
|
||||
protected translate: TranslateService,
|
||||
protected searchService: SearchService,
|
||||
protected requestService: RequestService) {
|
||||
super(injector, router, notificationsService, translate, searchService, requestService);
|
||||
}
|
||||
|
||||
reloadObjectExecution(): Observable<RemoteData<DSpaceObject> | DSpaceObject> {
|
||||
return observableOf(this.object);
|
||||
}
|
||||
|
||||
convertReloadedObject(dso: DSpaceObject): DSpaceObject {
|
||||
return Object.assign(new ClaimedDeclinedTaskTaskSearchResult(), dso, {
|
||||
indexableObject: dso
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,4 @@
|
||||
<button (click)="openAdvancedClaimedTaskTab()" class="ratingReviewerAction btn btn-primary"
|
||||
ngbTooltip="{{ 'submission.workflow.generic.' + option + '-help' | translate }}">
|
||||
<i class="fa fa-star-half-alt"></i> {{ 'submission.workflow.generic.' + option | translate}}
|
||||
</button>
|
@@ -0,0 +1,98 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { AdvancedClaimedTaskActionRatingComponent } from './advanced-claimed-task-action-rating.component';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { ClaimedTask } from '../../../../core/tasks/models/claimed-task-object.model';
|
||||
import { ClaimedTaskDataServiceStub } from '../../../testing/claimed-task-data-service.stub';
|
||||
import { NotificationsServiceStub } from '../../../testing/notifications-service.stub';
|
||||
import { RouterStub } from '../../../testing/router.stub';
|
||||
import { SearchServiceStub } from '../../../testing/search-service.stub';
|
||||
import { ClaimedTaskDataService } from '../../../../core/tasks/claimed-task-data.service';
|
||||
import { NotificationsService } from '../../../notifications/notifications.service';
|
||||
import { RequestService } from '../../../../core/data/request.service';
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
import { SearchService } from '../../../../core/shared/search/search.service';
|
||||
import { Location } from '@angular/common';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import {
|
||||
ADVANCED_WORKFLOW_ACTION_RATING
|
||||
} from '../../../../workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-rating/advanced-workflow-action-rating.component';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model';
|
||||
import { ActivatedRouteStub } from '../../../testing/active-router.stub';
|
||||
|
||||
const taskId = 'claimed-task-1';
|
||||
const workflowId = 'workflow-1';
|
||||
|
||||
describe('AdvancedClaimedTaskActionRatingComponent', () => {
|
||||
const object = Object.assign(new ClaimedTask(), {
|
||||
id: taskId,
|
||||
workflowitem: observableOf(Object.assign(new WorkflowItem(), {
|
||||
id: workflowId,
|
||||
})),
|
||||
});
|
||||
let component: AdvancedClaimedTaskActionRatingComponent;
|
||||
let fixture: ComponentFixture<AdvancedClaimedTaskActionRatingComponent>;
|
||||
|
||||
let claimedTaskDataService: ClaimedTaskDataServiceStub;
|
||||
let notificationService: NotificationsServiceStub;
|
||||
let route: ActivatedRouteStub;
|
||||
let router: RouterStub;
|
||||
let searchService: SearchServiceStub;
|
||||
|
||||
beforeEach(async () => {
|
||||
claimedTaskDataService = new ClaimedTaskDataServiceStub();
|
||||
notificationService = new NotificationsServiceStub();
|
||||
route = new ActivatedRouteStub();
|
||||
router = new RouterStub();
|
||||
searchService = new SearchServiceStub();
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [
|
||||
TranslateModule.forRoot(),
|
||||
],
|
||||
declarations: [
|
||||
AdvancedClaimedTaskActionRatingComponent,
|
||||
],
|
||||
providers: [
|
||||
{ provide: ActivatedRoute, useValue: route },
|
||||
{ provide: ClaimedTaskDataService, useValue: claimedTaskDataService },
|
||||
{ provide: NotificationsService, useValue: notificationService },
|
||||
{ provide: RequestService, useValue: {} },
|
||||
{ provide: Router, useValue: router },
|
||||
{ provide: SearchService, useValue: searchService },
|
||||
Location,
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AdvancedClaimedTaskActionRatingComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.object = object;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fixture.debugElement.nativeElement.remove();
|
||||
});
|
||||
|
||||
it('should display select reviewer button', () => {
|
||||
const btn = fixture.debugElement.query(By.css('.ratingReviewerAction'));
|
||||
|
||||
expect(btn).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should navigate to the advanced workflow page when clicked', () => {
|
||||
component.workflowTaskPageRoute = `/workflowitems/${workflowId}/advanced`;
|
||||
fixture.debugElement.query(By.css('.ratingReviewerAction')).nativeElement.click();
|
||||
|
||||
expect(router.navigate).toHaveBeenCalledWith([`/workflowitems/${workflowId}/advanced`], {
|
||||
queryParams: {
|
||||
workflow: ADVANCED_WORKFLOW_ACTION_RATING,
|
||||
claimedTask: taskId,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,46 @@
|
||||
import { Component, Injector } from '@angular/core';
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
import { NotificationsService } from '../../../notifications/notifications.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { SearchService } from '../../../../core/shared/search/search.service';
|
||||
import { RequestService } from '../../../../core/data/request.service';
|
||||
import {
|
||||
AdvancedClaimedTaskActionsAbstractComponent
|
||||
} from '../abstract/advanced-claimed-task-actions-abstract.component';
|
||||
import {
|
||||
ADVANCED_WORKFLOW_ACTION_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';
|
||||
|
||||
/**
|
||||
* 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',
|
||||
styleUrls: ['./advanced-claimed-task-action-rating.component.scss']
|
||||
})
|
||||
export class AdvancedClaimedTaskActionRatingComponent extends AdvancedClaimedTaskActionsAbstractComponent {
|
||||
|
||||
/**
|
||||
* This component represents the advanced select option
|
||||
*/
|
||||
option = ADVANCED_WORKFLOW_TASK_OPTION_RATING;
|
||||
|
||||
workflowType = ADVANCED_WORKFLOW_ACTION_RATING;
|
||||
|
||||
constructor(
|
||||
protected injector: Injector,
|
||||
protected router: Router,
|
||||
protected notificationsService: NotificationsService,
|
||||
protected translate: TranslateService,
|
||||
protected searchService: SearchService,
|
||||
protected requestService: RequestService,
|
||||
protected route: ActivatedRoute,
|
||||
) {
|
||||
super(injector, router, notificationsService, translate, searchService, requestService, route);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,4 @@
|
||||
<button (click)="openAdvancedClaimedTaskTab()" class="selectReviewerAction btn btn-primary"
|
||||
ngbTooltip="{{ 'submission.workflow.generic.' + option + '-help' | translate }}">
|
||||
<i class="fa fa-user"></i> {{ 'submission.workflow.generic.' + option | translate}}
|
||||
</button>
|
@@ -0,0 +1,102 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import {
|
||||
AdvancedClaimedTaskActionSelectReviewerComponent
|
||||
} from './advanced-claimed-task-action-select-reviewer.component';
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
import { RouterStub } from '../../../testing/router.stub';
|
||||
import { NotificationsServiceStub } from '../../../testing/notifications-service.stub';
|
||||
import { NotificationsService } from '../../../notifications/notifications.service';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { SearchServiceStub } from '../../../testing/search-service.stub';
|
||||
import { SearchService } from '../../../../core/shared/search/search.service';
|
||||
import { RequestService } from '../../../../core/data/request.service';
|
||||
import { ClaimedTaskDataService } from '../../../../core/tasks/claimed-task-data.service';
|
||||
import { ClaimedTaskDataServiceStub } from '../../../testing/claimed-task-data-service.stub';
|
||||
import { ClaimedTask } from '../../../../core/tasks/models/claimed-task-object.model';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { Location } from '@angular/common';
|
||||
import {
|
||||
ADVANCED_WORKFLOW_ACTION_SELECT_REVIEWER
|
||||
} from '../../../../workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-select-reviewer/advanced-workflow-action-select-reviewer.component';
|
||||
import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { ActivatedRouteStub } from '../../../testing/active-router.stub';
|
||||
|
||||
const taskId = 'claimed-task-1';
|
||||
const workflowId = 'workflow-1';
|
||||
|
||||
describe('AdvancedClaimedTaskActionSelectReviewerComponent', () => {
|
||||
const object = Object.assign(new ClaimedTask(), {
|
||||
id: taskId,
|
||||
workflowitem: observableOf(Object.assign(new WorkflowItem(), {
|
||||
id: workflowId,
|
||||
})),
|
||||
});
|
||||
let component: AdvancedClaimedTaskActionSelectReviewerComponent;
|
||||
let fixture: ComponentFixture<AdvancedClaimedTaskActionSelectReviewerComponent>;
|
||||
|
||||
let route: ActivatedRouteStub;
|
||||
let claimedTaskDataService: ClaimedTaskDataServiceStub;
|
||||
let notificationService: NotificationsServiceStub;
|
||||
let router: RouterStub;
|
||||
let searchService: SearchServiceStub;
|
||||
|
||||
beforeEach(async () => {
|
||||
route = new ActivatedRouteStub();
|
||||
claimedTaskDataService = new ClaimedTaskDataServiceStub();
|
||||
notificationService = new NotificationsServiceStub();
|
||||
router = new RouterStub();
|
||||
searchService = new SearchServiceStub();
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [
|
||||
TranslateModule.forRoot(),
|
||||
],
|
||||
declarations: [
|
||||
AdvancedClaimedTaskActionSelectReviewerComponent,
|
||||
NgbTooltip,
|
||||
],
|
||||
providers: [
|
||||
{ provide: ActivatedRoute, useValue: route },
|
||||
{ provide: ClaimedTaskDataService, useValue: claimedTaskDataService },
|
||||
{ provide: NotificationsService, useValue: notificationService },
|
||||
{ provide: RequestService, useValue: {} },
|
||||
{ provide: Router, useValue: router },
|
||||
{ provide: SearchService, useValue: searchService },
|
||||
Location,
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AdvancedClaimedTaskActionSelectReviewerComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.object = object;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fixture.debugElement.nativeElement.remove();
|
||||
});
|
||||
|
||||
it('should display select reviewer button', () => {
|
||||
const btn = fixture.debugElement.query(By.css('.selectReviewerAction'));
|
||||
|
||||
expect(btn).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should navigate to the advanced workflow page when clicked', () => {
|
||||
component.workflowTaskPageRoute = `/workflowitems/${workflowId}/advanced`;
|
||||
fixture.debugElement.query(By.css('.selectReviewerAction')).nativeElement.click();
|
||||
|
||||
expect(router.navigate).toHaveBeenCalledWith([`/workflowitems/${workflowId}/advanced`], {
|
||||
queryParams: {
|
||||
workflow: ADVANCED_WORKFLOW_ACTION_SELECT_REVIEWER,
|
||||
claimedTask: taskId,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,46 @@
|
||||
import { Component, Injector } from '@angular/core';
|
||||
import { rendersWorkflowTaskOption } from '../switcher/claimed-task-actions-decorator';
|
||||
import {
|
||||
AdvancedClaimedTaskActionsAbstractComponent
|
||||
} from '../abstract/advanced-claimed-task-actions-abstract.component';
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
import { NotificationsService } from '../../../notifications/notifications.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { SearchService } from '../../../../core/shared/search/search.service';
|
||||
import { RequestService } from '../../../../core/data/request.service';
|
||||
import {
|
||||
ADVANCED_WORKFLOW_ACTION_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';
|
||||
|
||||
/**
|
||||
* 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',
|
||||
styleUrls: ['./advanced-claimed-task-action-select-reviewer.component.scss']
|
||||
})
|
||||
export class AdvancedClaimedTaskActionSelectReviewerComponent extends AdvancedClaimedTaskActionsAbstractComponent {
|
||||
|
||||
/**
|
||||
* This component represents the advanced select option
|
||||
*/
|
||||
option = ADVANCED_WORKFLOW_TASK_OPTION_SELECT_REVIEWER;
|
||||
|
||||
workflowType = ADVANCED_WORKFLOW_ACTION_SELECT_REVIEWER;
|
||||
|
||||
constructor(
|
||||
protected injector: Injector,
|
||||
protected router: Router,
|
||||
protected notificationsService: NotificationsService,
|
||||
protected translate: TranslateService,
|
||||
protected searchService: SearchService,
|
||||
protected requestService: RequestService,
|
||||
protected route: ActivatedRoute,
|
||||
) {
|
||||
super(injector, router, notificationsService, translate, searchService, requestService, route);
|
||||
}
|
||||
|
||||
}
|
@@ -1,23 +1,44 @@
|
||||
import { hasNoValue } from '../../../empty.util';
|
||||
|
||||
const map = new Map();
|
||||
const workflowOptions = new Map();
|
||||
const advancedWorkflowOptions = new Map();
|
||||
|
||||
/**
|
||||
* Decorator used for rendering ClaimedTaskActions pages by option type
|
||||
*/
|
||||
export function rendersWorkflowTaskOption(option: string) {
|
||||
return function decorator(component: any) {
|
||||
if (hasNoValue(map.get(option))) {
|
||||
map.set(option, component);
|
||||
if (hasNoValue(workflowOptions.get(option))) {
|
||||
workflowOptions.set(option, component);
|
||||
} else {
|
||||
throw new Error(`There can't be more than one component to render ClaimedTaskActions for option "${option}"`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Decorator used for rendering AdvancedClaimedTaskActions pages by option type
|
||||
*/
|
||||
export function rendersAdvancedWorkflowTaskOption(option: string) {
|
||||
return function decorator(component: any) {
|
||||
if (hasNoValue(advancedWorkflowOptions.get(option))) {
|
||||
advancedWorkflowOptions.set(option, component);
|
||||
} else {
|
||||
throw new Error(`There can't be more than one component to render AdvancedClaimedTaskActions for option "${option}"`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the component used for rendering a ClaimedTaskActions page by option type
|
||||
*/
|
||||
export function getComponentByWorkflowTaskOption(option: string) {
|
||||
return map.get(option);
|
||||
return workflowOptions.get(option);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the component used for rendering a AdvancedClaimedTaskActions page by option type
|
||||
*/
|
||||
export function getAdvancedComponentByWorkflowTaskOption(option: string) {
|
||||
return advancedWorkflowOptions.get(option);
|
||||
}
|
||||
|
@@ -2,7 +2,7 @@ import { ClaimedTask } from '../../../core/tasks/models/claimed-task-object.mode
|
||||
import { SearchResult } from '../../search/models/search-result.model';
|
||||
|
||||
/**
|
||||
* Represents a search result object of a Declined ClaimedTask object
|
||||
* Represents a search result object of a Declined/Rejected ClaimedTask object (sent back to the submitter)
|
||||
*/
|
||||
export class ClaimedDeclinedTaskSearchResult extends SearchResult<ClaimedTask> {
|
||||
}
|
||||
|
@@ -0,0 +1,8 @@
|
||||
import { ClaimedTask } from '../../../core/tasks/models/claimed-task-object.model';
|
||||
import { SearchResult } from '../../search/models/search-result.model';
|
||||
|
||||
/**
|
||||
* Represents a search result object of a Declined ClaimedTask object (sent back to the Review Managers)
|
||||
*/
|
||||
export class ClaimedDeclinedTaskTaskSearchResult extends SearchResult<ClaimedTask> {
|
||||
}
|
@@ -6,4 +6,5 @@ export enum MyDspaceItemStatusType {
|
||||
ARCHIVED = 'mydspace.status.archived',
|
||||
DECLINED = 'mydspace.status.declined',
|
||||
APPROVED = 'mydspace.status.approved',
|
||||
DECLINED_TASk = 'mydspace.status.declined-task',
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<ng-container *ngVar="(workflowitemRD$ | async)?.payload as workflowitem">
|
||||
<div class="alert alert-success w-100" role="alert">
|
||||
<h4 class="alert-heading">Approved</h4>
|
||||
<h4 class="alert-heading mb-0">{{ 'claimed-approved-search-result-list-element.title' | translate }}</h4>
|
||||
<ds-themed-item-list-preview *ngIf="workflowitem"
|
||||
[item]="(workflowitem?.item | async)?.payload"
|
||||
[object]="object"
|
||||
|
@@ -19,6 +19,7 @@ import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service
|
||||
import { DSONameServiceMock } from '../../../../mocks/dso-name.service.mock';
|
||||
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
|
||||
import { environment } from '../../../../../../environments/environment';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
let component: ClaimedApprovedSearchResultListElementComponent;
|
||||
let fixture: ComponentFixture<ClaimedApprovedSearchResultListElementComponent>;
|
||||
@@ -64,7 +65,10 @@ const linkService = getMockLinkService();
|
||||
describe('ClaimedApprovedSearchResultListElementComponent', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [NoopAnimationsModule],
|
||||
imports: [
|
||||
TranslateModule.forRoot(),
|
||||
NoopAnimationsModule,
|
||||
],
|
||||
declarations: [ClaimedApprovedSearchResultListElementComponent, VarDirective],
|
||||
providers: [
|
||||
{ provide: TruncatableService, useValue: {} },
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<ng-container *ngVar="(workflowitemRD$ | async)?.payload as workflowitem">
|
||||
<div class="alert alert-secondary w-100" role="alert">
|
||||
<h4 class="alert-heading">Declined</h4>
|
||||
<h4 class="alert-heading mb-0">{{ 'claimed-declined-search-result-list-element.title' | translate }}</h4>
|
||||
<ds-themed-item-list-preview *ngIf="workflowitem"
|
||||
[item]="(workflowitem?.item | async)?.payload"
|
||||
[object]="object"
|
||||
|
@@ -19,6 +19,7 @@ import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service
|
||||
import { DSONameServiceMock } from '../../../../mocks/dso-name.service.mock';
|
||||
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
|
||||
import { environment } from '../../../../../../environments/environment';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
let component: ClaimedDeclinedSearchResultListElementComponent;
|
||||
let fixture: ComponentFixture<ClaimedDeclinedSearchResultListElementComponent>;
|
||||
@@ -64,7 +65,10 @@ const linkService = getMockLinkService();
|
||||
describe('ClaimedDeclinedSearchResultListElementComponent', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [NoopAnimationsModule],
|
||||
imports: [
|
||||
TranslateModule.forRoot(),
|
||||
NoopAnimationsModule,
|
||||
],
|
||||
declarations: [ClaimedDeclinedSearchResultListElementComponent, VarDirective],
|
||||
providers: [
|
||||
{ provide: TruncatableService, useValue: {} },
|
@@ -0,0 +1,11 @@
|
||||
<ng-container *ngVar="(workflowitemRD$ | async)?.payload as workflowitem">
|
||||
<div class="alert alert-warning w-100" role="alert">
|
||||
<h4 class="alert-heading mb-0">{{ 'claimed-declined-task-search-result-list-element.title' | translate }}</h4>
|
||||
<ds-themed-item-list-preview *ngIf="workflowitem"
|
||||
[item]="(workflowitem?.item | async)?.payload"
|
||||
[object]="object"
|
||||
[status]="status"
|
||||
[showSubmitter]="showSubmitter">
|
||||
</ds-themed-item-list-preview>
|
||||
</div>
|
||||
</ng-container>
|
@@ -0,0 +1,109 @@
|
||||
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { ClaimedDeclinedTaskSearchResultListElementComponent } from './claimed-declined-task-search-result-list-element.component';
|
||||
import { ClaimedDeclinedTaskTaskSearchResult } from '../../../../object-collection/shared/claimed-declined-task-task-search-result.model';
|
||||
import { Item } from '../../../../../core/shared/item.model';
|
||||
import { createSuccessfulRemoteDataObject } from '../../../../remote-data.utils';
|
||||
import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model';
|
||||
import { ClaimedTask } from '../../../../../core/tasks/models/claimed-task-object.model';
|
||||
import { getMockLinkService } from '../../../../mocks/link-service.mock';
|
||||
import { VarDirective } from '../../../../utils/var.directive';
|
||||
import { TruncatableService } from '../../../../truncatable/truncatable.service';
|
||||
import { LinkService } from '../../../../../core/cache/builders/link.service';
|
||||
import { MyDspaceItemStatusType } from '../../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type';
|
||||
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
||||
import { DSONameServiceMock } from '../../../../mocks/dso-name.service.mock';
|
||||
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
|
||||
import { environment } from '../../../../../../environments/environment';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
let component: ClaimedDeclinedTaskSearchResultListElementComponent;
|
||||
let fixture: ComponentFixture<ClaimedDeclinedTaskSearchResultListElementComponent>;
|
||||
|
||||
const mockResultObject: ClaimedDeclinedTaskTaskSearchResult = new ClaimedDeclinedTaskTaskSearchResult();
|
||||
mockResultObject.hitHighlights = {};
|
||||
|
||||
const item = Object.assign(new Item(), {
|
||||
bundles: observableOf({}),
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
{
|
||||
language: 'en_US',
|
||||
value: 'This is just another title'
|
||||
}
|
||||
],
|
||||
'dc.type': [
|
||||
{
|
||||
language: null,
|
||||
value: 'Article'
|
||||
}
|
||||
],
|
||||
'dc.contributor.author': [
|
||||
{
|
||||
language: 'en_US',
|
||||
value: 'Smith, Donald'
|
||||
}
|
||||
],
|
||||
'dc.date.issued': [
|
||||
{
|
||||
language: null,
|
||||
value: '2015-06-26'
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
const rdItem = createSuccessfulRemoteDataObject(item);
|
||||
const workflowitem = Object.assign(new WorkflowItem(), { item: observableOf(rdItem) });
|
||||
const rdWorkflowitem = createSuccessfulRemoteDataObject(workflowitem);
|
||||
mockResultObject.indexableObject = Object.assign(new ClaimedTask(), { workflowitem: observableOf(rdWorkflowitem) });
|
||||
const linkService = getMockLinkService();
|
||||
|
||||
describe('ClaimedDeclinedTaskSearchResultListElementComponent', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
NoopAnimationsModule,
|
||||
TranslateModule.forRoot(),
|
||||
],
|
||||
declarations: [ClaimedDeclinedTaskSearchResultListElementComponent, VarDirective],
|
||||
providers: [
|
||||
{ provide: TruncatableService, useValue: {} },
|
||||
{ provide: LinkService, useValue: linkService },
|
||||
{ provide: DSONameService, useClass: DSONameServiceMock },
|
||||
{ provide: APP_CONFIG, useValue: environment },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).overrideComponent(ClaimedDeclinedTaskSearchResultListElementComponent, {
|
||||
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
fixture = TestBed.createComponent(ClaimedDeclinedTaskSearchResultListElementComponent);
|
||||
component = fixture.componentInstance;
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
component.dso = mockResultObject.indexableObject;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should init workflowitem properly', (done) => {
|
||||
component.workflowitemRD$.subscribe((workflowitemRD) => {
|
||||
expect(linkService.resolveLinks).toHaveBeenCalledWith(
|
||||
component.dso,
|
||||
jasmine.objectContaining({ name: 'workflowitem' }),
|
||||
jasmine.objectContaining({ name: 'action' })
|
||||
);
|
||||
expect(workflowitemRD.payload).toEqual(workflowitem);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should have properly status', () => {
|
||||
expect(component.status).toEqual(MyDspaceItemStatusType.DECLINED_TASk);
|
||||
});
|
||||
|
||||
});
|
@@ -0,0 +1,68 @@
|
||||
import { Component, Inject, OnInit } from '@angular/core';
|
||||
import { listableObjectComponent } from '../../../../object-collection/shared/listable-object/listable-object.decorator';
|
||||
import { ClaimedDeclinedTaskTaskSearchResult } from 'src/app/shared/object-collection/shared/claimed-declined-task-task-search-result.model';
|
||||
import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
||||
import { LinkService } from '../../../../../core/cache/builders/link.service';
|
||||
import { TruncatableService } from '../../../../truncatable/truncatable.service';
|
||||
import { MyDspaceItemStatusType } from '../../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type';
|
||||
import { Observable } from 'rxjs';
|
||||
import { RemoteData } from '../../../../../core/data/remote-data';
|
||||
import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model';
|
||||
import { followLink } from '../../../../utils/follow-link-config.model';
|
||||
import { SearchResultListElementComponent } from '../../../search-result-list-element/search-result-list-element.component';
|
||||
import { ClaimedTaskSearchResult } from '../../../../object-collection/shared/claimed-task-search-result.model';
|
||||
import { ClaimedTask } from '../../../../../core/tasks/models/claimed-task-object.model';
|
||||
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
||||
import { APP_CONFIG, AppConfig } from '../../../../../../config/app-config.interface';
|
||||
|
||||
/**
|
||||
* This component renders claimed task declined task object for the search result in the list view.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-claimed-declined-task-search-result-list-element',
|
||||
styleUrls: ['../../../search-result-list-element/search-result-list-element.component.scss'],
|
||||
templateUrl: './claimed-declined-task-search-result-list-element.component.html'
|
||||
})
|
||||
@listableObjectComponent(ClaimedDeclinedTaskTaskSearchResult, ViewMode.ListElement)
|
||||
export class ClaimedDeclinedTaskSearchResultListElementComponent extends SearchResultListElementComponent<ClaimedTaskSearchResult, ClaimedTask> implements OnInit {
|
||||
|
||||
/**
|
||||
* A boolean representing if to show submitter information
|
||||
*/
|
||||
public showSubmitter = true;
|
||||
|
||||
/**
|
||||
* Represent item's status
|
||||
*/
|
||||
public status = MyDspaceItemStatusType.DECLINED_TASk;
|
||||
|
||||
/**
|
||||
* The workflowitem object that belonging to the result object
|
||||
*/
|
||||
public workflowitemRD$: Observable<RemoteData<WorkflowItem>>;
|
||||
|
||||
public constructor(
|
||||
protected linkService: LinkService,
|
||||
protected truncatableService: TruncatableService,
|
||||
protected dsoNameService: DSONameService,
|
||||
@Inject(APP_CONFIG) protected appConfig: AppConfig,
|
||||
) {
|
||||
super(truncatableService, dsoNameService, appConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize all instance variables
|
||||
*/
|
||||
ngOnInit() {
|
||||
super.ngOnInit();
|
||||
this.linkService.resolveLinks(this.dso,
|
||||
followLink('workflowitem',
|
||||
{ useCachedVersionIfAvailable: false },
|
||||
followLink('item'),
|
||||
followLink('submitter')
|
||||
),
|
||||
followLink('action'));
|
||||
this.workflowitemRD$ = this.dso.workflowitem as Observable<RemoteData<WorkflowItem>>;
|
||||
}
|
||||
|
||||
}
|
@@ -245,10 +245,13 @@ import {
|
||||
} from './object-list/listable-notification-object/listable-notification-object.component';
|
||||
import { ThemedCollectionDropdownComponent } from './collection-dropdown/themed-collection-dropdown.component';
|
||||
import { MetadataFieldWrapperComponent } from './metadata-field-wrapper/metadata-field-wrapper.component';
|
||||
import {
|
||||
LogInExternalProviderComponent
|
||||
} from './log-in/methods/log-in-external-provider/log-in-external-provider.component';
|
||||
import { ShortNumberPipe } from './utils/short-number.pipe';
|
||||
import { LogInExternalProviderComponent } from './log-in/methods/log-in-external-provider/log-in-external-provider.component';
|
||||
import { AdvancedClaimedTaskActionSelectReviewerComponent } from './mydspace-actions/claimed-task/select-reviewer/advanced-claimed-task-action-select-reviewer.component';
|
||||
import {
|
||||
AdvancedClaimedTaskActionRatingComponent
|
||||
} from './mydspace-actions/claimed-task/rating/advanced-claimed-task-action-rating.component';
|
||||
import { ClaimedTaskActionsDeclineTaskComponent } from './mydspace-actions/claimed-task/decline-task/claimed-task-actions-decline-task.component';
|
||||
import {
|
||||
SupervisionOrderStatusComponent
|
||||
} from './object-list/supervision-order-status/supervision-order-status.component';
|
||||
@@ -405,6 +408,7 @@ const ENTRY_COMPONENTS = [
|
||||
ItemMetadataRepresentationListElementComponent,
|
||||
LogInPasswordComponent,
|
||||
LogInExternalProviderComponent,
|
||||
ClaimedTaskActionsDeclineTaskComponent,
|
||||
CollectionDropdownComponent,
|
||||
ThemedCollectionDropdownComponent,
|
||||
FileDownloadLinkComponent,
|
||||
@@ -419,6 +423,8 @@ const ENTRY_COMPONENTS = [
|
||||
CommunitySidebarSearchListElementComponent,
|
||||
ScopeSelectorModalComponent,
|
||||
ListableNotificationObjectComponent,
|
||||
AdvancedClaimedTaskActionSelectReviewerComponent,
|
||||
AdvancedClaimedTaskActionRatingComponent,
|
||||
EpersonGroupListComponent,
|
||||
EpersonSearchBoxComponent,
|
||||
GroupSearchBoxComponent,
|
||||
@@ -444,7 +450,6 @@ const DIRECTIVES = [
|
||||
ClaimedTaskActionsDirective,
|
||||
FileValueAccessorDirective,
|
||||
FileValidator,
|
||||
ClaimedTaskActionsDirective,
|
||||
NgForTrackByIdDirective,
|
||||
MetadataFieldValidator,
|
||||
HoverClassDirective,
|
||||
|
@@ -55,7 +55,8 @@ export class ActivatedRouteStub {
|
||||
return {
|
||||
params: this.testParams,
|
||||
paramMap: convertToParamMap(this.params),
|
||||
queryParamMap: convertToParamMap(this.testParams)
|
||||
queryParamMap: convertToParamMap(this.testParams),
|
||||
queryParams: {} as Params,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
13
src/app/shared/testing/base-data-service.stub.ts
Normal file
13
src/app/shared/testing/base-data-service.stub.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
import { CacheableObject } from '../../core/cache/cacheable-object.model';
|
||||
|
||||
/**
|
||||
* Stub class for {@link BaseDataService}
|
||||
*/
|
||||
export abstract class BaseDataServiceStub<T extends CacheableObject> {
|
||||
|
||||
invalidateByHref(_href: string): Observable<boolean> {
|
||||
return observableOf(true);
|
||||
}
|
||||
|
||||
}
|
16
src/app/shared/testing/claimed-task-data-service.stub.ts
Normal file
16
src/app/shared/testing/claimed-task-data-service.stub.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Observable, EMPTY } from 'rxjs';
|
||||
import { ProcessTaskResponse } from '../../core/tasks/models/process-task-response';
|
||||
import { ClaimedTask } from '../../core/tasks/models/claimed-task-object.model';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
|
||||
export class ClaimedTaskDataServiceStub {
|
||||
|
||||
public submitTask(_scopeId: string, _body: any): Observable<ProcessTaskResponse> {
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
public findByItem(_uuid: string): Observable<RemoteData<ClaimedTask>> {
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
}
|
@@ -1,9 +0,0 @@
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
|
||||
export class DataServiceStub {
|
||||
|
||||
invalidateByHref(_href: string): Observable<boolean> {
|
||||
return observableOf(true);
|
||||
}
|
||||
|
||||
}
|
16
src/app/shared/testing/identifiable-data-service.stub.ts
Normal file
16
src/app/shared/testing/identifiable-data-service.stub.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { CacheableObject } from '../../core/cache/cacheable-object.model';
|
||||
import { BaseDataServiceStub } from './base-data-service.stub';
|
||||
import { FollowLinkConfig } from '../utils/follow-link-config.model';
|
||||
import { Observable, EMPTY } from 'rxjs';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
|
||||
/**
|
||||
* Stub class for {@link IdentifiableDataService}
|
||||
*/
|
||||
export class IdentifiableDataServiceStub<T extends CacheableObject> extends BaseDataServiceStub<T> {
|
||||
|
||||
findById(_id: string, _useCachedVersionIfAvailable = true, _reRequestOnStale = true, ..._linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<T>> {
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
}
|
7
src/app/shared/testing/location.stub.ts
Normal file
7
src/app/shared/testing/location.stub.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export class LocationStub {
|
||||
|
||||
getState(): unknown {
|
||||
return {};
|
||||
}
|
||||
|
||||
}
|
12
src/app/shared/testing/request-service.stub.ts
Normal file
12
src/app/shared/testing/request-service.stub.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
|
||||
/**
|
||||
* Stub service for {@link RequestService}.
|
||||
*/
|
||||
export class RequestServiceStub {
|
||||
|
||||
removeByHrefSubstring(_href: string): Observable<boolean> {
|
||||
return observableOf(true);
|
||||
}
|
||||
|
||||
}
|
18
src/app/shared/testing/workflow-action-data-service.stub.ts
Normal file
18
src/app/shared/testing/workflow-action-data-service.stub.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { FindListOptions } from '../../core/data/find-list-options.model';
|
||||
import { FollowLinkConfig } from '../utils/follow-link-config.model';
|
||||
import { WorkspaceItem } from '../../core/submission/models/workspaceitem.model';
|
||||
import { Observable, EMPTY } from 'rxjs';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { WorkflowItem } from '../../core/submission/models/workflowitem.model';
|
||||
import { IdentifiableDataServiceStub } from './identifiable-data-service.stub';
|
||||
|
||||
/**
|
||||
* Stub class for {@link WorkflowActionDataService}
|
||||
*/
|
||||
export class WorkflowActionDataServiceStub extends IdentifiableDataServiceStub<WorkflowItem> {
|
||||
|
||||
public findByItem(_uuid: string, _useCachedVersionIfAvailable = false, _reRequestOnStale = true, _options: FindListOptions = {}, ..._linksToFollow: FollowLinkConfig<WorkspaceItem>[]): Observable<RemoteData<WorkspaceItem>> {
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,8 @@
|
||||
import { WorkflowItem } from '../../core/submission/models/workflowitem.model';
|
||||
import { IdentifiableDataServiceStub } from './identifiable-data-service.stub';
|
||||
|
||||
/**
|
||||
* Stub class for {@link WorkflowItemDataService}
|
||||
*/
|
||||
export class WorkflowItemDataServiceStub extends IdentifiableDataServiceStub<WorkflowItem> {
|
||||
}
|
@@ -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>
|
@@ -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();
|
||||
});
|
||||
});
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
@@ -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>
|
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
@@ -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>
|
@@ -0,0 +1,7 @@
|
||||
:host ::ng-deep {
|
||||
.reviewersListWithGroup {
|
||||
#search, #search + form, #search + form + ds-pagination {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
@@ -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 });
|
||||
}
|
||||
|
||||
}
|
@@ -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([]);
|
||||
});
|
||||
|
||||
});
|
@@ -0,0 +1,149 @@
|
||||
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';
|
||||
|
||||
/**
|
||||
* 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,
|
||||
translateService: TranslateService,
|
||||
notificationsService: NotificationsService,
|
||||
formBuilder: FormBuilder,
|
||||
paginationService: PaginationService,
|
||||
router: Router) {
|
||||
super(groupService, ePersonDataService, translateService, notificationsService, formBuilder, paginationService, router);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.searchForm = this.formBuilder.group(({
|
||||
scope: 'metadata',
|
||||
query: '',
|
||||
}));
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
this.groupId = changes.groupId.currentValue;
|
||||
if (changes.groupId.currentValue !== changes.groupId.previousValue) {
|
||||
if (this.groupId === null) {
|
||||
this.retrieveMembers(this.config.currentPage);
|
||||
} else {
|
||||
this.subs.set(SubKey.ActiveGroup, this.groupService.findById(this.groupId).pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
).subscribe((activeGroup: Group) => {
|
||||
if (activeGroup != null) {
|
||||
this.groupDataService.editGroup(activeGroup);
|
||||
this.groupBeingEdited = activeGroup;
|
||||
this.retrieveMembers(this.config.currentPage);
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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));
|
||||
}
|
||||
|
||||
}
|
@@ -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';
|
||||
}
|
||||
|
||||
}
|
@@ -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;
|
||||
|
||||
}
|
@@ -0,0 +1 @@
|
||||
<ng-template dsAdvancedWorkflowActions></ng-template>
|
@@ -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 {
|
||||
}
|
@@ -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);
|
||||
}
|
||||
|
||||
}
|
@@ -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,
|
||||
) {
|
||||
}
|
||||
|
||||
}
|
@@ -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 {
|
||||
|
@@ -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 });
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@@ -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() },
|
||||
|
@@ -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)
|
||||
|
@@ -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() },
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -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';
|
||||
|
@@ -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' }
|
||||
},
|
||||
]
|
||||
}]
|
||||
)
|
||||
|
@@ -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,
|
||||
]
|
||||
})
|
||||
/**
|
||||
|
@@ -590,6 +590,70 @@
|
||||
|
||||
"admin.metadata-import.page.validateOnly.hint": "When selected, the uploaded CSV will be validated. You will receive a report of detected changes, but no changes will be saved.",
|
||||
|
||||
"advanced-workflow-action.rating.form.rating.label": "Rating",
|
||||
|
||||
"advanced-workflow-action.rating.form.rating.error": "You must rate the item",
|
||||
|
||||
"advanced-workflow-action.rating.form.review.label": "Review",
|
||||
|
||||
"advanced-workflow-action.rating.form.review.error": "You must enter a review to submit this rating",
|
||||
|
||||
"advanced-workflow-action.rating.description": "Please select a rating below",
|
||||
|
||||
"advanced-workflow-action.rating.description-requiredDescription": "Please select a rating below and also add a review",
|
||||
|
||||
|
||||
"advanced-workflow-action.select-reviewer.description-single": "Please select a single reviewer below before submitting",
|
||||
|
||||
"advanced-workflow-action.select-reviewer.description-multiple": "Please select one or more reviewers below before submitting",
|
||||
|
||||
|
||||
"advanced-workflow-action-select-reviewer.groups.form.reviewers-list.head": "EPeople",
|
||||
|
||||
"advanced-workflow-action-select-reviewer.groups.form.reviewers-list.search.head": "Add EPeople",
|
||||
|
||||
"advanced-workflow-action-select-reviewer.groups.form.reviewers-list.button.see-all": "Browse All",
|
||||
|
||||
"advanced-workflow-action-select-reviewer.groups.form.reviewers-list.headMembers": "Current Members",
|
||||
|
||||
"advanced-workflow-action-select-reviewer.groups.form.reviewers-list.search.scope.metadata": "Metadata",
|
||||
|
||||
"advanced-workflow-action-select-reviewer.groups.form.reviewers-list.search.scope.email": "E-mail (exact)",
|
||||
|
||||
"advanced-workflow-action-select-reviewer.groups.form.reviewers-list.search.button": "Search",
|
||||
|
||||
"advanced-workflow-action-select-reviewer.groups.form.reviewers-list.table.id": "ID",
|
||||
|
||||
"advanced-workflow-action-select-reviewer.groups.form.reviewers-list.table.name": "Name",
|
||||
|
||||
"advanced-workflow-action-select-reviewer.groups.form.reviewers-list.table.identity": "Identity",
|
||||
|
||||
"advanced-workflow-action-select-reviewer.groups.form.reviewers-list.table.email": "Email",
|
||||
|
||||
"advanced-workflow-action-select-reviewer.groups.form.reviewers-list.table.netid": "NetID",
|
||||
|
||||
"advanced-workflow-action-select-reviewer.groups.form.reviewers-list.table.edit": "Remove / Add",
|
||||
|
||||
"advanced-workflow-action-select-reviewer.groups.form.reviewers-list.table.edit.buttons.remove": "Remove member with name \"{{name}}\"",
|
||||
|
||||
"advanced-workflow-action-select-reviewer.groups.form.reviewers-list.notification.success.addMember": "Successfully added member: \"{{name}}\"",
|
||||
|
||||
"advanced-workflow-action-select-reviewer.groups.form.reviewers-list.notification.failure.addMember": "Failed to add member: \"{{name}}\"",
|
||||
|
||||
"advanced-workflow-action-select-reviewer.groups.form.reviewers-list.notification.success.deleteMember": "Successfully deleted member: \"{{name}}\"",
|
||||
|
||||
"advanced-workflow-action-select-reviewer.groups.form.reviewers-list.notification.failure.deleteMember": "Failed to delete member: \"{{name}}\"",
|
||||
|
||||
"advanced-workflow-action-select-reviewer.groups.form.reviewers-list.table.edit.buttons.add": "Add member with name \"{{name}}\"",
|
||||
|
||||
"advanced-workflow-action-select-reviewer.groups.form.reviewers-list.notification.failure.noActiveGroup": "No current active group, submit a name first.",
|
||||
|
||||
"advanced-workflow-action-select-reviewer.groups.form.reviewers-list.no-members-yet": "No members in group yet, search and add.",
|
||||
|
||||
"advanced-workflow-action-select-reviewer.groups.form.reviewers-list.no-items": "No EPeople found in that search",
|
||||
|
||||
"advanced-workflow-action.select-reviewer.no-reviewer-selected.error": "No reviewer selected.",
|
||||
|
||||
"admin.batch-import.page.validateOnly.hint": "When selected, the uploaded ZIP will be validated. You will receive a report of detected changes, but no changes will be saved.",
|
||||
|
||||
"admin.batch-import.page.remove": "remove",
|
||||
@@ -796,6 +860,12 @@
|
||||
"chips.remove": "Remove chip",
|
||||
|
||||
|
||||
"claimed-approved-search-result-list-element.title": "Approved",
|
||||
|
||||
"claimed-declined-search-result-list-element.title": "Rejected, sent back to submitter",
|
||||
|
||||
"claimed-declined-task-search-result-list-element.title": "Declined, sent back to Review Manager's workflow",
|
||||
|
||||
|
||||
"collection.create.head": "Create a Collection",
|
||||
|
||||
@@ -1206,6 +1276,11 @@
|
||||
"comcol-role.edit.reviewer.description": "Reviewers are able to accept or reject incoming submissions. However, they are not able to edit the submission's metadata.",
|
||||
|
||||
|
||||
"comcol-role.edit.scorereviewers.name": "Score Reviewers",
|
||||
|
||||
"comcol-role.edit.scorereviewers.description": "Reviewers are able to give a score to incoming submissions, this will define whether the submission will be rejected or not.",
|
||||
|
||||
|
||||
|
||||
"community.form.abstract": "Short Description",
|
||||
|
||||
@@ -4619,6 +4694,15 @@
|
||||
"submission.workflow.generic.view-help": "Select this option to view the item's metadata.",
|
||||
|
||||
|
||||
"submission.workflow.generic.submit_select_reviewer": "Select Reviewer",
|
||||
|
||||
"submission.workflow.generic.submit_select_reviewer-help": "",
|
||||
|
||||
|
||||
"submission.workflow.generic.submit_score": "Rate",
|
||||
|
||||
"submission.workflow.generic.submit_score-help": "",
|
||||
|
||||
|
||||
"submission.workflow.tasks.claimed.approve": "Approve",
|
||||
|
||||
@@ -4628,6 +4712,10 @@
|
||||
|
||||
"submission.workflow.tasks.claimed.edit_help": "Select this option to change the item's metadata.",
|
||||
|
||||
"submission.workflow.tasks.claimed.decline": "Decline",
|
||||
|
||||
"submission.workflow.tasks.claimed.decline_help": "",
|
||||
|
||||
"submission.workflow.tasks.claimed.reject.reason.info": "Please enter your reason for rejecting the submission into the box below, indicating whether the submitter may fix a problem and resubmit.",
|
||||
|
||||
"submission.workflow.tasks.claimed.reject.reason.placeholder": "Describe the reason of reject",
|
||||
@@ -4783,6 +4871,43 @@
|
||||
|
||||
"workspace-item.view.title": "Workspace View",
|
||||
|
||||
|
||||
"workflow-item.advanced.title": "Advanced workflow",
|
||||
|
||||
|
||||
"workflow-item.selectrevieweraction.notification.success.title": "Selected reviewer",
|
||||
|
||||
"workflow-item.selectrevieweraction.notification.success.content": "The reviewer for this workflow item has been successfully selected",
|
||||
|
||||
"workflow-item.selectrevieweraction.notification.error.title": "Something went wrong",
|
||||
|
||||
"workflow-item.selectrevieweraction.notification.error.content": "Couldn't select the reviewer for this workflow item",
|
||||
|
||||
"workflow-item.selectrevieweraction.title": "Select Reviewer",
|
||||
|
||||
"workflow-item.selectrevieweraction.header": "Select Reviewer",
|
||||
|
||||
"workflow-item.selectrevieweraction.button.cancel": "Cancel",
|
||||
|
||||
"workflow-item.selectrevieweraction.button.confirm": "Confirm",
|
||||
|
||||
|
||||
"workflow-item.scorereviewaction.notification.success.title": "Rating review",
|
||||
|
||||
"workflow-item.scorereviewaction.notification.success.content": "The rating for this item workflow item has been successfully submitted",
|
||||
|
||||
"workflow-item.scorereviewaction.notification.error.title": "Something went wrong",
|
||||
|
||||
"workflow-item.scorereviewaction.notification.error.content": "Couldn't rate this item",
|
||||
|
||||
"workflow-item.scorereviewaction.title": "Rate this item",
|
||||
|
||||
"workflow-item.scorereviewaction.header": "Rate this item",
|
||||
|
||||
"workflow-item.scorereviewaction.button.cancel": "Cancel",
|
||||
|
||||
"workflow-item.scorereviewaction.button.confirm": "Confirm",
|
||||
|
||||
"idle-modal.header": "Session will expire soon",
|
||||
|
||||
"idle-modal.info": "For security reasons, user sessions expire after {{ timeToExpire }} minutes of inactivity. Your session will expire soon. Would you like to extend it or log out?",
|
||||
|
Reference in New Issue
Block a user