68954: Display claimed task actions depending on config from REST API

Conflicts:
	src/app/core/core.module.ts
	src/app/shared/mydspace-actions/claimed-task/claimed-task-actions.component.html
This commit is contained in:
Kristof De Langhe
2020-02-24 17:13:12 +01:00
parent 01231ef105
commit 305c4ce882
22 changed files with 452 additions and 142 deletions

View File

@@ -142,6 +142,8 @@ import { PoolTask } from './tasks/models/pool-task-object.model';
import { TaskObject } from './tasks/models/task-object.model'; import { TaskObject } from './tasks/models/task-object.model';
import { PoolTaskDataService } from './tasks/pool-task-data.service'; import { PoolTaskDataService } from './tasks/pool-task-data.service';
import { TaskResponseParsingService } from './tasks/task-response-parsing.service'; import { TaskResponseParsingService } from './tasks/task-response-parsing.service';
import { WorkflowActionDataService } from './data/workflow-action-data.service';
import { NormalizedWorkflowAction } from './tasks/models/normalized-workflow-action-object.model';
/** /**
* When not in production, endpoint responses can be mocked for testing purposes * When not in production, endpoint responses can be mocked for testing purposes
@@ -257,6 +259,7 @@ const PROVIDERS = [
LookupRelationService, LookupRelationService,
LicenseDataService, LicenseDataService,
ItemTypeDataService, ItemTypeDataService,
WorkflowActionDataService,
// register AuthInterceptor as HttpInterceptor // register AuthInterceptor as HttpInterceptor
{ {
provide: HTTP_INTERCEPTORS, provide: HTTP_INTERCEPTORS,
@@ -305,6 +308,7 @@ export const models =
ItemType, ItemType,
ExternalSource, ExternalSource,
ExternalSourceEntry, ExternalSourceEntry,
NormalizedWorkflowAction
]; ];
@NgModule({ @NgModule({

View File

@@ -0,0 +1,40 @@
import { DataService } from './data.service';
import { WorkflowAction } from '../tasks/models/workflow-action-object.model';
import { RequestService } from './request.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
import { Store } from '@ngrx/store';
import { CoreState } from '../core.reducers';
import { ObjectCacheService } from '../cache/object-cache.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { HttpClient } from '@angular/common/http';
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
import { FindListOptions } from './request.models';
import { Observable } from 'rxjs/internal/Observable';
import { Injectable } from '@angular/core';
/**
* A service responsible for fetching/sending data from/to the REST API on the workflowactions endpoint
*/
@Injectable()
export class WorkflowActionDataService extends DataService<WorkflowAction> {
protected linkPath = 'workflowactions';
constructor(
protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
protected dataBuildService: NormalizedObjectBuildService,
protected store: Store<CoreState>,
protected objectCache: ObjectCacheService,
protected halService: HALEndpointService,
protected notificationsService: NotificationsService,
protected http: HttpClient,
protected comparator: DefaultChangeAnalyzer<WorkflowAction>) {
super();
}
getBrowseEndpoint(options: FindListOptions, linkPath?: string): Observable<string> {
return this.halService.getEndpoint(this.linkPath);
}
}

View File

@@ -0,0 +1,23 @@
import { autoserialize, inheritSerialization } from 'cerialize';
import { mapsTo } from '../../cache/builders/build-decorators';
import { WorkflowAction } from './workflow-action-object.model';
import { NormalizedDSpaceObject } from '../../cache/models/normalized-dspace-object.model';
/**
* A normalized model class for a WorkflowAction
*/
@mapsTo(WorkflowAction)
@inheritSerialization(NormalizedDSpaceObject)
export class NormalizedWorkflowAction extends NormalizedDSpaceObject<WorkflowAction> {
/**
* The workflow action's identifier
*/
@autoserialize
id: string;
/**
* The options available for this workflow action
*/
@autoserialize
options: string[];
}

View File

@@ -0,0 +1,19 @@
import { ResourceType } from '../../shared/resource-type';
import { DSpaceObject } from '../../shared/dspace-object.model';
/**
* A model class for a WorkflowAction
*/
export class WorkflowAction extends DSpaceObject {
static type = new ResourceType('workflowaction');
/**
* The workflow action's identifier
*/
id: string;
/**
* The options available for this workflow action
*/
options: string[];
}

View File

@@ -0,0 +1,28 @@
import { EventEmitter, Input, Output } from '@angular/core';
import { ClaimedTask } from '../../../../core/tasks/models/claimed-task-object.model';
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
/**
* Abstract component for rendering a claimed task's action
*/
export abstract class ClaimedTaskActionsAbstractComponent {
/**
* The Claimed Task to display an action for
*/
@Input() object: ClaimedTask;
/**
* Emits the success or failure of a processed action
*/
@Output() processCompleted: EventEmitter<boolean> = new EventEmitter<boolean>();
/**
* A boolean representing if the operation is pending
*/
processing$ = new BehaviorSubject<boolean>(false);
/**
* Method called when the action's button is clicked
*/
abstract process();
}

View File

@@ -1,8 +1,8 @@
<button type="button" <button type="button"
[className]="'btn btn-success ' + wrapperClass" [className]="'btn btn-success ' + wrapperClass"
ngbTooltip="{{'submission.workflow.tasks.claimed.approve_help' | translate}}" ngbTooltip="{{'submission.workflow.tasks.claimed.approve_help' | translate}}"
[disabled]="processingApprove" [disabled]="processing$ | async"
(click)="confirmApprove()"> (click)="process()">
<span *ngIf="processingApprove"><i class='fas fa-circle-notch fa-spin'></i> {{'submission.workflow.tasks.generic.processing' | translate}}</span> <span *ngIf="processing$ | async"><i class='fas fa-circle-notch fa-spin'></i> {{'submission.workflow.tasks.generic.processing' | translate}}</span>
<span *ngIf="!processingApprove"><i class="fa fa-thumbs-up"></i> {{'submission.workflow.tasks.claimed.approve' | translate}}</span> <span *ngIf="!(processing$ | async)"><i class="fa fa-thumbs-up"></i> {{'submission.workflow.tasks.claimed.approve' | translate}}</span>
</button> </button>

View File

@@ -1,32 +1,33 @@
import { Component, EventEmitter, Input, Output } from '@angular/core'; import { Component } from '@angular/core';
import { ClaimedTaskActionsAbstractComponent } from '../abstract/claimed-task-actions-abstract.component';
import { ProcessTaskResponse } from '../../../../core/tasks/models/process-task-response';
import { rendersWorkflowTaskOption } from '../switcher/claimed-task-actions-decorator';
import { ClaimedTaskDataService } from '../../../../core/tasks/claimed-task-data.service';
@rendersWorkflowTaskOption('submit_approve')
@Component({ @Component({
selector: 'ds-claimed-task-actions-approve', selector: 'ds-claimed-task-actions-approve',
styleUrls: ['./claimed-task-actions-approve.component.scss'], styleUrls: ['./claimed-task-actions-approve.component.scss'],
templateUrl: './claimed-task-actions-approve.component.html', templateUrl: './claimed-task-actions-approve.component.html',
}) })
/**
* Component for displaying and processing the approve action on a workflow task item
*/
export class ClaimedTaskActionsApproveComponent extends ClaimedTaskActionsAbstractComponent {
export class ClaimedTaskActionsApproveComponent { constructor(protected claimedTaskService: ClaimedTaskDataService) {
super();
}
/** /**
* A boolean representing if a reject operation is pending * Approve the task
*/ */
@Input() processingApprove: boolean; process() {
this.processing$.next(true);
/** this.claimedTaskService.approveTask(this.object.id)
* CSS classes to append to reject button .subscribe((res: ProcessTaskResponse) => {
*/ this.processing$.next(false);
@Input() wrapperClass: string; this.processCompleted.emit(res.hasSucceeded);
});
/**
* An event fired when a approve action is confirmed.
*/
@Output() approve: EventEmitter<any> = new EventEmitter<any>();
/**
* Emit approve event
*/
confirmApprove() {
this.approve.emit();
} }
} }

View File

@@ -1,20 +1,13 @@
<ng-container *ngVar="(actionRD$ | async)?.payload as workflowAction">
<a [class.disabled]="!(object.workflowitem | async)?.hasSucceeded" <div class="mt-1 mb-3">
class="btn btn-primary mt-1 mb-3" <ds-claimed-task-actions-loader *ngFor="let option of workflowAction?.options"
ngbTooltip="{{'submission.workflow.tasks.claimed.edit_help' | translate}}" [option]="option"
[routerLink]="['/workflowitems/' + (object?.workflowitem | async)?.payload?.id + '/edit']" [object]="object"
role="button"> (processCompleted)="handleActionResponse($event)">
<i class="fa fa-edit"></i> {{'submission.workflow.tasks.claimed.edit' | translate}} </ds-claimed-task-actions-loader>
</a> <ds-claimed-task-actions-loader [option]="returnToPoolOption"
[object]="object"
<ds-claimed-task-actions-approve [processingApprove]="(processingApprove$ | async)" (processCompleted)="handleActionResponse($event)">
[wrapperClass]="'mt-1 mb-3'" </ds-claimed-task-actions-loader>
(approve)="approve()"></ds-claimed-task-actions-approve> </div>
</ng-container>
<ds-claimed-task-actions-reject [processingReject]="(processingReject$ | async)"
[wrapperClass]="'mt-1 mb-3'"
(reject)="reject($event)"></ds-claimed-task-actions-reject>
<ds-claimed-task-actions-return-to-pool [processingReturnToPool]="(processingReturnToPool$ | async)"
[wrapperClass]="'mt-1 mb-3'"
(returnToPool)="returnToPool()"></ds-claimed-task-actions-return-to-pool>

View File

@@ -1,13 +1,12 @@
import { Component, Injector, Input, OnInit } from '@angular/core'; import { Component, Injector, Input, OnInit } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { BehaviorSubject, Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { filter, map } from 'rxjs/operators'; import { filter, map } from 'rxjs/operators';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { ClaimedTaskDataService } from '../../../core/tasks/claimed-task-data.service'; import { ClaimedTaskDataService } from '../../../core/tasks/claimed-task-data.service';
import { ClaimedTask } from '../../../core/tasks/models/claimed-task-object.model'; import { ClaimedTask } from '../../../core/tasks/models/claimed-task-object.model';
import { ProcessTaskResponse } from '../../../core/tasks/models/process-task-response';
import { isNotUndefined } from '../../empty.util'; import { isNotUndefined } from '../../empty.util';
import { WorkflowItem } from '../../../core/submission/models/workflowitem.model'; import { WorkflowItem } from '../../../core/submission/models/workflowitem.model';
import { RemoteData } from '../../../core/data/remote-data'; import { RemoteData } from '../../../core/data/remote-data';
@@ -15,6 +14,9 @@ import { MyDSpaceActionsComponent } from '../mydspace-actions';
import { NotificationsService } from '../../notifications/notifications.service'; import { NotificationsService } from '../../notifications/notifications.service';
import { RequestService } from '../../../core/data/request.service'; import { RequestService } from '../../../core/data/request.service';
import { SearchService } from '../../../core/shared/search/search.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 } from './return-to-pool/claimed-task-actions-return-to-pool.component';
/** /**
* This component represents actions related to ClaimedTask object. * This component represents actions related to ClaimedTask object.
@@ -37,19 +39,15 @@ export class ClaimedTaskActionsComponent extends MyDSpaceActionsComponent<Claime
public workflowitem$: Observable<WorkflowItem>; public workflowitem$: Observable<WorkflowItem>;
/** /**
* A boolean representing if an approve operation is pending * The workflow action available for this task
*/ */
public processingApprove$ = new BehaviorSubject<boolean>(false); public actionRD$: Observable<RemoteData<WorkflowAction>>;
/** /**
* A boolean representing if a reject operation is pending * The option used to render the "return to pool" component
* Every claimed task contains this option
*/ */
public processingReject$ = new BehaviorSubject<boolean>(false); public returnToPoolOption = WORKFLOW_TASK_OPTION_RETURN;
/**
* A boolean representing if a return to pool operation is pending
*/
public processingReturnToPool$ = new BehaviorSubject<boolean>(false);
/** /**
* Initialize instance variables * Initialize instance variables
@@ -60,13 +58,15 @@ export class ClaimedTaskActionsComponent extends MyDSpaceActionsComponent<Claime
* @param {TranslateService} translate * @param {TranslateService} translate
* @param {SearchService} searchService * @param {SearchService} searchService
* @param {RequestService} requestService * @param {RequestService} requestService
* @param workflowActionService
*/ */
constructor(protected injector: Injector, constructor(protected injector: Injector,
protected router: Router, protected router: Router,
protected notificationsService: NotificationsService, protected notificationsService: NotificationsService,
protected translate: TranslateService, protected translate: TranslateService,
protected searchService: SearchService, protected searchService: SearchService,
protected requestService: RequestService) { protected requestService: RequestService,
protected workflowActionService: WorkflowActionDataService) {
super(ClaimedTask.type, injector, router, notificationsService, translate, searchService, requestService); super(ClaimedTask.type, injector, router, notificationsService, translate, searchService, requestService);
} }
@@ -75,6 +75,7 @@ export class ClaimedTaskActionsComponent extends MyDSpaceActionsComponent<Claime
*/ */
ngOnInit() { ngOnInit() {
this.initObjects(this.object); this.initObjects(this.object);
this.initAction(this.object);
} }
/** /**
@@ -90,39 +91,12 @@ export class ClaimedTaskActionsComponent extends MyDSpaceActionsComponent<Claime
} }
/** /**
* Approve the task. * Init the WorkflowAction
*
* @param object
*/ */
approve() { initAction(object: ClaimedTask) {
this.processingApprove$.next(true); this.actionRD$ = this.workflowActionService.findById(object.action);
this.objectDataService.approveTask(this.object.id)
.subscribe((res: ProcessTaskResponse) => {
this.processingApprove$.next(false);
this.handleActionResponse(res.hasSucceeded);
});
}
/**
* Reject the task.
*/
reject(reason) {
this.processingReject$.next(true);
this.objectDataService.rejectTask(reason, this.object.id)
.subscribe((res: ProcessTaskResponse) => {
this.processingReject$.next(false);
this.handleActionResponse(res.hasSucceeded);
});
}
/**
* Return task to the pool.
*/
returnToPool() {
this.processingReturnToPool$.next(true);
this.objectDataService.returnToPoolTask(this.object.id)
.subscribe((res: ProcessTaskResponse) => {
this.processingReturnToPool$.next(false);
this.handleActionResponse(res.hasSucceeded);
});
} }
} }

View File

@@ -0,0 +1,7 @@
<a *ngIf="object"
class="btn btn-primary"
ngbTooltip="{{'submission.workflow.tasks.claimed.edit_help' | translate}}"
[routerLink]="['/workflowitems/' + (object.workflowitem | async)?.payload.id + '/edit']"
role="button">
<i class="fa fa-edit"></i> {{'submission.workflow.tasks.claimed.edit' | translate}}
</a>

View File

@@ -0,0 +1,65 @@
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { ClaimedTaskActionsEditMetadataComponent } from './claimed-task-actions-approve.component';
import { MockTranslateLoader } from '../../../mocks/mock-translate-loader';
let component: ClaimedTaskActionsEditMetadataComponent;
let fixture: ComponentFixture<ClaimedTaskActionsEditMetadataComponent>;
describe('ClaimedTaskActionsApproveComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: MockTranslateLoader
}
})
],
declarations: [ClaimedTaskActionsEditMetadataComponent],
schemas: [NO_ERRORS_SCHEMA]
}).overrideComponent(ClaimedTaskActionsEditMetadataComponent, {
set: { changeDetection: ChangeDetectionStrategy.Default }
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ClaimedTaskActionsEditMetadataComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
afterEach(() => {
fixture = null;
component = null;
});
it('should display approve button', () => {
const btn = fixture.debugElement.query(By.css('.btn-success'));
expect(btn).toBeDefined();
});
it('should display spin icon when approve is pending', () => {
component.processingApprove = true;
fixture.detectChanges();
const span = fixture.debugElement.query(By.css('.btn-success .fa-spin'));
expect(span).toBeDefined();
});
it('should emit approve event', () => {
spyOn(component.approve, 'emit');
component.confirmApprove();
fixture.detectChanges();
expect(component.approve.emit).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,20 @@
import { Component } from '@angular/core';
import { ClaimedTaskActionsAbstractComponent } from '../abstract/claimed-task-actions-abstract.component';
import { ProcessTaskResponse } from '../../../../core/tasks/models/process-task-response';
import { rendersWorkflowTaskOption } from '../switcher/claimed-task-actions-decorator';
import { ClaimedTaskDataService } from '../../../../core/tasks/claimed-task-data.service';
@rendersWorkflowTaskOption('submit_edit_metadata')
@Component({
selector: 'ds-claimed-task-actions-edit-metadata',
styleUrls: ['./claimed-task-actions-edit-metadata.component.scss'],
templateUrl: './claimed-task-actions-edit-metadata.component.html',
})
/**
* Component for displaying the edit metadata action on a workflow task item
*/
export class ClaimedTaskActionsEditMetadataComponent extends ClaimedTaskActionsAbstractComponent {
process() {
// Nothing needs to happen for the edit-metadata button, it simply renders a link to another page
}
}

View File

@@ -1,10 +1,10 @@
<ng-template #rejectTipContent><p [innerHTML]="'submission.workflow.tasks.claimed.reject_help' | translate"></p></ng-template> <ng-template #rejectTipContent><p [innerHTML]="'submission.workflow.tasks.claimed.reject_help' | translate"></p></ng-template>
<button [className]="'btn btn-danger ' + wrapperClass" <button [className]="'btn btn-danger ' + wrapperClass"
[ngbTooltip]="rejectTipContent" [ngbTooltip]="rejectTipContent"
[disabled]="processingReject" [disabled]="processing$ | async"
(click)="openRejectModal(rejectModal)" > (click)="openRejectModal(rejectModal)" >
<span *ngIf="processingReject"><i class='fas fa-circle-notch fa-spin'></i> {{'submission.workflow.tasks.generic.processing' | translate}}</span> <span *ngIf="processing$ | async"><i class='fas fa-circle-notch fa-spin'></i> {{'submission.workflow.tasks.generic.processing' | translate}}</span>
<span *ngIf="!processingReject"><i class="fa fa-trash"></i> {{'submission.workflow.tasks.claimed.reject.submit' | translate}}</span> <span *ngIf="!(processing$ | async)"><i class="fa fa-trash"></i> {{'submission.workflow.tasks.claimed.reject.submit' | translate}}</span>
</button> </button>
<ng-template #rejectModal let-c="close" let-d="dismiss"> <ng-template #rejectModal let-c="close" let-d="dismiss">
@@ -21,17 +21,17 @@
<div class="alert alert-info" role="alert"> <div class="alert alert-info" role="alert">
{{'submission.workflow.tasks.claimed.reject.reason.info' | translate}} {{'submission.workflow.tasks.claimed.reject.reason.info' | translate}}
</div> </div>
<form (ngSubmit)="confirmReject(rejectModal);" [formGroup]="rejectForm" > <form (ngSubmit)="process(rejectModal);" [formGroup]="rejectForm" >
<textarea style="width: 100%" <textarea style="width: 100%"
formControlName="reason" formControlName="reason"
rows="4" rows="4"
placeholder="{{'submission.workflow.tasks.claimed.reject.reason.placeholder' | translate}}"></textarea> placeholder="{{'submission.workflow.tasks.claimed.reject.reason.placeholder' | translate}}"></textarea>
<button id="btn-chat" <button id="btn-chat"
class="btn btn-danger btn-lg btn-block mt-3" class="btn btn-danger btn-lg btn-block mt-3"
[disabled]="!rejectForm.valid || processingReject" [disabled]="!rejectForm.valid || (processing$ | async)"
type="submit"> type="submit">
<span *ngIf="processingReject"><i class='fas fa-circle-notch fa-spin'></i> {{'submission.workflow.tasks.generic.processing' | translate}}</span> <span *ngIf="processing$ | async"><i class='fas fa-circle-notch fa-spin'></i> {{'submission.workflow.tasks.generic.processing' | translate}}</span>
<span *ngIf="!processingReject">{{'submission.workflow.tasks.claimed.reject.reason.submit' | translate}}</span> <span *ngIf="!(processing$ | async)">{{'submission.workflow.tasks.claimed.reject.reason.submit' | translate}}</span>
</button> </button>
</form> </form>
</div> </div>

View File

@@ -1,26 +1,22 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { Component, EventEmitter, OnInit, Output } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
import { ClaimedTaskActionsAbstractComponent } from '../abstract/claimed-task-actions-abstract.component';
import { ClaimedTaskDataService } from '../../../../core/tasks/claimed-task-data.service';
import { ProcessTaskResponse } from '../../../../core/tasks/models/process-task-response';
import { rendersWorkflowTaskOption } from '../switcher/claimed-task-actions-decorator';
@rendersWorkflowTaskOption('submit_reject')
@Component({ @Component({
selector: 'ds-claimed-task-actions-reject', selector: 'ds-claimed-task-actions-reject',
styleUrls: ['./claimed-task-actions-reject.component.scss'], styleUrls: ['./claimed-task-actions-reject.component.scss'],
templateUrl: './claimed-task-actions-reject.component.html', templateUrl: './claimed-task-actions-reject.component.html',
}) })
/**
export class ClaimedTaskActionsRejectComponent implements OnInit { * Component for displaying and processing the reject action on a workflow task item
*/
/** export class ClaimedTaskActionsRejectComponent extends ClaimedTaskActionsAbstractComponent implements OnInit {
* A boolean representing if a reject operation is pending
*/
@Input() processingReject: boolean;
/**
* CSS classes to append to reject button
*/
@Input() wrapperClass: string;
/** /**
* An event fired when a reject action is confirmed. * An event fired when a reject action is confirmed.
* Event's payload equals to reject reason. * Event's payload equals to reject reason.
@@ -42,8 +38,12 @@ export class ClaimedTaskActionsRejectComponent implements OnInit {
* *
* @param {FormBuilder} formBuilder * @param {FormBuilder} formBuilder
* @param {NgbModal} modalService * @param {NgbModal} modalService
* @param claimedTaskService
*/ */
constructor(private formBuilder: FormBuilder, private modalService: NgbModal) { constructor(protected claimedTaskService: ClaimedTaskDataService,
private formBuilder: FormBuilder,
private modalService: NgbModal) {
super();
} }
/** /**
@@ -53,17 +53,20 @@ export class ClaimedTaskActionsRejectComponent implements OnInit {
this.rejectForm = this.formBuilder.group({ this.rejectForm = this.formBuilder.group({
reason: ['', Validators.required] reason: ['', Validators.required]
}); });
} }
/** /**
* Close modal and emit reject event * Reject the task
*/ */
confirmReject() { process() {
this.processingReject = true; this.processing$.next(true);
this.modalRef.close('Send Button');
const reason = this.rejectForm.get('reason').value; const reason = this.rejectForm.get('reason').value;
this.reject.emit(reason); this.modalRef.close('Send Button');
this.claimedTaskService.rejectTask(reason, this.object.id)
.subscribe((res: ProcessTaskResponse) => {
this.processing$.next(false);
this.processCompleted.emit(res.hasSucceeded);
});
} }
/** /**

View File

@@ -1,8 +1,8 @@
<button type="button" <button type="button"
[className]="'btn btn-secondary ' + wrapperClass" [className]="'btn btn-secondary ' + wrapperClass"
ngbTooltip="{{'submission.workflow.tasks.claimed.return_help' | translate}}" ngbTooltip="{{'submission.workflow.tasks.claimed.return_help' | translate}}"
[disabled]="processingReturnToPool" [disabled]="processing$ | async"
(click)="confirmReturnToPool()"> (click)="process()">
<span *ngIf="processingReturnToPool"><i class='fas fa-circle-notch fa-spin'></i> {{'submission.workflow.tasks.generic.processing' | translate}}</span> <span *ngIf="processing$ | async"><i class='fas fa-circle-notch fa-spin'></i> {{'submission.workflow.tasks.generic.processing' | translate}}</span>
<span *ngIf="!processingReturnToPool"><i class="fa fa-undo"></i> {{'submission.workflow.tasks.claimed.return' | translate}}</span> <span *ngIf="!(processing$ | async)"><i class="fa fa-undo"></i> {{'submission.workflow.tasks.claimed.return' | translate}}</span>
</button> </button>

View File

@@ -1,32 +1,35 @@
import { Component, EventEmitter, Input, Output } from '@angular/core'; import { Component } from '@angular/core';
import { ProcessTaskResponse } from '../../../../core/tasks/models/process-task-response';
import { ClaimedTaskActionsAbstractComponent } from '../abstract/claimed-task-actions-abstract.component';
import { rendersWorkflowTaskOption } from '../switcher/claimed-task-actions-decorator';
import { ClaimedTaskDataService } from '../../../../core/tasks/claimed-task-data.service';
export const WORKFLOW_TASK_OPTION_RETURN = 'return_to_pool';
@rendersWorkflowTaskOption(WORKFLOW_TASK_OPTION_RETURN)
@Component({ @Component({
selector: 'ds-claimed-task-actions-return-to-pool', selector: 'ds-claimed-task-actions-return-to-pool',
styleUrls: ['./claimed-task-actions-return-to-pool.component.scss'], styleUrls: ['./claimed-task-actions-return-to-pool.component.scss'],
templateUrl: './claimed-task-actions-return-to-pool.component.html', templateUrl: './claimed-task-actions-return-to-pool.component.html',
}) })
/**
* Component for displaying and processing the return to pool action on a workflow task item
*/
export class ClaimedTaskActionsReturnToPoolComponent extends ClaimedTaskActionsAbstractComponent {
export class ClaimedTaskActionsReturnToPoolComponent { constructor(protected claimedTaskService: ClaimedTaskDataService) {
super();
}
/** /**
* A boolean representing if a return to pool operation is pending * Return task to pool
*/ */
@Input() processingReturnToPool: boolean; process() {
this.processing$.next(true);
/** this.claimedTaskService.returnToPoolTask(this.object.id)
* CSS classes to append to return to pool button .subscribe((res: ProcessTaskResponse) => {
*/ this.processing$.next(false);
@Input() wrapperClass: string; this.processCompleted.emit(res.hasSucceeded);
});
/**
* An event fired when a return to pool action is confirmed.
*/
@Output() returnToPool: EventEmitter<any> = new EventEmitter<any>();
/**
* Emit returnToPool event
*/
confirmReturnToPool() {
this.returnToPool.emit();
} }
} }

View File

@@ -0,0 +1,23 @@
import { hasNoValue } from '../../../empty.util';
const map = 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);
} else {
throw new Error(`There can't be more than one component to render ClaimedTaskActions for option "${option}"`);
}
};
}
/**
* Get the component used for rendering a ClaimedTaskActions page by option type
*/
export function getComponentByWorkflowTaskOption(option: string) {
return map.get(option);
}

View File

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

View File

@@ -0,0 +1,85 @@
import {
Component,
ComponentFactoryResolver,
EventEmitter,
Input,
OnDestroy,
OnInit,
Output,
ViewChild
} from '@angular/core';
import { getComponentByWorkflowTaskOption } from './claimed-task-actions-decorator';
import { ClaimedTask } from '../../../../core/tasks/models/claimed-task-object.model';
import { ClaimedTaskActionsDirective } from './claimed-task-actions.directive';
import { ClaimedTaskActionsAbstractComponent } from '../abstract/claimed-task-actions-abstract.component';
import { hasValue } from '../../../empty.util';
import { Subscription } from 'rxjs/internal/Subscription';
@Component({
selector: 'ds-claimed-task-actions-loader',
templateUrl: './claimed-task-actions-loader.component.html'
})
/**
* Component for loading a ClaimedTaskAction component depending on the "option" input
* Passes on the ClaimedTask to the component and subscribes to the processCompleted output
*/
export class ClaimedTaskActionsLoaderComponent implements OnInit, OnDestroy {
/**
* The ClaimedTask object
*/
@Input() object: ClaimedTask;
/**
* The name of the option to render
* Passed on to the decorator to fetch the relevant component for this option
*/
@Input() option: string;
/**
* Emits the success or failure of a processed action
*/
@Output() processCompleted: EventEmitter<boolean> = new EventEmitter<boolean>();
/**
* Directive to determine where the dynamic child component is located
*/
@ViewChild(ClaimedTaskActionsDirective, {static: true}) claimedTaskActionsDirective: ClaimedTaskActionsDirective;
/**
* Array to track all subscriptions and unsubscribe them onDestroy
* @type {Array}
*/
protected subs: Subscription[] = [];
constructor(private componentFactoryResolver: ComponentFactoryResolver) {
}
/**
* Fetch, create and initialize the relevant component
*/
ngOnInit(): void {
const comp = getComponentByWorkflowTaskOption(this.option);
if (hasValue(comp)) {
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(comp);
const viewContainerRef = this.claimedTaskActionsDirective.viewContainerRef;
viewContainerRef.clear();
const componentRef = viewContainerRef.createComponent(componentFactory);
const componentInstance = (componentRef.instance as ClaimedTaskActionsAbstractComponent);
componentInstance.object = this.object;
if (hasValue(componentInstance.processCompleted)) {
this.subs.push(componentInstance.processCompleted.subscribe((success) => this.processCompleted.emit(success)));
}
}
}
/**
* Unsubscribe from open subscriptions
*/
ngOnDestroy(): void {
this.subs
.filter((subscription) => hasValue(subscription))
.forEach((subscription) => subscription.unsubscribe());
}
}

View File

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

View File

@@ -177,6 +177,9 @@ import { ImportableListItemControlComponent } from './object-collection/shared/i
import { DragDropModule } from '@angular/cdk/drag-drop'; import { DragDropModule } from '@angular/cdk/drag-drop';
import { ExistingMetadataListElementComponent } from './form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component'; import { ExistingMetadataListElementComponent } from './form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component';
import { SortablejsModule } from 'ngx-sortablejs'; import { SortablejsModule } from 'ngx-sortablejs';
import { ClaimedTaskActionsLoaderComponent } from './mydspace-actions/claimed-task/switcher/claimed-task-actions-loader.component';
import { ClaimedTaskActionsDirective } from './mydspace-actions/claimed-task/switcher/claimed-task-actions.directive';
import { ClaimedTaskActionsEditMetadataComponent } from './mydspace-actions/claimed-task/edit-metadata/claimed-task-actions-edit-metadata.component';
const MODULES = [ const MODULES = [
// Do NOT include UniversalModule, HttpModule, or JsonpModule here // Do NOT include UniversalModule, HttpModule, or JsonpModule here
@@ -279,6 +282,8 @@ const COMPONENTS = [
ClaimedTaskActionsApproveComponent, ClaimedTaskActionsApproveComponent,
ClaimedTaskActionsRejectComponent, ClaimedTaskActionsRejectComponent,
ClaimedTaskActionsReturnToPoolComponent, ClaimedTaskActionsReturnToPoolComponent,
ClaimedTaskActionsEditMetadataComponent,
ClaimedTaskActionsLoaderComponent,
ItemActionsComponent, ItemActionsComponent,
PoolTaskActionsComponent, PoolTaskActionsComponent,
WorkflowitemActionsComponent, WorkflowitemActionsComponent,
@@ -402,7 +407,11 @@ const ENTRY_COMPONENTS = [
DsDynamicLookupRelationSearchTabComponent, DsDynamicLookupRelationSearchTabComponent,
DsDynamicLookupRelationSelectionTabComponent, DsDynamicLookupRelationSelectionTabComponent,
DsDynamicLookupRelationExternalSourceTabComponent, DsDynamicLookupRelationExternalSourceTabComponent,
ExternalSourceEntryImportModalComponent ExternalSourceEntryImportModalComponent,
ClaimedTaskActionsApproveComponent,
ClaimedTaskActionsRejectComponent,
ClaimedTaskActionsReturnToPoolComponent,
ClaimedTaskActionsEditMetadataComponent
]; ];
const SHARED_ITEM_PAGE_COMPONENTS = [ const SHARED_ITEM_PAGE_COMPONENTS = [
@@ -430,7 +439,8 @@ const DIRECTIVES = [
AutoFocusDirective, AutoFocusDirective,
RoleDirective, RoleDirective,
MetadataRepresentationDirective, MetadataRepresentationDirective,
ListableObjectDirective ListableObjectDirective,
ClaimedTaskActionsDirective
]; ];
@NgModule({ @NgModule({