mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 18:14:17 +00:00
Merge branch 'w2p-111638_Process-admin-UI-redesign_Overview-page-tables_PR-feedback' into process-admin-ui-redesign-8.0.0-next
This commit is contained in:
@@ -7,8 +7,7 @@ import { ControlContainer, NgForm } from '@angular/forms';
|
|||||||
import { ScriptParameter } from '../scripts/script-parameter.model';
|
import { ScriptParameter } from '../scripts/script-parameter.model';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { RequestService } from '../../core/data/request.service';
|
import { Router, NavigationExtras } from '@angular/router';
|
||||||
import { Router } from '@angular/router';
|
|
||||||
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
|
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
import { getProcessListRoute } from '../process-page-routing.paths';
|
import { getProcessListRoute } from '../process-page-routing.paths';
|
||||||
@@ -57,7 +56,6 @@ export class ProcessFormComponent implements OnInit {
|
|||||||
private scriptService: ScriptDataService,
|
private scriptService: ScriptDataService,
|
||||||
private notificationsService: NotificationsService,
|
private notificationsService: NotificationsService,
|
||||||
private translationService: TranslateService,
|
private translationService: TranslateService,
|
||||||
private requestService: RequestService,
|
|
||||||
private router: Router) {
|
private router: Router) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,7 +89,7 @@ export class ProcessFormComponent implements OnInit {
|
|||||||
const title = this.translationService.get('process.new.notification.success.title');
|
const title = this.translationService.get('process.new.notification.success.title');
|
||||||
const content = this.translationService.get('process.new.notification.success.content');
|
const content = this.translationService.get('process.new.notification.success.content');
|
||||||
this.notificationsService.success(title, content);
|
this.notificationsService.success(title, content);
|
||||||
this.sendBack();
|
this.sendBack(rd.payload);
|
||||||
} else {
|
} else {
|
||||||
const title = this.translationService.get('process.new.notification.error.title');
|
const title = this.translationService.get('process.new.notification.error.title');
|
||||||
const content = this.translationService.get('process.new.notification.error.content');
|
const content = this.translationService.get('process.new.notification.error.content');
|
||||||
@@ -143,11 +141,17 @@ export class ProcessFormComponent implements OnInit {
|
|||||||
return this.missingParameters.length > 0;
|
return this.missingParameters.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private sendBack() {
|
/**
|
||||||
this.requestService.removeByHrefSubstring('/processes');
|
* Redirect the user to the processes overview page with the new process' ID,
|
||||||
/* should subscribe on the previous method to know the action is finished and then navigate,
|
* so it can be highlighted in the overview table.
|
||||||
will fix this when the removeByHrefSubstring changes are merged */
|
* @param newProcess The newly created process
|
||||||
this.router.navigateByUrl(getProcessListRoute());
|
* @private
|
||||||
|
*/
|
||||||
|
private sendBack(newProcess: Process) {
|
||||||
|
const extras: NavigationExtras = {
|
||||||
|
queryParams: { new_process_id: newProcess.processId },
|
||||||
|
};
|
||||||
|
void this.router.navigate([getProcessListRoute()], extras);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -2,6 +2,33 @@
|
|||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<h1 class="flex-grow-1">{{'process.overview.title' | translate}}</h1>
|
<h1 class="flex-grow-1">{{'process.overview.title' | translate}}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
<ng-container *ngTemplateOutlet="buttons"></ng-container>
|
||||||
|
|
||||||
|
<div class="sections">
|
||||||
|
<ds-process-overview-table
|
||||||
|
[processStatus]="ProcessStatus.RUNNING"
|
||||||
|
[useAutoRefreshingSearchBy]="true"
|
||||||
|
[getInfoValueMethod]="processOverviewService.timeStarted"/>
|
||||||
|
<ds-process-overview-table
|
||||||
|
[processStatus]="ProcessStatus.SCHEDULED"
|
||||||
|
[useAutoRefreshingSearchBy]="true"
|
||||||
|
[getInfoValueMethod]="processOverviewService.timeCreated"/>
|
||||||
|
<ds-process-overview-table
|
||||||
|
[processStatus]="ProcessStatus.COMPLETED"
|
||||||
|
[sortField]="ProcessSortField.endTime"
|
||||||
|
[useAutoRefreshingSearchBy]="true"
|
||||||
|
[getInfoValueMethod]="processOverviewService.timeCompleted"/>
|
||||||
|
<ds-process-overview-table
|
||||||
|
[processStatus]="ProcessStatus.FAILED"
|
||||||
|
[sortField]="ProcessSortField.endTime"
|
||||||
|
[useAutoRefreshingSearchBy]="true"
|
||||||
|
[getInfoValueMethod]="processOverviewService.timeCompleted"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ng-container *ngTemplateOutlet="buttons"></ng-container>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ng-template #buttons>
|
||||||
<div class="d-flex justify-content-end mb-2">
|
<div class="d-flex justify-content-end mb-2">
|
||||||
<button *ngIf="processBulkDeleteService.hasSelected()" class="btn btn-primary mr-2"
|
<button *ngIf="processBulkDeleteService.hasSelected()" class="btn btn-primary mr-2"
|
||||||
(click)="processBulkDeleteService.clearAllProcesses()"><i
|
(click)="processBulkDeleteService.clearAllProcesses()"><i
|
||||||
@@ -14,26 +41,7 @@
|
|||||||
<button class="btn btn-success" routerLink="/processes/new"><i
|
<button class="btn btn-success" routerLink="/processes/new"><i
|
||||||
class="fas fa-plus pr-2"></i>{{'process.overview.new' | translate}}</button>
|
class="fas fa-plus pr-2"></i>{{'process.overview.new' | translate}}</button>
|
||||||
</div>
|
</div>
|
||||||
|
</ng-template>
|
||||||
<div class="sections">
|
|
||||||
<ds-process-overview-table
|
|
||||||
[processStatus]="ProcessStatus.RUNNING"
|
|
||||||
[useAutoRefreshingSearchBy]="true"
|
|
||||||
[getInfoValueMethod]="processOverviewService.timeStarted"/>
|
|
||||||
<ds-process-overview-table
|
|
||||||
[processStatus]="ProcessStatus.SCHEDULED"
|
|
||||||
[useAutoRefreshingSearchBy]="true"
|
|
||||||
[getInfoValueMethod]="processOverviewService.timeStarted"/>
|
|
||||||
<ds-process-overview-table
|
|
||||||
[processStatus]="ProcessStatus.COMPLETED"
|
|
||||||
[useAutoRefreshingSearchBy]="true"
|
|
||||||
[getInfoValueMethod]="processOverviewService.timeCompleted"/>
|
|
||||||
<ds-process-overview-table
|
|
||||||
[processStatus]="ProcessStatus.FAILED"
|
|
||||||
[useAutoRefreshingSearchBy]="true"
|
|
||||||
[getInfoValueMethod]="processOverviewService.timeCompleted"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ng-template #deleteModal>
|
<ng-template #deleteModal>
|
||||||
|
|
||||||
|
@@ -3,86 +3,27 @@ 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';
|
||||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
import { NO_ERRORS_SCHEMA, TemplateRef } from '@angular/core';
|
||||||
import { ProcessDataService } from '../../core/data/processes/process-data.service';
|
import { ProcessDataService } from '../../core/data/processes/process-data.service';
|
||||||
import { Process } from '../processes/process.model';
|
|
||||||
import { EPersonDataService } from '../../core/eperson/eperson-data.service';
|
|
||||||
import { EPerson } from '../../core/eperson/models/eperson.model';
|
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { ProcessStatus } from '../processes/process-status.model';
|
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
|
||||||
import { createPaginatedList } from '../../shared/testing/utils.test';
|
|
||||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
|
||||||
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
|
|
||||||
import { DatePipe } from '@angular/common';
|
|
||||||
import { BehaviorSubject } from 'rxjs';
|
import { BehaviorSubject } from 'rxjs';
|
||||||
import { ProcessBulkDeleteService } from './process-bulk-delete.service';
|
import { ProcessBulkDeleteService } from './process-bulk-delete.service';
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { ProcessOverviewService } from './process-overview.service';
|
||||||
|
|
||||||
describe('ProcessOverviewComponent', () => {
|
describe('ProcessOverviewComponent', () => {
|
||||||
let component: ProcessOverviewComponent;
|
let component: ProcessOverviewComponent;
|
||||||
let fixture: ComponentFixture<ProcessOverviewComponent>;
|
let fixture: ComponentFixture<ProcessOverviewComponent>;
|
||||||
|
|
||||||
let processService: ProcessDataService;
|
let processService: ProcessDataService;
|
||||||
let ePersonService: EPersonDataService;
|
|
||||||
let paginationService;
|
|
||||||
|
|
||||||
let processes: Process[];
|
|
||||||
let ePerson: EPerson;
|
|
||||||
|
|
||||||
let processBulkDeleteService;
|
let processBulkDeleteService;
|
||||||
let modalService;
|
let modalService;
|
||||||
|
|
||||||
const pipe = new DatePipe('en-US');
|
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
processes = [
|
processService = jasmine.createSpyObj('processOverviewService', {
|
||||||
Object.assign(new Process(), {
|
timeStarted: '2024-02-05 16:43:32',
|
||||||
processId: 1,
|
|
||||||
scriptName: 'script-name',
|
|
||||||
startTime: '2020-03-19 00:30:00',
|
|
||||||
endTime: '2020-03-19 23:30:00',
|
|
||||||
processStatus: ProcessStatus.COMPLETED
|
|
||||||
}),
|
|
||||||
Object.assign(new Process(), {
|
|
||||||
processId: 2,
|
|
||||||
scriptName: 'script-name',
|
|
||||||
startTime: '2020-03-20 00:30:00',
|
|
||||||
endTime: '2020-03-20 23:30:00',
|
|
||||||
processStatus: ProcessStatus.FAILED
|
|
||||||
}),
|
|
||||||
Object.assign(new Process(), {
|
|
||||||
processId: 3,
|
|
||||||
scriptName: 'another-script-name',
|
|
||||||
startTime: '2020-03-21 00:30:00',
|
|
||||||
endTime: '2020-03-21 23:30:00',
|
|
||||||
processStatus: ProcessStatus.RUNNING
|
|
||||||
})
|
|
||||||
];
|
|
||||||
ePerson = Object.assign(new EPerson(), {
|
|
||||||
metadata: {
|
|
||||||
'eperson.firstname': [
|
|
||||||
{
|
|
||||||
value: 'John',
|
|
||||||
language: null
|
|
||||||
}
|
|
||||||
],
|
|
||||||
'eperson.lastname': [
|
|
||||||
{
|
|
||||||
value: 'Doe',
|
|
||||||
language: null
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
processService = jasmine.createSpyObj('processService', {
|
|
||||||
findAll: createSuccessfulRemoteDataObject$(createPaginatedList(processes))
|
|
||||||
});
|
|
||||||
ePersonService = jasmine.createSpyObj('ePersonService', {
|
|
||||||
findById: createSuccessfulRemoteDataObject$(ePerson)
|
|
||||||
});
|
|
||||||
|
|
||||||
paginationService = new PaginationServiceStub();
|
|
||||||
|
|
||||||
processBulkDeleteService = jasmine.createSpyObj('processBulkDeleteService', {
|
processBulkDeleteService = jasmine.createSpyObj('processBulkDeleteService', {
|
||||||
clearAllProcesses: {},
|
clearAllProcesses: {},
|
||||||
@@ -96,11 +37,7 @@ describe('ProcessOverviewComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
(processBulkDeleteService.isToBeDeleted as jasmine.Spy).and.callFake((id) => {
|
(processBulkDeleteService.isToBeDeleted as jasmine.Spy).and.callFake((id) => {
|
||||||
if (id === 2) {
|
return id === 2;
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
modalService = jasmine.createSpyObj('modalService', {
|
modalService = jasmine.createSpyObj('modalService', {
|
||||||
@@ -114,9 +51,7 @@ describe('ProcessOverviewComponent', () => {
|
|||||||
declarations: [ProcessOverviewComponent, VarDirective],
|
declarations: [ProcessOverviewComponent, VarDirective],
|
||||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
|
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: ProcessDataService, useValue: processService },
|
{ provide: ProcessOverviewService, useValue: processService },
|
||||||
{ provide: EPersonDataService, useValue: ePersonService },
|
|
||||||
{ provide: PaginationService, useValue: paginationService },
|
|
||||||
{ provide: ProcessBulkDeleteService, useValue: processBulkDeleteService },
|
{ provide: ProcessBulkDeleteService, useValue: processBulkDeleteService },
|
||||||
{ provide: NgbModal, useValue: modalService },
|
{ provide: NgbModal, useValue: modalService },
|
||||||
],
|
],
|
||||||
@@ -165,7 +100,7 @@ describe('ProcessOverviewComponent', () => {
|
|||||||
|
|
||||||
describe('openDeleteModal', () => {
|
describe('openDeleteModal', () => {
|
||||||
it('should open the modal', () => {
|
it('should open the modal', () => {
|
||||||
component.openDeleteModal({});
|
component.openDeleteModal({} as TemplateRef<any>);
|
||||||
expect(modalService.open).toHaveBeenCalledWith({});
|
expect(modalService.open).toHaveBeenCalledWith({});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -173,13 +108,11 @@ describe('ProcessOverviewComponent', () => {
|
|||||||
describe('deleteSelected', () => {
|
describe('deleteSelected', () => {
|
||||||
it('should call the deleteSelectedProcesses method on the processBulkDeleteService and close the modal when processing is done', () => {
|
it('should call the deleteSelectedProcesses method on the processBulkDeleteService and close the modal when processing is done', () => {
|
||||||
spyOn(component, 'closeModal');
|
spyOn(component, 'closeModal');
|
||||||
spyOn(component, 'setProcesses');
|
|
||||||
|
|
||||||
component.deleteSelected();
|
component.deleteSelected();
|
||||||
|
|
||||||
expect(processBulkDeleteService.deleteSelectedProcesses).toHaveBeenCalled();
|
expect(processBulkDeleteService.deleteSelectedProcesses).toHaveBeenCalled();
|
||||||
expect(component.closeModal).toHaveBeenCalled();
|
expect(component.closeModal).toHaveBeenCalled();
|
||||||
expect(component.setProcesses).toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,19 +1,9 @@
|
|||||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
import { Component, OnDestroy, OnInit, TemplateRef } from '@angular/core';
|
||||||
import { Observable, Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
|
||||||
import { PaginatedList } from '../../core/data/paginated-list.model';
|
|
||||||
import { Process } from '../processes/process.model';
|
|
||||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
|
||||||
import { EPersonDataService } from '../../core/eperson/eperson-data.service';
|
|
||||||
import { switchMap } from 'rxjs/operators';
|
|
||||||
import { ProcessDataService } from '../../core/data/processes/process-data.service';
|
|
||||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
|
||||||
import { FindListOptions } from '../../core/data/find-list-options.model';
|
|
||||||
import { ProcessBulkDeleteService } from './process-bulk-delete.service';
|
import { ProcessBulkDeleteService } from './process-bulk-delete.service';
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { hasValue } from '../../shared/empty.util';
|
import { hasValue } from '../../shared/empty.util';
|
||||||
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
import { ProcessOverviewService, ProcessSortField } from './process-overview.service';
|
||||||
import { ProcessOverviewService } from './process-overview.service';
|
|
||||||
import { ProcessStatus } from '../processes/process-status.model';
|
import { ProcessStatus } from '../processes/process-status.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -25,64 +15,25 @@ import { ProcessStatus } from '../processes/process-status.model';
|
|||||||
*/
|
*/
|
||||||
export class ProcessOverviewComponent implements OnInit, OnDestroy {
|
export class ProcessOverviewComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
// Enums are redeclared here so they can be used in the template
|
||||||
protected readonly ProcessStatus = ProcessStatus;
|
protected readonly ProcessStatus = ProcessStatus;
|
||||||
|
protected readonly ProcessSortField = ProcessSortField;
|
||||||
|
|
||||||
/**
|
|
||||||
* List of all processes
|
|
||||||
*/
|
|
||||||
processesRD$: Observable<RemoteData<PaginatedList<Process>>>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The current pagination configuration for the page used by the FindAll method
|
|
||||||
*/
|
|
||||||
config: FindListOptions = Object.assign(new FindListOptions(), {
|
|
||||||
elementsPerPage: 20
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The current pagination configuration for the page
|
|
||||||
*/
|
|
||||||
pageConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
|
||||||
id: 'po',
|
|
||||||
pageSize: 20
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Date format to use for start and end time of processes
|
|
||||||
*/
|
|
||||||
dateFormat = 'yyyy-MM-dd HH:mm:ss';
|
|
||||||
|
|
||||||
processesToDelete: string[] = [];
|
|
||||||
private modalRef: any;
|
private modalRef: any;
|
||||||
|
|
||||||
isProcessingSub: Subscription;
|
isProcessingSub: Subscription;
|
||||||
|
|
||||||
constructor(protected processService: ProcessDataService,
|
constructor(protected processOverviewService: ProcessOverviewService,
|
||||||
protected processOverviewService: ProcessOverviewService,
|
|
||||||
protected paginationService: PaginationService,
|
|
||||||
protected ePersonService: EPersonDataService,
|
|
||||||
protected modalService: NgbModal,
|
protected modalService: NgbModal,
|
||||||
public processBulkDeleteService: ProcessBulkDeleteService,
|
public processBulkDeleteService: ProcessBulkDeleteService,
|
||||||
protected dsoNameService: DSONameService,
|
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.setProcesses();
|
|
||||||
this.processBulkDeleteService.clearAllProcesses();
|
this.processBulkDeleteService.clearAllProcesses();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Send a request to fetch all processes for the current page
|
|
||||||
*/
|
|
||||||
setProcesses() {
|
|
||||||
this.processesRD$ = this.paginationService.getFindListOptions(this.pageConfig.id, this.config).pipe(
|
|
||||||
switchMap((config) => this.processService.findAll(config, true, false))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.paginationService.clearPagination(this.pageConfig.id);
|
|
||||||
if (hasValue(this.isProcessingSub)) {
|
if (hasValue(this.isProcessingSub)) {
|
||||||
this.isProcessingSub.unsubscribe();
|
this.isProcessingSub.unsubscribe();
|
||||||
}
|
}
|
||||||
@@ -92,7 +43,7 @@ export class ProcessOverviewComponent implements OnInit, OnDestroy {
|
|||||||
* Open a given modal.
|
* Open a given modal.
|
||||||
* @param content - the modal content.
|
* @param content - the modal content.
|
||||||
*/
|
*/
|
||||||
openDeleteModal(content) {
|
openDeleteModal(content: TemplateRef<any>) {
|
||||||
this.modalRef = this.modalService.open(content);
|
this.modalRef = this.modalService.open(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,7 +69,6 @@ export class ProcessOverviewComponent implements OnInit, OnDestroy {
|
|||||||
.subscribe((isProcessing) => {
|
.subscribe((isProcessing) => {
|
||||||
if (!isProcessing) {
|
if (!isProcessing) {
|
||||||
this.closeModal();
|
this.closeModal();
|
||||||
this.setProcesses();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@@ -9,6 +9,19 @@ import { RequestParam } from '../../core/cache/models/request-param.model';
|
|||||||
import { ProcessStatus } from '../processes/process-status.model';
|
import { ProcessStatus } from '../processes/process-status.model';
|
||||||
import { DatePipe } from '@angular/common';
|
import { DatePipe } from '@angular/common';
|
||||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||||
|
import { SortOptions, SortDirection } from '../../core/cache/models/sort-options.model';
|
||||||
|
import { hasValue } from '../../shared/empty.util';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The sortable fields for processes
|
||||||
|
* See [the endpoint documentation]{@link https://github.com/DSpace/RestContract/blob/main/processes-endpoint.md#search-processes-by-property}
|
||||||
|
* for details.
|
||||||
|
*/
|
||||||
|
export enum ProcessSortField {
|
||||||
|
creationTime = 'creationTime',
|
||||||
|
startTime = 'startTime',
|
||||||
|
endTime = 'endTime',
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service to manage the processes displayed in the
|
* Service to manage the processes displayed in the
|
||||||
@@ -30,6 +43,7 @@ export class ProcessOverviewService {
|
|||||||
datePipe = new DatePipe('en-US');
|
datePipe = new DatePipe('en-US');
|
||||||
|
|
||||||
|
|
||||||
|
timeCreated = (process: Process) => this.datePipe.transform(process.creationTime, this.dateFormat, 'UTC');
|
||||||
timeCompleted = (process: Process) => this.datePipe.transform(process.endTime, this.dateFormat, 'UTC');
|
timeCompleted = (process: Process) => this.datePipe.transform(process.endTime, this.dateFormat, 'UTC');
|
||||||
timeStarted = (process: Process) => this.datePipe.transform(process.startTime, this.dateFormat, 'UTC');
|
timeStarted = (process: Process) => this.datePipe.transform(process.startTime, this.dateFormat, 'UTC');
|
||||||
|
|
||||||
@@ -47,7 +61,7 @@ export class ProcessOverviewService {
|
|||||||
elementsPerPage: 5,
|
elementsPerPage: 5,
|
||||||
}, findListOptions);
|
}, findListOptions);
|
||||||
|
|
||||||
if (autoRefreshingIntervalInMs !== null && autoRefreshingIntervalInMs > 0) {
|
if (hasValue(autoRefreshingIntervalInMs) && autoRefreshingIntervalInMs > 0) {
|
||||||
return this.processDataService.autoRefreshingSearchBy('byProperty', options, autoRefreshingIntervalInMs);
|
return this.processDataService.autoRefreshingSearchBy('byProperty', options, autoRefreshingIntervalInMs);
|
||||||
} else {
|
} else {
|
||||||
return this.processDataService.searchBy('byProperty', options);
|
return this.processDataService.searchBy('byProperty', options);
|
||||||
@@ -57,13 +71,16 @@ export class ProcessOverviewService {
|
|||||||
/**
|
/**
|
||||||
* Map the provided paginationOptions to FindListOptions
|
* Map the provided paginationOptions to FindListOptions
|
||||||
* @param paginationOptions the PaginationComponentOptions to map
|
* @param paginationOptions the PaginationComponentOptions to map
|
||||||
|
* @param sortField the field on which the processes are sorted
|
||||||
*/
|
*/
|
||||||
getFindListOptions(paginationOptions: PaginationComponentOptions): FindListOptions {
|
getFindListOptions(paginationOptions: PaginationComponentOptions, sortField: ProcessSortField): FindListOptions {
|
||||||
|
let sortOptions = new SortOptions(sortField, SortDirection.DESC);
|
||||||
return Object.assign(
|
return Object.assign(
|
||||||
new FindListOptions(),
|
new FindListOptions(),
|
||||||
{
|
{
|
||||||
currentPage: paginationOptions.currentPage,
|
currentPage: paginationOptions.currentPage,
|
||||||
elementsPerPage: paginationOptions.pageSize,
|
elementsPerPage: paginationOptions.pageSize,
|
||||||
|
sort: sortOptions,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -12,7 +12,7 @@
|
|||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ngbCollapse #collapse="ngbCollapse">
|
<div ngbCollapse #collapse="ngbCollapse" [ngbCollapse]="isCollapsed">
|
||||||
|
|
||||||
<ng-container *ngVar="(processesRD$ | async) as processesRD">
|
<ng-container *ngVar="(processesRD$ | async) as processesRD">
|
||||||
<ds-themed-loading *ngIf="!processesRD || processesRD.isLoading"/>
|
<ds-themed-loading *ngIf="!processesRD || processesRD.isLoading"/>
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
<table class="table table-striped table-hover">
|
<table class="table table-striped table-hover">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col" class="status-header">{{'process.overview.table.status' | translate}}</th>
|
<th scope="col" class="id-header">{{'process.overview.table.id' | translate}}</th>
|
||||||
<th scope="col" class="name-header">{{'process.overview.table.name' | translate}}</th>
|
<th scope="col" class="name-header">{{'process.overview.table.name' | translate}}</th>
|
||||||
<th scope="col" class="user-header">{{'process.overview.table.user' | translate}}</th>
|
<th scope="col" class="user-header">{{'process.overview.table.user' | translate}}</th>
|
||||||
<th scope="col" class="info-header">{{'process.overview.table.' + processStatus.toLowerCase() + '.info' | translate}}</th>
|
<th scope="col" class="info-header">{{'process.overview.table.' + processStatus.toLowerCase() + '.info' | translate}}</th>
|
||||||
@@ -37,13 +37,14 @@
|
|||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let tableEntry of processesRD?.payload?.page"
|
<tr *ngFor="let tableEntry of processesRD?.payload?.page"
|
||||||
[class.table-danger]="processBulkDeleteService.isToBeDeleted(tableEntry.process.processId)">
|
[class]="getRowClass(tableEntry.process)">
|
||||||
<td>{{tableEntry.process.processStatus}}</td>
|
<td><a [routerLink]="['/processes/', tableEntry.process.processId]">{{tableEntry.process.processId}}</a></td>
|
||||||
<td><a [routerLink]="['/processes/', tableEntry.process.processId]">{{tableEntry.process.scriptName}}</a></td>
|
<td><a [routerLink]="['/processes/', tableEntry.process.processId]">{{tableEntry.process.scriptName}}</a></td>
|
||||||
<td>{{tableEntry.user}}</td>
|
<td>{{tableEntry.user}}</td>
|
||||||
<td>{{tableEntry.info}}</td>
|
<td>{{tableEntry.info}}</td>
|
||||||
<td>
|
<td>
|
||||||
<button [attr.aria-label]="'process.overview.delete-process' | translate"
|
<button [attr.aria-label]="'process.overview.delete-process' | translate"
|
||||||
|
aria-hidden="true"
|
||||||
(click)="processBulkDeleteService.toggleDelete(tableEntry.process.processId)"
|
(click)="processBulkDeleteService.toggleDelete(tableEntry.process.processId)"
|
||||||
class="btn btn-outline-danger">
|
class="btn btn-outline-danger">
|
||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i>
|
||||||
|
@@ -7,8 +7,8 @@
|
|||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-header {
|
.id-header {
|
||||||
width: var(--ds-process-overview-table-status-column-width);
|
width: var(--ds-process-overview-table-id-column-width);
|
||||||
}
|
}
|
||||||
|
|
||||||
.name-header {
|
.name-header {
|
||||||
|
@@ -19,6 +19,8 @@ import { NO_ERRORS_SCHEMA } from '@angular/core';
|
|||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { AuthService } from '../../../core/auth/auth.service';
|
import { AuthService } from '../../../core/auth/auth.service';
|
||||||
import { AuthServiceMock } from '../../../shared/mocks/auth.service.mock';
|
import { AuthServiceMock } from '../../../shared/mocks/auth.service.mock';
|
||||||
|
import { RouteService } from '../../../core/services/route.service';
|
||||||
|
import { routeServiceStub } from '../../../shared/testing/route-service.stub';
|
||||||
|
|
||||||
|
|
||||||
describe('ProcessOverviewTableComponent', () => {
|
describe('ProcessOverviewTableComponent', () => {
|
||||||
@@ -31,6 +33,7 @@ describe('ProcessOverviewTableComponent', () => {
|
|||||||
let processBulkDeleteService: ProcessBulkDeleteService;
|
let processBulkDeleteService: ProcessBulkDeleteService;
|
||||||
let modalService: NgbModal;
|
let modalService: NgbModal;
|
||||||
let authService; // : AuthService; Not typed as the mock does not fully implement AuthService
|
let authService; // : AuthService; Not typed as the mock does not fully implement AuthService
|
||||||
|
let routeService: RouteService;
|
||||||
|
|
||||||
let processes: Process[];
|
let processes: Process[];
|
||||||
let ePerson: EPerson;
|
let ePerson: EPerson;
|
||||||
@@ -104,6 +107,7 @@ describe('ProcessOverviewTableComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
authService = new AuthServiceMock();
|
authService = new AuthServiceMock();
|
||||||
|
routeService = routeServiceStub;
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
@@ -119,6 +123,7 @@ describe('ProcessOverviewTableComponent', () => {
|
|||||||
{ provide: ProcessBulkDeleteService, useValue: processBulkDeleteService },
|
{ provide: ProcessBulkDeleteService, useValue: processBulkDeleteService },
|
||||||
{ provide: NgbModal, useValue: modalService },
|
{ provide: NgbModal, useValue: modalService },
|
||||||
{ provide: AuthService, useValue: authService },
|
{ provide: AuthService, useValue: authService },
|
||||||
|
{ provide: RouteService, useValue: routeService },
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
@@ -143,10 +148,10 @@ describe('ProcessOverviewTableComponent', () => {
|
|||||||
expect(rowElements.length).toEqual(3);
|
expect(rowElements.length).toEqual(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should display the process\' status in the first column', () => {
|
it('should display the process\' ID in the first column', () => {
|
||||||
rowElements.forEach((rowElement, index) => {
|
rowElements.forEach((rowElement, index) => {
|
||||||
const el = rowElement.query(By.css('td:nth-child(1)')).nativeElement;
|
const el = rowElement.query(By.css('td:nth-child(1)')).nativeElement;
|
||||||
expect(el.textContent).toContain(processes[index].processStatus);
|
expect(el.textContent).toContain(processes[index].processId);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -1,22 +1,30 @@
|
|||||||
import { Component, Input, OnInit } from '@angular/core';
|
import { Component, Input, OnInit, Inject, PLATFORM_ID } from '@angular/core';
|
||||||
import { ProcessStatus } from '../../processes/process-status.model';
|
import { ProcessStatus } from '../../processes/process-status.model';
|
||||||
import { Observable, mergeMap, from as observableFrom } from 'rxjs';
|
import { Observable, mergeMap, from as observableFrom } 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';
|
||||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||||
import { ProcessOverviewService } from '../process-overview.service';
|
import { ProcessOverviewService, ProcessSortField } from '../process-overview.service';
|
||||||
import { ProcessBulkDeleteService } from '../process-bulk-delete.service';
|
import { ProcessBulkDeleteService } from '../process-bulk-delete.service';
|
||||||
import { EPersonDataService } from '../../../core/eperson/eperson-data.service';
|
import { EPersonDataService } from '../../../core/eperson/eperson-data.service';
|
||||||
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
|
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
|
||||||
import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators';
|
import {
|
||||||
import { map, switchMap, toArray } from 'rxjs/operators';
|
getFirstSucceededRemoteDataPayload,
|
||||||
|
getFirstCompletedRemoteData,
|
||||||
|
getAllCompletedRemoteData
|
||||||
|
} from '../../../core/shared/operators';
|
||||||
|
import { map, switchMap, toArray, take } from 'rxjs/operators';
|
||||||
import { EPerson } from '../../../core/eperson/models/eperson.model';
|
import { EPerson } from '../../../core/eperson/models/eperson.model';
|
||||||
import { PaginationService } from 'src/app/core/pagination/pagination.service';
|
import { PaginationService } from 'src/app/core/pagination/pagination.service';
|
||||||
import { FindListOptions } from '../../../core/data/find-list-options.model';
|
import { FindListOptions } from '../../../core/data/find-list-options.model';
|
||||||
import { redirectOn4xx } from '../../../core/shared/authorized.operators';
|
import { redirectOn4xx } from '../../../core/shared/authorized.operators';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { AuthService } from '../../../core/auth/auth.service';
|
import { AuthService } from '../../../core/auth/auth.service';
|
||||||
|
import { isPlatformBrowser } from '@angular/common';
|
||||||
|
import { RouteService } from '../../../core/services/route.service';
|
||||||
|
|
||||||
|
const NEW_PROCESS_PARAM = 'new_process_id';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An interface to store a process and extra information related to the process
|
* An interface to store a process and extra information related to the process
|
||||||
@@ -40,6 +48,13 @@ export class ProcessOverviewTableComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
@Input() processStatus: ProcessStatus;
|
@Input() processStatus: ProcessStatus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The field on which the processes in this table are sorted
|
||||||
|
* {@link ProcessSortField.creationTime} by default as every single process has a creation time,
|
||||||
|
* but not every process has a start or end time
|
||||||
|
*/
|
||||||
|
@Input() sortField: ProcessSortField = ProcessSortField.creationTime;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether to use auto refresh for the processes shown in this table.
|
* Whether to use auto refresh for the processes shown in this table.
|
||||||
*/
|
*/
|
||||||
@@ -71,17 +86,38 @@ export class ProcessOverviewTableComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
paginationOptions$: Observable<PaginationComponentOptions>;
|
paginationOptions$: Observable<PaginationComponentOptions>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the table is collapsed
|
||||||
|
*/
|
||||||
|
isCollapsed = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The id of the process to highlight
|
||||||
|
*/
|
||||||
|
newProcessId: string;
|
||||||
|
|
||||||
constructor(protected processOverviewService: ProcessOverviewService,
|
constructor(protected processOverviewService: ProcessOverviewService,
|
||||||
protected processBulkDeleteService: ProcessBulkDeleteService,
|
protected processBulkDeleteService: ProcessBulkDeleteService,
|
||||||
protected ePersonDataService: EPersonDataService,
|
protected ePersonDataService: EPersonDataService,
|
||||||
protected dsoNameService: DSONameService,
|
protected dsoNameService: DSONameService,
|
||||||
protected paginationService: PaginationService,
|
protected paginationService: PaginationService,
|
||||||
|
protected routeService: RouteService,
|
||||||
protected router: Router,
|
protected router: Router,
|
||||||
protected auth: AuthService,
|
protected auth: AuthService,
|
||||||
|
@Inject(PLATFORM_ID) protected platformId: object,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
|
// Only auto refresh on browsers
|
||||||
|
if (!isPlatformBrowser(this.platformId)) {
|
||||||
|
this.useAutoRefreshingSearchBy = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.routeService.getQueryParameterValue(NEW_PROCESS_PARAM).pipe(take(1)).subscribe((id) => {
|
||||||
|
this.newProcessId = id;
|
||||||
|
});
|
||||||
|
|
||||||
// Creates an ID from the first 2 characters of the process status.
|
// Creates an ID from the first 2 characters of the process status.
|
||||||
// Should two process status values ever start with the same substring,
|
// Should two process status values ever start with the same substring,
|
||||||
// increase the number of characters until the ids are distinct.
|
// increase the number of characters until the ids are distinct.
|
||||||
@@ -113,7 +149,7 @@ export class ProcessOverviewTableComponent implements OnInit {
|
|||||||
.pipe(
|
.pipe(
|
||||||
// Map the paginationOptions to findListOptions
|
// Map the paginationOptions to findListOptions
|
||||||
map((paginationOptions: PaginationComponentOptions) =>
|
map((paginationOptions: PaginationComponentOptions) =>
|
||||||
this.processOverviewService.getFindListOptions(paginationOptions)),
|
this.processOverviewService.getFindListOptions(paginationOptions, this.sortField)),
|
||||||
// Use the findListOptions to retrieve the relevant processes every interval
|
// Use the findListOptions to retrieve the relevant processes every interval
|
||||||
switchMap((findListOptions: FindListOptions) =>
|
switchMap((findListOptions: FindListOptions) =>
|
||||||
this.processOverviewService.getProcessesByProcessStatus(
|
this.processOverviewService.getProcessesByProcessStatus(
|
||||||
@@ -121,6 +157,7 @@ export class ProcessOverviewTableComponent implements OnInit {
|
|||||||
),
|
),
|
||||||
// Redirect the user when he is logged out
|
// Redirect the user when he is logged out
|
||||||
redirectOn4xx(this.router, this.auth),
|
redirectOn4xx(this.router, this.auth),
|
||||||
|
getAllCompletedRemoteData(),
|
||||||
// Map RemoteData<PaginatedList<Process>> to RemoteData<PaginatedList<ProcessOverviewTableEntry>>
|
// Map RemoteData<PaginatedList<Process>> to RemoteData<PaginatedList<ProcessOverviewTableEntry>>
|
||||||
switchMap((processesRD: RemoteData<PaginatedList<Process>>) => {
|
switchMap((processesRD: RemoteData<PaginatedList<Process>>) => {
|
||||||
// Create observable emitting all processes one by one
|
// Create observable emitting all processes one by one
|
||||||
@@ -152,6 +189,15 @@ export class ProcessOverviewTableComponent implements OnInit {
|
|||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Collapse this section when the number of processes is zero the first time processes are retrieved
|
||||||
|
this.processesRD$.pipe(getFirstCompletedRemoteData()).subscribe(
|
||||||
|
(processesRD: RemoteData<PaginatedList<ProcessOverviewTableEntry>>) => {
|
||||||
|
if (!(processesRD.payload.totalElements > 0)) {
|
||||||
|
this.isCollapsed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -165,4 +211,18 @@ export class ProcessOverviewTableComponent implements OnInit {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the css class for a row depending on the state of the process
|
||||||
|
* @param process
|
||||||
|
*/
|
||||||
|
getRowClass(process: Process): string {
|
||||||
|
if (this.processBulkDeleteService.isToBeDeleted(process.processId)) {
|
||||||
|
return 'table-danger';
|
||||||
|
} else if (this.newProcessId === process.processId) {
|
||||||
|
return 'table-info';
|
||||||
|
} else {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -3,7 +3,7 @@ import { PROCESS_OUTPUT_TYPE } from '../../core/shared/process-output.resource-t
|
|||||||
import { ProcessStatus } from './process-status.model';
|
import { ProcessStatus } from './process-status.model';
|
||||||
import { ProcessParameter } from './process-parameter.model';
|
import { ProcessParameter } from './process-parameter.model';
|
||||||
import { HALLink } from '../../core/shared/hal-link.model';
|
import { HALLink } from '../../core/shared/hal-link.model';
|
||||||
import { autoserialize, deserialize } from 'cerialize';
|
import { autoserialize, deserialize, autoserializeAs } from 'cerialize';
|
||||||
import { PROCESS } from './process.resource-type';
|
import { PROCESS } from './process.resource-type';
|
||||||
import { excludeFromEquals } from '../../core/utilities/equals.decorators';
|
import { excludeFromEquals } from '../../core/utilities/equals.decorators';
|
||||||
import { ResourceType } from '../../core/shared/resource-type';
|
import { ResourceType } from '../../core/shared/resource-type';
|
||||||
@@ -35,7 +35,7 @@ export class Process implements CacheableObject {
|
|||||||
/**
|
/**
|
||||||
* The identifier for this process
|
* The identifier for this process
|
||||||
*/
|
*/
|
||||||
@autoserialize
|
@autoserializeAs(String)
|
||||||
processId: string;
|
processId: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -44,6 +44,12 @@ export class Process implements CacheableObject {
|
|||||||
@autoserialize
|
@autoserialize
|
||||||
userId: string;
|
userId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The creation time for this process
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
creationTime: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The start time for this process
|
* The start time for this process
|
||||||
*/
|
*/
|
||||||
|
@@ -3408,7 +3408,7 @@
|
|||||||
|
|
||||||
"process.overview.table.completed.info": "Finish time (UTC)",
|
"process.overview.table.completed.info": "Finish time (UTC)",
|
||||||
|
|
||||||
"process.overview.table.completed.title": "Completed processes",
|
"process.overview.table.completed.title": "Succeeded processes",
|
||||||
|
|
||||||
"process.overview.table.empty": "No matching processes found.",
|
"process.overview.table.empty": "No matching processes found.",
|
||||||
|
|
||||||
@@ -3426,7 +3426,7 @@
|
|||||||
|
|
||||||
"process.overview.table.running.title": "Running processes",
|
"process.overview.table.running.title": "Running processes",
|
||||||
|
|
||||||
"process.overview.table.scheduled.info": "Start time (UTC)",
|
"process.overview.table.scheduled.info": "Creation time (UTC)",
|
||||||
|
|
||||||
"process.overview.table.scheduled.title": "Scheduled processes",
|
"process.overview.table.scheduled.title": "Scheduled processes",
|
||||||
|
|
||||||
|
@@ -107,7 +107,7 @@
|
|||||||
--ds-comcol-logo-max-height: 500px;
|
--ds-comcol-logo-max-height: 500px;
|
||||||
|
|
||||||
--ds-process-overview-table-nb-processes-badge-size: 0.5em;
|
--ds-process-overview-table-nb-processes-badge-size: 0.5em;
|
||||||
--ds-process-overview-table-status-column-width: 150px;
|
--ds-process-overview-table-id-column-width: 120px;
|
||||||
--ds-process-overview-table-name-column-width: auto;
|
--ds-process-overview-table-name-column-width: auto;
|
||||||
--ds-process-overview-table-user-column-width: 200px;
|
--ds-process-overview-table-user-column-width: 200px;
|
||||||
--ds-process-overview-table-info-column-width: 250px;
|
--ds-process-overview-table-info-column-width: 250px;
|
||||||
|
Reference in New Issue
Block a user