mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
Merge pull request #1792 from atmire/w2p-93914_delete-process
Add delete process functionality
This commit is contained in:
@@ -1,53 +1,99 @@
|
|||||||
<div class="container" *ngVar="(processRD$ | async)?.payload as process">
|
<div class="container" *ngVar="(processRD$ | async)?.payload as process">
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<h2 class="flex-grow-1">{{'process.detail.title' | translate:{id: process?.processId, name: process?.scriptName} }}</h2>
|
<h2 class="flex-grow-1">{{'process.detail.title' | translate:{
|
||||||
<div>
|
id: process?.processId,
|
||||||
<button class="btn btn-lg btn-success " routerLink="/processes/new" [queryParams]="{id: process?.processId}"><i class="fas fa-plus pr-2"></i>{{'process.detail.create' | translate}}</button>
|
name: process?.scriptName
|
||||||
</div>
|
} }}</h2>
|
||||||
</div>
|
</div>
|
||||||
<ds-process-detail-field id="process-name" [title]="'process.detail.script'">
|
<ds-process-detail-field id="process-name" [title]="'process.detail.script'">
|
||||||
<div>{{ process?.scriptName }}</div>
|
<div>{{ process?.scriptName }}</div>
|
||||||
</ds-process-detail-field>
|
</ds-process-detail-field>
|
||||||
|
|
||||||
<ds-process-detail-field *ngIf="process?.parameters && process?.parameters?.length > 0" id="process-arguments" [title]="'process.detail.arguments'">
|
<ds-process-detail-field *ngIf="process?.parameters && process?.parameters?.length > 0" id="process-arguments"
|
||||||
|
[title]="'process.detail.arguments'">
|
||||||
<div *ngFor="let argument of process?.parameters">{{ argument?.name }} {{ argument?.value }}</div>
|
<div *ngFor="let argument of process?.parameters">{{ argument?.name }} {{ argument?.value }}</div>
|
||||||
</ds-process-detail-field>
|
</ds-process-detail-field>
|
||||||
|
|
||||||
<div *ngVar="(filesRD$ | async)?.payload?.page as files">
|
<div *ngVar="(filesRD$ | async)?.payload?.page as files">
|
||||||
<ds-process-detail-field *ngIf="files && files?.length > 0" id="process-files" [title]="'process.detail.output-files'">
|
<ds-process-detail-field *ngIf="files && files?.length > 0" id="process-files"
|
||||||
<ds-file-download-link *ngFor="let file of files; let last=last;" [bitstream]="file">
|
[title]="'process.detail.output-files'">
|
||||||
<span>{{getFileName(file)}}</span>
|
<ds-file-download-link *ngFor="let file of files; let last=last;" [bitstream]="file">
|
||||||
<span>({{(file?.sizeBytes) | dsFileSize }})</span>
|
<span>{{getFileName(file)}}</span>
|
||||||
</ds-file-download-link>
|
<span>({{(file?.sizeBytes) | dsFileSize }})</span>
|
||||||
|
</ds-file-download-link>
|
||||||
</ds-process-detail-field>
|
</ds-process-detail-field>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ds-process-detail-field *ngIf="process && process.startTime" id="process-start-time" [title]="'process.detail.start-time' | translate">
|
<ds-process-detail-field *ngIf="process && process.startTime" id="process-start-time"
|
||||||
|
[title]="'process.detail.start-time' | translate">
|
||||||
<div>{{ process.startTime | date:dateFormat:'UTC' }}</div>
|
<div>{{ process.startTime | date:dateFormat:'UTC' }}</div>
|
||||||
</ds-process-detail-field>
|
</ds-process-detail-field>
|
||||||
|
|
||||||
<ds-process-detail-field *ngIf="process && process.endTime" id="process-end-time" [title]="'process.detail.end-time' | translate">
|
<ds-process-detail-field *ngIf="process && process.endTime" id="process-end-time"
|
||||||
|
[title]="'process.detail.end-time' | translate">
|
||||||
<div>{{ process.endTime | date:dateFormat:'UTC' }}</div>
|
<div>{{ process.endTime | date:dateFormat:'UTC' }}</div>
|
||||||
</ds-process-detail-field>
|
</ds-process-detail-field>
|
||||||
|
|
||||||
<ds-process-detail-field *ngIf="process && process.processStatus" id="process-status" [title]="'process.detail.status' | translate">
|
<ds-process-detail-field *ngIf="process && process.processStatus" id="process-status"
|
||||||
|
[title]="'process.detail.status' | translate">
|
||||||
<div>{{ process.processStatus }}</div>
|
<div>{{ process.processStatus }}</div>
|
||||||
</ds-process-detail-field>
|
</ds-process-detail-field>
|
||||||
|
|
||||||
<ds-process-detail-field *ngIf="isProcessFinished(process)" id="process-output" [title]="'process.detail.output'">
|
<ds-process-detail-field *ngIf="isProcessFinished(process)" id="process-output" [title]="'process.detail.output'">
|
||||||
<button *ngIf="!showOutputLogs && process?._links?.output?.href != undefined" id="showOutputButton" class="btn btn-primary" (click)="showProcessOutputLogs()">
|
<button *ngIf="!showOutputLogs && process?._links?.output?.href != undefined" id="showOutputButton"
|
||||||
{{ 'process.detail.logs.button' | translate }}
|
class="btn btn-primary" (click)="showProcessOutputLogs()">
|
||||||
</button>
|
{{ 'process.detail.logs.button' | translate }}
|
||||||
<ds-themed-loading *ngIf="retrievingOutputLogs$ | async" class="ds-themed-loading" message="{{ 'process.detail.logs.loading' | translate }}"></ds-themed-loading>
|
</button>
|
||||||
<pre class="font-weight-bold text-secondary bg-light p-3"
|
<ds-themed-loading *ngIf="retrievingOutputLogs$ | async" class="ds-themed-loading"
|
||||||
*ngIf="showOutputLogs && (outputLogs$ | async)?.length > 0">{{ (outputLogs$ | async) }}</pre>
|
message="{{ 'process.detail.logs.loading' | translate }}"></ds-themed-loading>
|
||||||
<p id="no-output-logs-message" *ngIf="(!(retrievingOutputLogs$ | async) && showOutputLogs)
|
<pre class="font-weight-bold text-secondary bg-light p-3"
|
||||||
|
*ngIf="showOutputLogs && (outputLogs$ | async)?.length > 0">{{ (outputLogs$ | async) }}</pre>
|
||||||
|
<p id="no-output-logs-message" *ngIf="(!(retrievingOutputLogs$ | async) && showOutputLogs)
|
||||||
&& !(outputLogs$ | async) || (outputLogs$ | async)?.length == 0 || !process._links.output">
|
&& !(outputLogs$ | async) || (outputLogs$ | async)?.length == 0 || !process._links.output">
|
||||||
{{ 'process.detail.logs.none' | translate }}
|
{{ 'process.detail.logs.none' | translate }}
|
||||||
</p>
|
</p>
|
||||||
|
</ds-process-detail-field>
|
||||||
|
|
||||||
|
<ds-process-detail-field id="process-actions" [title]="'process.detail.actions'">
|
||||||
|
<button class="btn btn-success mr-2" routerLink="/processes/new" [queryParams]="{id: process?.processId}"><i
|
||||||
|
class="fas fa-plus pr-2"></i>{{'process.detail.create' | translate}}</button>
|
||||||
|
<button *ngIf="isProcessFinished(process)" id="delete" class="btn btn-danger"
|
||||||
|
(click)="openDeleteModal(deleteModal)">
|
||||||
|
<i class="fas fa-trash pr-2"></i>{{ 'process.detail.delete.button' | translate }}
|
||||||
|
</button>
|
||||||
</ds-process-detail-field>
|
</ds-process-detail-field>
|
||||||
|
|
||||||
<div style="text-align: right;">
|
<div style="text-align: right;">
|
||||||
<a class="btn btn-outline-secondary mt-3" [routerLink]="'/processes'">{{'process.detail.back' | translate}}</a>
|
<a class="btn btn-outline-secondary mt-3" [routerLink]="'/processes'">{{'process.detail.back' | translate}}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ng-template #deleteModal >
|
||||||
|
|
||||||
|
<div *ngVar="(processRD$ | async)?.payload as process">
|
||||||
|
|
||||||
|
<div class="modal-header">
|
||||||
|
<div>
|
||||||
|
<h4>{{'process.detail.delete.header' | translate }}</h4>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="close"
|
||||||
|
(click)="closeModal()" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<div>{{'process.detail.delete.body' | translate }}</div>
|
||||||
|
<div class="mt-4">
|
||||||
|
<button class="btn btn-primary mr-2" (click)="closeModal()">{{'process.detail.delete.cancel' | translate}}</button>
|
||||||
|
<button id="delete-confirm" class="btn btn-danger"
|
||||||
|
(click)="deleteProcess(process)">{{ 'process.detail.delete.confirm' | translate }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
@@ -19,15 +19,23 @@ import { RouterTestingModule } from '@angular/router/testing';
|
|||||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
import { ProcessDetailFieldComponent } from './process-detail-field/process-detail-field.component';
|
import { ProcessDetailFieldComponent } from './process-detail-field/process-detail-field.component';
|
||||||
import { Process } from '../processes/process.model';
|
import { Process } from '../processes/process.model';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { FileSizePipe } from '../../shared/utils/file-size-pipe';
|
import { FileSizePipe } from '../../shared/utils/file-size-pipe';
|
||||||
import { Bitstream } from '../../core/shared/bitstream.model';
|
import { Bitstream } from '../../core/shared/bitstream.model';
|
||||||
import { ProcessDataService } from '../../core/data/processes/process-data.service';
|
import { ProcessDataService } from '../../core/data/processes/process-data.service';
|
||||||
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||||
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
import {
|
||||||
|
createFailedRemoteDataObject$,
|
||||||
|
createSuccessfulRemoteDataObject,
|
||||||
|
createSuccessfulRemoteDataObject$
|
||||||
|
} from '../../shared/remote-data.utils';
|
||||||
import { createPaginatedList } from '../../shared/testing/utils.test';
|
import { createPaginatedList } from '../../shared/testing/utils.test';
|
||||||
|
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
|
||||||
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { getProcessListRoute } from '../process-page-routing.paths';
|
||||||
|
|
||||||
describe('ProcessDetailComponent', () => {
|
describe('ProcessDetailComponent', () => {
|
||||||
let component: ProcessDetailComponent;
|
let component: ProcessDetailComponent;
|
||||||
@@ -44,6 +52,11 @@ describe('ProcessDetailComponent', () => {
|
|||||||
|
|
||||||
let processOutput;
|
let processOutput;
|
||||||
|
|
||||||
|
let modalService;
|
||||||
|
let notificationsService;
|
||||||
|
|
||||||
|
let router;
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
processOutput = 'Process Started';
|
processOutput = 'Process Started';
|
||||||
process = Object.assign(new Process(), {
|
process = Object.assign(new Process(), {
|
||||||
@@ -93,7 +106,8 @@ describe('ProcessDetailComponent', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
processService = jasmine.createSpyObj('processService', {
|
processService = jasmine.createSpyObj('processService', {
|
||||||
getFiles: createSuccessfulRemoteDataObject$(createPaginatedList(files))
|
getFiles: createSuccessfulRemoteDataObject$(createPaginatedList(files)),
|
||||||
|
delete: createSuccessfulRemoteDataObject$(null)
|
||||||
});
|
});
|
||||||
bitstreamDataService = jasmine.createSpyObj('bitstreamDataService', {
|
bitstreamDataService = jasmine.createSpyObj('bitstreamDataService', {
|
||||||
findByHref: createSuccessfulRemoteDataObject$(logBitstream)
|
findByHref: createSuccessfulRemoteDataObject$(logBitstream)
|
||||||
@@ -104,13 +118,23 @@ describe('ProcessDetailComponent', () => {
|
|||||||
httpClient = jasmine.createSpyObj('httpClient', {
|
httpClient = jasmine.createSpyObj('httpClient', {
|
||||||
get: observableOf(processOutput)
|
get: observableOf(processOutput)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modalService = jasmine.createSpyObj('modalService', {
|
||||||
|
open: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
notificationsService = new NotificationsServiceStub();
|
||||||
|
|
||||||
|
router = jasmine.createSpyObj('router', {
|
||||||
|
navigateByUrl:{}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
init();
|
init();
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [ProcessDetailComponent, ProcessDetailFieldComponent, VarDirective, FileSizePipe],
|
declarations: [ProcessDetailComponent, ProcessDetailFieldComponent, VarDirective, FileSizePipe],
|
||||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
|
imports: [TranslateModule.forRoot()],
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
provide: ActivatedRoute,
|
provide: ActivatedRoute,
|
||||||
@@ -121,6 +145,9 @@ describe('ProcessDetailComponent', () => {
|
|||||||
{ provide: DSONameService, useValue: nameService },
|
{ provide: DSONameService, useValue: nameService },
|
||||||
{ provide: AuthService, useValue: new AuthServiceMock() },
|
{ provide: AuthService, useValue: new AuthServiceMock() },
|
||||||
{ provide: HttpClient, useValue: httpClient },
|
{ provide: HttpClient, useValue: httpClient },
|
||||||
|
{ provide: NgbModal, useValue: modalService },
|
||||||
|
{ provide: NotificationsService, useValue: notificationsService },
|
||||||
|
{ provide: Router, useValue: router },
|
||||||
],
|
],
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
@@ -207,4 +234,34 @@ describe('ProcessDetailComponent', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('openDeleteModal', () => {
|
||||||
|
it('should open the modal', () => {
|
||||||
|
component.openDeleteModal({});
|
||||||
|
expect(modalService.open).toHaveBeenCalledWith({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deleteProcess', () => {
|
||||||
|
it('should delete the process and navigate back to the overview page on success', () => {
|
||||||
|
spyOn(component, 'closeModal');
|
||||||
|
component.deleteProcess(process);
|
||||||
|
|
||||||
|
expect(processService.delete).toHaveBeenCalledWith(process.processId);
|
||||||
|
expect(notificationsService.success).toHaveBeenCalled();
|
||||||
|
expect(component.closeModal).toHaveBeenCalled();
|
||||||
|
expect(router.navigateByUrl).toHaveBeenCalledWith(getProcessListRoute());
|
||||||
|
});
|
||||||
|
it('should delete the process and not navigate on error', () => {
|
||||||
|
(processService.delete as jasmine.Spy).and.returnValue(createFailedRemoteDataObject$());
|
||||||
|
spyOn(component, 'closeModal');
|
||||||
|
|
||||||
|
component.deleteProcess(process);
|
||||||
|
|
||||||
|
expect(processService.delete).toHaveBeenCalledWith(process.processId);
|
||||||
|
expect(notificationsService.error).toHaveBeenCalled();
|
||||||
|
expect(component.closeModal).not.toHaveBeenCalled();
|
||||||
|
expect(router.navigateByUrl).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -12,8 +12,9 @@ import { RemoteData } from '../../core/data/remote-data';
|
|||||||
import { Bitstream } from '../../core/shared/bitstream.model';
|
import { Bitstream } from '../../core/shared/bitstream.model';
|
||||||
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||||
import {
|
import {
|
||||||
getFirstSucceededRemoteDataPayload,
|
getFirstCompletedRemoteData,
|
||||||
getFirstSucceededRemoteData
|
getFirstSucceededRemoteData,
|
||||||
|
getFirstSucceededRemoteDataPayload
|
||||||
} from '../../core/shared/operators';
|
} from '../../core/shared/operators';
|
||||||
import { URLCombiner } from '../../core/url-combiner/url-combiner';
|
import { URLCombiner } from '../../core/url-combiner/url-combiner';
|
||||||
import { AlertType } from '../../shared/alert/aletr-type';
|
import { AlertType } from '../../shared/alert/aletr-type';
|
||||||
@@ -21,6 +22,10 @@ import { hasValue } from '../../shared/empty.util';
|
|||||||
import { ProcessStatus } from '../processes/process-status.model';
|
import { ProcessStatus } from '../processes/process-status.model';
|
||||||
import { Process } from '../processes/process.model';
|
import { Process } from '../processes/process.model';
|
||||||
import { redirectOn4xx } from '../../core/shared/authorized.operators';
|
import { redirectOn4xx } from '../../core/shared/authorized.operators';
|
||||||
|
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { getProcessListRoute } from '../process-page-routing.paths';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-process-detail',
|
selector: 'ds-process-detail',
|
||||||
@@ -71,6 +76,11 @@ export class ProcessDetailComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
dateFormat = 'yyyy-MM-dd HH:mm:ss ZZZZ';
|
dateFormat = 'yyyy-MM-dd HH:mm:ss ZZZZ';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reference to NgbModal
|
||||||
|
*/
|
||||||
|
protected modalRef: NgbModalRef;
|
||||||
|
|
||||||
constructor(protected route: ActivatedRoute,
|
constructor(protected route: ActivatedRoute,
|
||||||
protected router: Router,
|
protected router: Router,
|
||||||
protected processService: ProcessDataService,
|
protected processService: ProcessDataService,
|
||||||
@@ -78,7 +88,11 @@ export class ProcessDetailComponent implements OnInit {
|
|||||||
protected nameService: DSONameService,
|
protected nameService: DSONameService,
|
||||||
private zone: NgZone,
|
private zone: NgZone,
|
||||||
protected authService: AuthService,
|
protected authService: AuthService,
|
||||||
protected http: HttpClient) {
|
protected http: HttpClient,
|
||||||
|
protected modalService: NgbModal,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected translateService: TranslateService
|
||||||
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -172,4 +186,36 @@ export class ProcessDetailComponent implements OnInit {
|
|||||||
|| process.processStatus.toString() === ProcessStatus[ProcessStatus.FAILED].toString()));
|
|| process.processStatus.toString() === ProcessStatus[ProcessStatus.FAILED].toString()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete the current process
|
||||||
|
* @param process
|
||||||
|
*/
|
||||||
|
deleteProcess(process: Process) {
|
||||||
|
this.processService.delete(process.processId).pipe(
|
||||||
|
getFirstCompletedRemoteData()
|
||||||
|
).subscribe((rd) => {
|
||||||
|
if (rd.hasSucceeded) {
|
||||||
|
this.notificationsService.success(this.translateService.get('process.detail.delete.success'));
|
||||||
|
this.closeModal();
|
||||||
|
this.router.navigateByUrl(getProcessListRoute());
|
||||||
|
} else {
|
||||||
|
this.notificationsService.error(this.translateService.get('process.detail.delete.error'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open a given modal.
|
||||||
|
* @param content - the modal content.
|
||||||
|
*/
|
||||||
|
openDeleteModal(content) {
|
||||||
|
this.modalRef = this.modalService.open(content);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Close the modal.
|
||||||
|
*/
|
||||||
|
closeModal() {
|
||||||
|
this.modalRef.close();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,149 @@
|
|||||||
|
import { ProcessBulkDeleteService } from './process-bulk-delete.service';
|
||||||
|
import { waitForAsync } from '@angular/core/testing';
|
||||||
|
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
|
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
|
||||||
|
import { getMockTranslateService } from '../../shared/mocks/translate.service.mock';
|
||||||
|
|
||||||
|
describe('ProcessBulkDeleteService', () => {
|
||||||
|
|
||||||
|
let service: ProcessBulkDeleteService;
|
||||||
|
let processDataService;
|
||||||
|
let notificationsService;
|
||||||
|
let mockTranslateService;
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
processDataService = jasmine.createSpyObj('processDataService', {
|
||||||
|
delete: createSuccessfulRemoteDataObject$(null)
|
||||||
|
});
|
||||||
|
notificationsService = new NotificationsServiceStub();
|
||||||
|
mockTranslateService = getMockTranslateService();
|
||||||
|
service = new ProcessBulkDeleteService(processDataService, notificationsService, mockTranslateService);
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('toggleDelete', () => {
|
||||||
|
it('should add a new value to the processesToDelete list when not yet present', () => {
|
||||||
|
service.toggleDelete('test-id-1');
|
||||||
|
service.toggleDelete('test-id-2');
|
||||||
|
|
||||||
|
expect(service.processesToDelete).toEqual(['test-id-1', 'test-id-2']);
|
||||||
|
});
|
||||||
|
it('should remove a value from the processesToDelete list when already present', () => {
|
||||||
|
service.toggleDelete('test-id-1');
|
||||||
|
service.toggleDelete('test-id-2');
|
||||||
|
|
||||||
|
expect(service.processesToDelete).toEqual(['test-id-1', 'test-id-2']);
|
||||||
|
|
||||||
|
service.toggleDelete('test-id-1');
|
||||||
|
expect(service.processesToDelete).toEqual(['test-id-2']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isToBeDeleted', () => {
|
||||||
|
it('should return true when the provided process id is present in the list', () => {
|
||||||
|
service.toggleDelete('test-id-1');
|
||||||
|
service.toggleDelete('test-id-2');
|
||||||
|
|
||||||
|
expect(service.isToBeDeleted('test-id-1')).toBeTrue();
|
||||||
|
});
|
||||||
|
it('should return false when the provided process id is not present in the list', () => {
|
||||||
|
service.toggleDelete('test-id-1');
|
||||||
|
service.toggleDelete('test-id-2');
|
||||||
|
|
||||||
|
expect(service.isToBeDeleted('test-id-3')).toBeFalse();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('clearAllProcesses', () => {
|
||||||
|
it('should clear the list of to be deleted processes', () => {
|
||||||
|
service.toggleDelete('test-id-1');
|
||||||
|
service.toggleDelete('test-id-2');
|
||||||
|
|
||||||
|
expect(service.processesToDelete).toEqual(['test-id-1', 'test-id-2']);
|
||||||
|
|
||||||
|
service.clearAllProcesses();
|
||||||
|
expect(service.processesToDelete).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('getAmountOfSelectedProcesses', () => {
|
||||||
|
it('should return the amount of the currently selected processes for deletion', () => {
|
||||||
|
service.toggleDelete('test-id-1');
|
||||||
|
service.toggleDelete('test-id-2');
|
||||||
|
|
||||||
|
expect(service.getAmountOfSelectedProcesses()).toEqual(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('isProcessing$', () => {
|
||||||
|
it('should return a behavior subject containing whether a delete is currently processing or not', () => {
|
||||||
|
const result = service.isProcessing$();
|
||||||
|
expect(result.getValue()).toBeFalse();
|
||||||
|
|
||||||
|
result.next(true);
|
||||||
|
expect(result.getValue()).toBeTrue();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('hasSelected', () => {
|
||||||
|
it('should return if the list of selected processes has values', () => {
|
||||||
|
expect(service.hasSelected()).toBeFalse();
|
||||||
|
|
||||||
|
service.toggleDelete('test-id-1');
|
||||||
|
service.toggleDelete('test-id-2');
|
||||||
|
|
||||||
|
expect(service.hasSelected()).toBeTrue();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('deleteSelectedProcesses', () => {
|
||||||
|
it('should delete all selected processes, show an error for each failed one and a notification at the end with the amount of succeeded deletions', () => {
|
||||||
|
(processDataService.delete as jasmine.Spy).and.callFake((processId: string) => {
|
||||||
|
if (processId.includes('error')) {
|
||||||
|
return createFailedRemoteDataObject$();
|
||||||
|
} else {
|
||||||
|
return createSuccessfulRemoteDataObject$(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
service.toggleDelete('test-id-1');
|
||||||
|
service.toggleDelete('test-id-2');
|
||||||
|
service.toggleDelete('error-id-3');
|
||||||
|
service.toggleDelete('test-id-4');
|
||||||
|
service.toggleDelete('error-id-5');
|
||||||
|
service.toggleDelete('error-id-6');
|
||||||
|
service.toggleDelete('test-id-7');
|
||||||
|
|
||||||
|
|
||||||
|
service.deleteSelectedProcesses();
|
||||||
|
|
||||||
|
expect(processDataService.delete).toHaveBeenCalledWith('test-id-1');
|
||||||
|
|
||||||
|
|
||||||
|
expect(processDataService.delete).toHaveBeenCalledWith('test-id-2');
|
||||||
|
|
||||||
|
|
||||||
|
expect(processDataService.delete).toHaveBeenCalledWith('error-id-3');
|
||||||
|
expect(notificationsService.error).toHaveBeenCalled();
|
||||||
|
expect(mockTranslateService.get).toHaveBeenCalledWith('process.bulk.delete.error.body', {processId: 'error-id-3'});
|
||||||
|
|
||||||
|
|
||||||
|
expect(processDataService.delete).toHaveBeenCalledWith('test-id-4');
|
||||||
|
|
||||||
|
|
||||||
|
expect(processDataService.delete).toHaveBeenCalledWith('error-id-5');
|
||||||
|
expect(notificationsService.error).toHaveBeenCalled();
|
||||||
|
expect(mockTranslateService.get).toHaveBeenCalledWith('process.bulk.delete.error.body', {processId: 'error-id-5'});
|
||||||
|
|
||||||
|
|
||||||
|
expect(processDataService.delete).toHaveBeenCalledWith('error-id-6');
|
||||||
|
expect(notificationsService.error).toHaveBeenCalled();
|
||||||
|
expect(mockTranslateService.get).toHaveBeenCalledWith('process.bulk.delete.error.body', {processId: 'error-id-6'});
|
||||||
|
|
||||||
|
|
||||||
|
expect(processDataService.delete).toHaveBeenCalledWith('test-id-7');
|
||||||
|
|
||||||
|
expect(notificationsService.success).toHaveBeenCalled();
|
||||||
|
expect(mockTranslateService.get).toHaveBeenCalledWith('process.bulk.delete.success', {count: 4});
|
||||||
|
|
||||||
|
expect(service.processesToDelete).toEqual(['error-id-3', 'error-id-5', 'error-id-6']);
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
118
src/app/process-page/overview/process-bulk-delete.service.ts
Normal file
118
src/app/process-page/overview/process-bulk-delete.service.ts
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import { Process } from '../processes/process.model';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ProcessDataService } from '../../core/data/processes/process-data.service';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
|
import { BehaviorSubject, count, from } from 'rxjs';
|
||||||
|
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
|
||||||
|
import { concatMap, filter, tap } from 'rxjs/operators';
|
||||||
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Service to facilitate removing processes in bulk.
|
||||||
|
*/
|
||||||
|
export class ProcessBulkDeleteService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Array to track the processes to be deleted
|
||||||
|
*/
|
||||||
|
processesToDelete: string[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Behavior subject to track whether the delete is processing
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected isProcessingBehaviorSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected processDataService: ProcessDataService,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected translateService: TranslateService
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add or remove a process id to/from the list
|
||||||
|
* If the id is already present it will be removed, otherwise it will be added.
|
||||||
|
*
|
||||||
|
* @param processId - The process id to add or remove
|
||||||
|
*/
|
||||||
|
toggleDelete(processId: string) {
|
||||||
|
if (this.isToBeDeleted(processId)) {
|
||||||
|
this.processesToDelete.splice(this.processesToDelete.indexOf(processId), 1);
|
||||||
|
} else {
|
||||||
|
this.processesToDelete.push(processId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the provided process id is present in the to be deleted list
|
||||||
|
* @param processId
|
||||||
|
*/
|
||||||
|
isToBeDeleted(processId: string) {
|
||||||
|
return this.processesToDelete.includes(processId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the list of processes to be deleted
|
||||||
|
*/
|
||||||
|
clearAllProcesses() {
|
||||||
|
this.processesToDelete.splice(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the amount of processes selected for deletion
|
||||||
|
*/
|
||||||
|
getAmountOfSelectedProcesses() {
|
||||||
|
return this.processesToDelete.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a behavior subject to indicate whether the bulk delete is processing
|
||||||
|
*/
|
||||||
|
isProcessing$() {
|
||||||
|
return this.isProcessingBehaviorSubject;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether there currently are values selected for deletion
|
||||||
|
*/
|
||||||
|
hasSelected(): boolean {
|
||||||
|
return isNotEmpty(this.processesToDelete);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete all selected processes one by one
|
||||||
|
* When the deletion for a process fails, an error notification will be shown with the process id,
|
||||||
|
* but it will continue deleting the other processes.
|
||||||
|
* At the end it will show a notification stating the amount of successful deletes
|
||||||
|
* The successfully deleted processes will be removed from the list of selected values, the failed ones will be retained.
|
||||||
|
*/
|
||||||
|
deleteSelectedProcesses() {
|
||||||
|
this.isProcessingBehaviorSubject.next(true);
|
||||||
|
|
||||||
|
from([...this.processesToDelete]).pipe(
|
||||||
|
concatMap((processId) => {
|
||||||
|
return this.processDataService.delete(processId).pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
tap((rd: RemoteData<Process>) => {
|
||||||
|
if (rd.hasFailed) {
|
||||||
|
this.notificationsService.error(this.translateService.get('process.bulk.delete.error.head'), this.translateService.get('process.bulk.delete.error.body', {processId: processId}));
|
||||||
|
} else {
|
||||||
|
this.toggleDelete(processId);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
filter((rd: RemoteData<Process>) => rd.hasSucceeded),
|
||||||
|
count(),
|
||||||
|
).subscribe((value) => {
|
||||||
|
this.notificationsService.success(this.translateService.get('process.bulk.delete.success', {count: value}));
|
||||||
|
this.isProcessingBehaviorSubject.next(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@@ -1,7 +1,19 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<h2 class="flex-grow-1">{{'process.overview.title' | translate}}</h2>
|
<h2 class="flex-grow-1">{{'process.overview.title' | translate}}</h2>
|
||||||
<button class="btn btn-lg btn-success " routerLink="/processes/new"><i class="fas fa-plus pr-2"></i>{{'process.overview.new' | translate}}</button>
|
</div>
|
||||||
|
<div class="d-flex justify-content-end">
|
||||||
|
<button *ngIf="processBulkDeleteService.hasSelected()" class="btn btn-primary mr-2"
|
||||||
|
(click)="processBulkDeleteService.clearAllProcesses()"><i
|
||||||
|
class="fas fa-undo pr-2"></i>{{'process.overview.delete.clear' | translate }}
|
||||||
|
</button>
|
||||||
|
<button *ngIf="processBulkDeleteService.hasSelected()" class="btn btn-danger mr-2"
|
||||||
|
(click)="openDeleteModal(deleteModal)"><i
|
||||||
|
class="fas fa-trash pr-2"></i>{{'process.overview.delete' | translate: {count: processBulkDeleteService.getAmountOfSelectedProcesses()} }}
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-success" routerLink="/processes/new"><i
|
||||||
|
class="fas fa-plus pr-2"></i>{{'process.overview.new' | translate}}</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<ds-pagination *ngIf="(processesRD$ | async)?.payload?.totalElements > 0"
|
<ds-pagination *ngIf="(processesRD$ | async)?.payload?.totalElements > 0"
|
||||||
[paginationOptions]="pageConfig"
|
[paginationOptions]="pageConfig"
|
||||||
@@ -19,19 +31,61 @@
|
|||||||
<th scope="col">{{'process.overview.table.start' | translate}}</th>
|
<th scope="col">{{'process.overview.table.start' | translate}}</th>
|
||||||
<th scope="col">{{'process.overview.table.finish' | translate}}</th>
|
<th scope="col">{{'process.overview.table.finish' | translate}}</th>
|
||||||
<th scope="col">{{'process.overview.table.status' | translate}}</th>
|
<th scope="col">{{'process.overview.table.status' | translate}}</th>
|
||||||
|
<th scope="col">{{'process.overview.table.actions' | translate}}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let process of (processesRD$ | async)?.payload?.page">
|
<tr *ngFor="let process of (processesRD$ | async)?.payload?.page"
|
||||||
|
[class.table-danger]="processBulkDeleteService.isToBeDeleted(process.processId)">
|
||||||
<td><a [routerLink]="['/processes/', process.processId]">{{process.processId}}</a></td>
|
<td><a [routerLink]="['/processes/', process.processId]">{{process.processId}}</a></td>
|
||||||
<td><a [routerLink]="['/processes/', process.processId]">{{process.scriptName}}</a></td>
|
<td><a [routerLink]="['/processes/', process.processId]">{{process.scriptName}}</a></td>
|
||||||
<td *ngVar="(getEpersonName(process.userId) | async) as ePersonName">{{ePersonName}}</td>
|
<td *ngVar="(getEpersonName(process.userId) | async) as ePersonName">{{ePersonName}}</td>
|
||||||
<td>{{process.startTime | date:dateFormat:'UTC'}}</td>
|
<td>{{process.startTime | date:dateFormat:'UTC'}}</td>
|
||||||
<td>{{process.endTime | date:dateFormat:'UTC'}}</td>
|
<td>{{process.endTime | date:dateFormat:'UTC'}}</td>
|
||||||
<td>{{process.processStatus}}</td>
|
<td>{{process.processStatus}}</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-outline-danger"
|
||||||
|
(click)="processBulkDeleteService.toggleDelete(process.processId)"><i
|
||||||
|
class="fas fa-trash"></i></button>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</ds-pagination>
|
</ds-pagination>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ng-template #deleteModal>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<div class="modal-header">
|
||||||
|
<div>
|
||||||
|
<h4>{{'process.overview.delete.header' | translate }}</h4>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="close"
|
||||||
|
(click)="closeModal()" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<div *ngIf="!(processBulkDeleteService.isProcessing$() |async)">{{'process.overview.delete.body' | translate: {count: processBulkDeleteService.getAmountOfSelectedProcesses()} }}</div>
|
||||||
|
<div *ngIf="processBulkDeleteService.isProcessing$() |async" class="alert alert-info">
|
||||||
|
<span class="spinner-border spinner-border-sm spinner-button" role="status" aria-hidden="true"></span>
|
||||||
|
<span> {{ 'process.overview.delete.processing' | translate: {count: processBulkDeleteService.getAmountOfSelectedProcesses()} }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4">
|
||||||
|
<button class="btn btn-primary mr-2" [disabled]="processBulkDeleteService.isProcessing$() |async"
|
||||||
|
(click)="closeModal()">{{'process.detail.delete.cancel' | translate}}</button>
|
||||||
|
<button id="delete-confirm" class="btn btn-danger"
|
||||||
|
[disabled]="processBulkDeleteService.isProcessing$() |async"
|
||||||
|
(click)="deleteSelected()">{{ 'process.overview.delete' | translate: {count: processBulkDeleteService.getAmountOfSelectedProcesses()} }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { ProcessOverviewComponent } from './process-overview.component';
|
import { ProcessOverviewComponent } from './process-overview.component';
|
||||||
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
import { VarDirective } from '../../shared/utils/var.directive';
|
import { VarDirective } from '../../shared/utils/var.directive';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { RouterTestingModule } from '@angular/router/testing';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
@@ -13,11 +13,11 @@ import { ProcessStatus } from '../processes/process-status.model';
|
|||||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
import { createPaginatedList } from '../../shared/testing/utils.test';
|
import { createPaginatedList } from '../../shared/testing/utils.test';
|
||||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
|
||||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
|
||||||
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
|
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
|
||||||
import { FindListOptions } from '../../core/data/find-list-options.model';
|
|
||||||
import { DatePipe } from '@angular/common';
|
import { DatePipe } from '@angular/common';
|
||||||
|
import { BehaviorSubject } from 'rxjs';
|
||||||
|
import { ProcessBulkDeleteService } from './process-bulk-delete.service';
|
||||||
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
|
||||||
describe('ProcessOverviewComponent', () => {
|
describe('ProcessOverviewComponent', () => {
|
||||||
let component: ProcessOverviewComponent;
|
let component: ProcessOverviewComponent;
|
||||||
@@ -30,6 +30,9 @@ describe('ProcessOverviewComponent', () => {
|
|||||||
let processes: Process[];
|
let processes: Process[];
|
||||||
let ePerson: EPerson;
|
let ePerson: EPerson;
|
||||||
|
|
||||||
|
let processBulkDeleteService;
|
||||||
|
let modalService;
|
||||||
|
|
||||||
const pipe = new DatePipe('en-US');
|
const pipe = new DatePipe('en-US');
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
@@ -80,6 +83,29 @@ describe('ProcessOverviewComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
paginationService = new PaginationServiceStub();
|
paginationService = new PaginationServiceStub();
|
||||||
|
|
||||||
|
processBulkDeleteService = jasmine.createSpyObj('processBulkDeleteService', {
|
||||||
|
clearAllProcesses: {},
|
||||||
|
deleteSelectedProcesses: {},
|
||||||
|
isProcessing$: new BehaviorSubject(false),
|
||||||
|
hasSelected: true,
|
||||||
|
isToBeDeleted: true,
|
||||||
|
toggleDelete: {},
|
||||||
|
getAmountOfSelectedProcesses: 5
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
(processBulkDeleteService.isToBeDeleted as jasmine.Spy).and.callFake((id) => {
|
||||||
|
if (id === 2) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
modalService = jasmine.createSpyObj('modalService', {
|
||||||
|
open: {}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
@@ -90,7 +116,9 @@ describe('ProcessOverviewComponent', () => {
|
|||||||
providers: [
|
providers: [
|
||||||
{ provide: ProcessDataService, useValue: processService },
|
{ provide: ProcessDataService, useValue: processService },
|
||||||
{ provide: EPersonDataService, useValue: ePersonService },
|
{ provide: EPersonDataService, useValue: ePersonService },
|
||||||
{ provide: PaginationService, useValue: paginationService }
|
{ provide: PaginationService, useValue: paginationService },
|
||||||
|
{ provide: ProcessBulkDeleteService, useValue: processBulkDeleteService },
|
||||||
|
{ provide: NgbModal, useValue: modalService },
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
@@ -154,5 +182,71 @@ describe('ProcessOverviewComponent', () => {
|
|||||||
expect(el.textContent).toContain(processes[index].processStatus);
|
expect(el.textContent).toContain(processes[index].processStatus);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
it('should display a delete button in the seventh column', () => {
|
||||||
|
rowElements.forEach((rowElement, index) => {
|
||||||
|
const el = rowElement.query(By.css('td:nth-child(7)'));
|
||||||
|
expect(el.nativeElement.innerHTML).toContain('fas fa-trash');
|
||||||
|
|
||||||
|
el.query(By.css('button')).triggerEventHandler('click', null);
|
||||||
|
expect(processBulkDeleteService.toggleDelete).toHaveBeenCalledWith(processes[index].processId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should indicate a row that has been selected for deletion', () => {
|
||||||
|
const deleteRow = fixture.debugElement.query(By.css('.table-danger'));
|
||||||
|
expect(deleteRow.nativeElement.innerHTML).toContain('/processes/' + processes[1].processId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('overview buttons', () => {
|
||||||
|
it('should show a button to clear selected processes when there are selected processes', () => {
|
||||||
|
const clearButton = fixture.debugElement.query(By.css('.btn-primary'));
|
||||||
|
expect(clearButton.nativeElement.innerHTML).toContain('process.overview.delete.clear');
|
||||||
|
|
||||||
|
clearButton.triggerEventHandler('click', null);
|
||||||
|
expect(processBulkDeleteService.clearAllProcesses).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
it('should not show a button to clear selected processes when there are no selected processes', () => {
|
||||||
|
(processBulkDeleteService.hasSelected as jasmine.Spy).and.returnValue(false);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const clearButton = fixture.debugElement.query(By.css('.btn-primary'));
|
||||||
|
expect(clearButton).toBeNull();
|
||||||
|
});
|
||||||
|
it('should show a button to open the delete modal when there are selected processes', () => {
|
||||||
|
spyOn(component, 'openDeleteModal');
|
||||||
|
|
||||||
|
const deleteButton = fixture.debugElement.query(By.css('.btn-danger'));
|
||||||
|
expect(deleteButton.nativeElement.innerHTML).toContain('process.overview.delete');
|
||||||
|
|
||||||
|
deleteButton.triggerEventHandler('click', null);
|
||||||
|
expect(component.openDeleteModal).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
it('should not show a button to clear selected processes when there are no selected processes', () => {
|
||||||
|
(processBulkDeleteService.hasSelected as jasmine.Spy).and.returnValue(false);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const deleteButton = fixture.debugElement.query(By.css('.btn-danger'));
|
||||||
|
expect(deleteButton).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('openDeleteModal', () => {
|
||||||
|
it('should open the modal', () => {
|
||||||
|
component.openDeleteModal({});
|
||||||
|
expect(modalService.open).toHaveBeenCalledWith({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deleteSelected', () => {
|
||||||
|
it('should call the deleteSelectedProcesses method on the processBulkDeleteService and close the modal when processing is done', () => {
|
||||||
|
spyOn(component, 'closeModal');
|
||||||
|
spyOn(component, 'setProcesses');
|
||||||
|
|
||||||
|
component.deleteSelected();
|
||||||
|
|
||||||
|
expect(processBulkDeleteService.deleteSelectedProcesses).toHaveBeenCalled();
|
||||||
|
expect(component.closeModal).toHaveBeenCalled();
|
||||||
|
expect(component.setProcesses).toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable, Subscription } from 'rxjs';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
import { PaginatedList } from '../../core/data/paginated-list.model';
|
import { PaginatedList } from '../../core/data/paginated-list.model';
|
||||||
import { Process } from '../processes/process.model';
|
import { Process } from '../processes/process.model';
|
||||||
@@ -11,6 +11,9 @@ import { map, switchMap } from 'rxjs/operators';
|
|||||||
import { ProcessDataService } from '../../core/data/processes/process-data.service';
|
import { ProcessDataService } from '../../core/data/processes/process-data.service';
|
||||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||||
import { FindListOptions } from '../../core/data/find-list-options.model';
|
import { FindListOptions } from '../../core/data/find-list-options.model';
|
||||||
|
import { ProcessBulkDeleteService } from './process-bulk-delete.service';
|
||||||
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { hasValue } from '../../shared/empty.util';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-process-overview',
|
selector: 'ds-process-overview',
|
||||||
@@ -19,7 +22,7 @@ import { FindListOptions } from '../../core/data/find-list-options.model';
|
|||||||
/**
|
/**
|
||||||
* Component displaying a list of all processes in a paginated table
|
* Component displaying a list of all processes in a paginated table
|
||||||
*/
|
*/
|
||||||
export class ProcessOverviewComponent implements OnInit {
|
export class ProcessOverviewComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of all processes
|
* List of all processes
|
||||||
@@ -46,13 +49,22 @@ export class ProcessOverviewComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
dateFormat = 'yyyy-MM-dd HH:mm:ss';
|
dateFormat = 'yyyy-MM-dd HH:mm:ss';
|
||||||
|
|
||||||
|
processesToDelete: string[] = [];
|
||||||
|
private modalRef: any;
|
||||||
|
|
||||||
|
isProcessingSub: Subscription;
|
||||||
|
|
||||||
constructor(protected processService: ProcessDataService,
|
constructor(protected processService: ProcessDataService,
|
||||||
protected paginationService: PaginationService,
|
protected paginationService: PaginationService,
|
||||||
protected ePersonService: EPersonDataService) {
|
protected ePersonService: EPersonDataService,
|
||||||
|
protected modalService: NgbModal,
|
||||||
|
public processBulkDeleteService: ProcessBulkDeleteService,
|
||||||
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.setProcesses();
|
this.setProcesses();
|
||||||
|
this.processBulkDeleteService.clearAllProcesses();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -60,7 +72,7 @@ export class ProcessOverviewComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
setProcesses() {
|
setProcesses() {
|
||||||
this.processesRD$ = this.paginationService.getFindListOptions(this.pageConfig.id, this.config).pipe(
|
this.processesRD$ = this.paginationService.getFindListOptions(this.pageConfig.id, this.config).pipe(
|
||||||
switchMap((config) => this.processService.findAll(config))
|
switchMap((config) => this.processService.findAll(config, true, false))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,8 +86,46 @@ export class ProcessOverviewComponent implements OnInit {
|
|||||||
map((eperson: EPerson) => eperson.name)
|
map((eperson: EPerson) => eperson.name)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.paginationService.clearPagination(this.pageConfig.id);
|
this.paginationService.clearPagination(this.pageConfig.id);
|
||||||
|
if (hasValue(this.isProcessingSub)) {
|
||||||
|
this.isProcessingSub.unsubscribe();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open a given modal.
|
||||||
|
* @param content - the modal content.
|
||||||
|
*/
|
||||||
|
openDeleteModal(content) {
|
||||||
|
this.modalRef = this.modalService.open(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the modal.
|
||||||
|
*/
|
||||||
|
closeModal() {
|
||||||
|
this.modalRef.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete the previously selected processes using the processBulkDeleteService
|
||||||
|
* After the deletion has started, subscribe to the isProcessing$ and when it is set
|
||||||
|
* to false after the processing is done, close the modal and reinitialise the processes
|
||||||
|
*/
|
||||||
|
deleteSelected() {
|
||||||
|
this.processBulkDeleteService.deleteSelectedProcesses();
|
||||||
|
|
||||||
|
if (hasValue(this.isProcessingSub)) {
|
||||||
|
this.isProcessingSub.unsubscribe();
|
||||||
|
}
|
||||||
|
this.isProcessingSub = this.processBulkDeleteService.isProcessing$()
|
||||||
|
.subscribe((isProcessing) => {
|
||||||
|
if (!isProcessing) {
|
||||||
|
this.closeModal();
|
||||||
|
this.setProcesses();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -2983,6 +2983,22 @@
|
|||||||
|
|
||||||
"process.detail.create" : "Create similar process",
|
"process.detail.create" : "Create similar process",
|
||||||
|
|
||||||
|
"process.detail.actions": "Actions",
|
||||||
|
|
||||||
|
"process.detail.delete.button": "Delete process",
|
||||||
|
|
||||||
|
"process.detail.delete.header": "Delete process",
|
||||||
|
|
||||||
|
"process.detail.delete.body": "Are you sure you want to delete the current process?",
|
||||||
|
|
||||||
|
"process.detail.delete.cancel": "Cancel",
|
||||||
|
|
||||||
|
"process.detail.delete.confirm": "Delete process",
|
||||||
|
|
||||||
|
"process.detail.delete.success": "The process was successfully deleted.",
|
||||||
|
|
||||||
|
"process.detail.delete.error": "Something went wrong when deleting the process",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"process.overview.table.finish" : "Finish time (UTC)",
|
"process.overview.table.finish" : "Finish time (UTC)",
|
||||||
@@ -3003,6 +3019,25 @@
|
|||||||
|
|
||||||
"process.overview.new": "New",
|
"process.overview.new": "New",
|
||||||
|
|
||||||
|
"process.overview.table.actions": "Actions",
|
||||||
|
|
||||||
|
"process.overview.delete": "Delete {{count}} processes",
|
||||||
|
|
||||||
|
"process.overview.delete.clear": "Clear delete selection",
|
||||||
|
|
||||||
|
"process.overview.delete.processing": "{{count}} process(es) are being deleted. Please wait for the deletion to fully complete. Note that this can take a while.",
|
||||||
|
|
||||||
|
"process.overview.delete.body": "Are you sure you want to delete {{count}} process(es)?",
|
||||||
|
|
||||||
|
"process.overview.delete.header": "Delete processes",
|
||||||
|
|
||||||
|
"process.bulk.delete.error.head": "Error on deleteing process",
|
||||||
|
|
||||||
|
"process.bulk.delete.error.body": "The process with ID {{processId}} could not be deleted. The remaining processes will continue being deleted. ",
|
||||||
|
|
||||||
|
"process.bulk.delete.success": "{{count}} process(es) have been succesfully deleted",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"profile.breadcrumbs": "Update Profile",
|
"profile.breadcrumbs": "Update Profile",
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user