mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
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:
@@ -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({
|
||||||
|
40
src/app/core/data/workflow-action-data.service.ts
Normal file
40
src/app/core/data/workflow-action-data.service.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@@ -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[];
|
||||||
|
}
|
19
src/app/core/tasks/models/workflow-action-object.model.ts
Normal file
19
src/app/core/tasks/models/workflow-action-object.model.ts
Normal 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[];
|
||||||
|
}
|
@@ -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();
|
||||||
|
}
|
@@ -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>
|
||||||
|
@@ -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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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>
|
|
||||||
|
@@ -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);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -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>
|
@@ -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();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@@ -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
|
||||||
|
}
|
||||||
|
}
|
@@ -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>
|
||||||
|
@@ -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 {
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A boolean representing if a reject operation is pending
|
* Component for displaying and processing the reject action on a workflow task item
|
||||||
*/
|
*/
|
||||||
@Input() processingReject: boolean;
|
export class ClaimedTaskActionsRejectComponent extends ClaimedTaskActionsAbstractComponent implements OnInit {
|
||||||
|
|
||||||
/**
|
|
||||||
* 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);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -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>
|
||||||
|
@@ -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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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);
|
||||||
|
}
|
@@ -0,0 +1 @@
|
|||||||
|
<ng-template dsClaimedTaskActions></ng-template>
|
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
@@ -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) { }
|
||||||
|
}
|
@@ -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({
|
||||||
|
Reference in New Issue
Block a user