mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
Merge remote-tracking branch 'origin/main' into w2p-92900_Admin_options_dont_appear_after_Shibboleth_authentication_PR
This commit is contained in:
16
.gitattributes
vendored
16
.gitattributes
vendored
@@ -1,2 +1,16 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
# By default, auto detect text files and perform LF normalization
|
||||
# This ensures code is always checked in with LF line endings
|
||||
* text=auto
|
||||
|
||||
# JS and TS files must always use LF for Angular tools to work
|
||||
# Some Angular tools expect LF line endings, even on Windows.
|
||||
# This ensures Windows always checks out these files with LF line endings
|
||||
# We've copied many of these rules from https://github.com/angular/angular-cli/
|
||||
*.js eol=lf
|
||||
*.ts eol=lf
|
||||
*.json eol=lf
|
||||
*.json5 eol=lf
|
||||
*.css eol=lf
|
||||
*.scss eol=lf
|
||||
*.html eol=lf
|
||||
*.svg eol=lf
|
0
scripts/sync-i18n-files.ts
Executable file → Normal file
0
scripts/sync-i18n-files.ts
Executable file → Normal file
0
src/app/app.module.ts
Executable file → Normal file
0
src/app/app.module.ts
Executable file → Normal file
@@ -16,24 +16,24 @@ import { filter, find, map, take } from 'rxjs/operators';
|
||||
import { hasValue } from './shared/empty.util';
|
||||
import { FeatureID } from './core/data/feature-authorization/feature-id';
|
||||
import {
|
||||
CreateCommunityParentSelectorComponent
|
||||
} from './shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component';
|
||||
ThemedCreateCommunityParentSelectorComponent
|
||||
} from './shared/dso-selector/modal-wrappers/create-community-parent-selector/themed-create-community-parent-selector.component';
|
||||
import { OnClickMenuItemModel } from './shared/menu/menu-item/models/onclick.model';
|
||||
import {
|
||||
CreateCollectionParentSelectorComponent
|
||||
} from './shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component';
|
||||
ThemedCreateCollectionParentSelectorComponent
|
||||
} from './shared/dso-selector/modal-wrappers/create-collection-parent-selector/themed-create-collection-parent-selector.component';
|
||||
import {
|
||||
CreateItemParentSelectorComponent
|
||||
} from './shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component';
|
||||
ThemedCreateItemParentSelectorComponent
|
||||
} from './shared/dso-selector/modal-wrappers/create-item-parent-selector/themed-create-item-parent-selector.component';
|
||||
import {
|
||||
EditCommunitySelectorComponent
|
||||
} from './shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component';
|
||||
ThemedEditCommunitySelectorComponent
|
||||
} from './shared/dso-selector/modal-wrappers/edit-community-selector/themed-edit-community-selector.component';
|
||||
import {
|
||||
EditCollectionSelectorComponent
|
||||
} from './shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component';
|
||||
ThemedEditCollectionSelectorComponent
|
||||
} from './shared/dso-selector/modal-wrappers/edit-collection-selector/themed-edit-collection-selector.component';
|
||||
import {
|
||||
EditItemSelectorComponent
|
||||
} from './shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component';
|
||||
ThemedEditItemSelectorComponent
|
||||
} from './shared/dso-selector/modal-wrappers/edit-item-selector/themed-edit-item-selector.component';
|
||||
import {
|
||||
ExportMetadataSelectorComponent
|
||||
} from './shared/dso-selector/modal-wrappers/export-metadata-selector/export-metadata-selector.component';
|
||||
@@ -188,7 +188,7 @@ export class MenuResolver implements Resolve<boolean> {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.new_community',
|
||||
function: () => {
|
||||
this.modalService.open(CreateCommunityParentSelectorComponent);
|
||||
this.modalService.open(ThemedCreateCommunityParentSelectorComponent);
|
||||
}
|
||||
} as OnClickMenuItemModel,
|
||||
},
|
||||
@@ -201,7 +201,7 @@ export class MenuResolver implements Resolve<boolean> {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.new_collection',
|
||||
function: () => {
|
||||
this.modalService.open(CreateCollectionParentSelectorComponent);
|
||||
this.modalService.open(ThemedCreateCollectionParentSelectorComponent);
|
||||
}
|
||||
} as OnClickMenuItemModel,
|
||||
},
|
||||
@@ -214,7 +214,7 @@ export class MenuResolver implements Resolve<boolean> {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.new_item',
|
||||
function: () => {
|
||||
this.modalService.open(CreateItemParentSelectorComponent);
|
||||
this.modalService.open(ThemedCreateItemParentSelectorComponent);
|
||||
}
|
||||
} as OnClickMenuItemModel,
|
||||
},
|
||||
@@ -263,7 +263,7 @@ export class MenuResolver implements Resolve<boolean> {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.edit_community',
|
||||
function: () => {
|
||||
this.modalService.open(EditCommunitySelectorComponent);
|
||||
this.modalService.open(ThemedEditCommunitySelectorComponent);
|
||||
}
|
||||
} as OnClickMenuItemModel,
|
||||
},
|
||||
@@ -276,7 +276,7 @@ export class MenuResolver implements Resolve<boolean> {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.edit_collection',
|
||||
function: () => {
|
||||
this.modalService.open(EditCollectionSelectorComponent);
|
||||
this.modalService.open(ThemedEditCollectionSelectorComponent);
|
||||
}
|
||||
} as OnClickMenuItemModel,
|
||||
},
|
||||
@@ -289,7 +289,7 @@ export class MenuResolver implements Resolve<boolean> {
|
||||
type: MenuItemType.ONCLICK,
|
||||
text: 'menu.section.edit_item',
|
||||
function: () => {
|
||||
this.modalService.open(EditItemSelectorComponent);
|
||||
this.modalService.open(ThemedEditItemSelectorComponent);
|
||||
}
|
||||
} as OnClickMenuItemModel,
|
||||
},
|
||||
|
@@ -1,53 +1,99 @@
|
||||
<div class="container" *ngVar="(processRD$ | async)?.payload as process">
|
||||
<div class="d-flex">
|
||||
<h2 class="flex-grow-1">{{'process.detail.title' | translate:{id: process?.processId, name: process?.scriptName} }}</h2>
|
||||
<div>
|
||||
<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>
|
||||
</div>
|
||||
<h2 class="flex-grow-1">{{'process.detail.title' | translate:{
|
||||
id: process?.processId,
|
||||
name: process?.scriptName
|
||||
} }}</h2>
|
||||
</div>
|
||||
<ds-process-detail-field id="process-name" [title]="'process.detail.script'">
|
||||
<div>{{ process?.scriptName }}</div>
|
||||
</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>
|
||||
</ds-process-detail-field>
|
||||
|
||||
<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-file-download-link *ngFor="let file of files; let last=last;" [bitstream]="file">
|
||||
<span>{{getFileName(file)}}</span>
|
||||
<span>({{(file?.sizeBytes) | dsFileSize }})</span>
|
||||
</ds-file-download-link>
|
||||
<ds-process-detail-field *ngIf="files && files?.length > 0" id="process-files"
|
||||
[title]="'process.detail.output-files'">
|
||||
<ds-file-download-link *ngFor="let file of files; let last=last;" [bitstream]="file">
|
||||
<span>{{getFileName(file)}}</span>
|
||||
<span>({{(file?.sizeBytes) | dsFileSize }})</span>
|
||||
</ds-file-download-link>
|
||||
</ds-process-detail-field>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
</ds-process-detail-field>
|
||||
|
||||
<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()">
|
||||
{{ 'process.detail.logs.button' | translate }}
|
||||
</button>
|
||||
<ds-themed-loading *ngIf="retrievingOutputLogs$ | async" class="ds-themed-loading" message="{{ 'process.detail.logs.loading' | translate }}"></ds-themed-loading>
|
||||
<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)
|
||||
<button *ngIf="!showOutputLogs && process?._links?.output?.href != undefined" id="showOutputButton"
|
||||
class="btn btn-primary" (click)="showProcessOutputLogs()">
|
||||
{{ 'process.detail.logs.button' | translate }}
|
||||
</button>
|
||||
<ds-themed-loading *ngIf="retrievingOutputLogs$ | async" class="ds-themed-loading"
|
||||
message="{{ 'process.detail.logs.loading' | translate }}"></ds-themed-loading>
|
||||
<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">
|
||||
{{ 'process.detail.logs.none' | translate }}
|
||||
</p>
|
||||
{{ 'process.detail.logs.none' | translate }}
|
||||
</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>
|
||||
|
||||
<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>
|
||||
|
||||
<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 { ProcessDetailFieldComponent } from './process-detail-field/process-detail-field.component';
|
||||
import { Process } from '../processes/process.model';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { FileSizePipe } from '../../shared/utils/file-size-pipe';
|
||||
import { Bitstream } from '../../core/shared/bitstream.model';
|
||||
import { ProcessDataService } from '../../core/data/processes/process-data.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 { 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', () => {
|
||||
let component: ProcessDetailComponent;
|
||||
@@ -44,6 +52,11 @@ describe('ProcessDetailComponent', () => {
|
||||
|
||||
let processOutput;
|
||||
|
||||
let modalService;
|
||||
let notificationsService;
|
||||
|
||||
let router;
|
||||
|
||||
function init() {
|
||||
processOutput = 'Process Started';
|
||||
process = Object.assign(new Process(), {
|
||||
@@ -93,7 +106,8 @@ describe('ProcessDetailComponent', () => {
|
||||
}
|
||||
});
|
||||
processService = jasmine.createSpyObj('processService', {
|
||||
getFiles: createSuccessfulRemoteDataObject$(createPaginatedList(files))
|
||||
getFiles: createSuccessfulRemoteDataObject$(createPaginatedList(files)),
|
||||
delete: createSuccessfulRemoteDataObject$(null)
|
||||
});
|
||||
bitstreamDataService = jasmine.createSpyObj('bitstreamDataService', {
|
||||
findByHref: createSuccessfulRemoteDataObject$(logBitstream)
|
||||
@@ -104,13 +118,23 @@ describe('ProcessDetailComponent', () => {
|
||||
httpClient = jasmine.createSpyObj('httpClient', {
|
||||
get: observableOf(processOutput)
|
||||
});
|
||||
|
||||
modalService = jasmine.createSpyObj('modalService', {
|
||||
open: {}
|
||||
});
|
||||
|
||||
notificationsService = new NotificationsServiceStub();
|
||||
|
||||
router = jasmine.createSpyObj('router', {
|
||||
navigateByUrl:{}
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
init();
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ProcessDetailComponent, ProcessDetailFieldComponent, VarDirective, FileSizePipe],
|
||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
|
||||
imports: [TranslateModule.forRoot()],
|
||||
providers: [
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
@@ -121,6 +145,9 @@ describe('ProcessDetailComponent', () => {
|
||||
{ provide: DSONameService, useValue: nameService },
|
||||
{ provide: AuthService, useValue: new AuthServiceMock() },
|
||||
{ provide: HttpClient, useValue: httpClient },
|
||||
{ provide: NgbModal, useValue: modalService },
|
||||
{ provide: NotificationsService, useValue: notificationsService },
|
||||
{ provide: Router, useValue: router },
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
}).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 { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||
import {
|
||||
getFirstSucceededRemoteDataPayload,
|
||||
getFirstSucceededRemoteData
|
||||
getFirstCompletedRemoteData,
|
||||
getFirstSucceededRemoteData,
|
||||
getFirstSucceededRemoteDataPayload
|
||||
} from '../../core/shared/operators';
|
||||
import { URLCombiner } from '../../core/url-combiner/url-combiner';
|
||||
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 { Process } from '../processes/process.model';
|
||||
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({
|
||||
selector: 'ds-process-detail',
|
||||
@@ -71,6 +76,11 @@ export class ProcessDetailComponent implements OnInit {
|
||||
*/
|
||||
dateFormat = 'yyyy-MM-dd HH:mm:ss ZZZZ';
|
||||
|
||||
/**
|
||||
* Reference to NgbModal
|
||||
*/
|
||||
protected modalRef: NgbModalRef;
|
||||
|
||||
constructor(protected route: ActivatedRoute,
|
||||
protected router: Router,
|
||||
protected processService: ProcessDataService,
|
||||
@@ -78,7 +88,11 @@ export class ProcessDetailComponent implements OnInit {
|
||||
protected nameService: DSONameService,
|
||||
private zone: NgZone,
|
||||
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()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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="d-flex">
|
||||
<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>
|
||||
<ds-pagination *ngIf="(processesRD$ | async)?.payload?.totalElements > 0"
|
||||
[paginationOptions]="pageConfig"
|
||||
@@ -19,19 +31,61 @@
|
||||
<th scope="col">{{'process.overview.table.start' | 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.actions' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<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.scriptName}}</a></td>
|
||||
<td *ngVar="(getEpersonName(process.userId) | async) as ePersonName">{{ePersonName}}</td>
|
||||
<td>{{process.startTime | date:dateFormat:'UTC'}}</td>
|
||||
<td>{{process.endTime | date:dateFormat:'UTC'}}</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>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</ds-pagination>
|
||||
</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 { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { VarDirective } from '../../shared/utils/var.directive';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
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 { createPaginatedList } from '../../shared/testing/utils.test';
|
||||
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 { FindListOptions } from '../../core/data/find-list-options.model';
|
||||
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', () => {
|
||||
let component: ProcessOverviewComponent;
|
||||
@@ -30,6 +30,9 @@ describe('ProcessOverviewComponent', () => {
|
||||
let processes: Process[];
|
||||
let ePerson: EPerson;
|
||||
|
||||
let processBulkDeleteService;
|
||||
let modalService;
|
||||
|
||||
const pipe = new DatePipe('en-US');
|
||||
|
||||
function init() {
|
||||
@@ -80,6 +83,29 @@ describe('ProcessOverviewComponent', () => {
|
||||
});
|
||||
|
||||
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(() => {
|
||||
@@ -90,7 +116,9 @@ describe('ProcessOverviewComponent', () => {
|
||||
providers: [
|
||||
{ provide: ProcessDataService, useValue: processService },
|
||||
{ provide: EPersonDataService, useValue: ePersonService },
|
||||
{ provide: PaginationService, useValue: paginationService }
|
||||
{ provide: PaginationService, useValue: paginationService },
|
||||
{ provide: ProcessBulkDeleteService, useValue: processBulkDeleteService },
|
||||
{ provide: NgbModal, useValue: modalService },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
@@ -154,5 +182,71 @@ describe('ProcessOverviewComponent', () => {
|
||||
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 { Observable } from 'rxjs';
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Observable, Subscription } from 'rxjs';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { PaginatedList } from '../../core/data/paginated-list.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 { PaginationService } from '../../core/pagination/pagination.service';
|
||||
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({
|
||||
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
|
||||
*/
|
||||
export class ProcessOverviewComponent implements OnInit {
|
||||
export class ProcessOverviewComponent implements OnInit, OnDestroy {
|
||||
|
||||
/**
|
||||
* List of all processes
|
||||
@@ -46,13 +49,22 @@ export class ProcessOverviewComponent implements OnInit {
|
||||
*/
|
||||
dateFormat = 'yyyy-MM-dd HH:mm:ss';
|
||||
|
||||
processesToDelete: string[] = [];
|
||||
private modalRef: any;
|
||||
|
||||
isProcessingSub: Subscription;
|
||||
|
||||
constructor(protected processService: ProcessDataService,
|
||||
protected paginationService: PaginationService,
|
||||
protected ePersonService: EPersonDataService) {
|
||||
protected ePersonService: EPersonDataService,
|
||||
protected modalService: NgbModal,
|
||||
public processBulkDeleteService: ProcessBulkDeleteService,
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.setProcesses();
|
||||
this.processBulkDeleteService.clearAllProcesses();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -60,7 +72,7 @@ export class ProcessOverviewComponent implements OnInit {
|
||||
*/
|
||||
setProcesses() {
|
||||
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)
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,28 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {CreateCollectionParentSelectorComponent} from './create-collection-parent-selector.component';
|
||||
import {ThemedComponent} from 'src/app/shared/theme-support/themed.component';
|
||||
|
||||
/**
|
||||
* Themed wrapper for CreateCollectionParentSelectorComponent
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-themed-create-collection-parent-selector',
|
||||
styleUrls: [],
|
||||
templateUrl: '../../../theme-support/themed.component.html'
|
||||
})
|
||||
export class ThemedCreateCollectionParentSelectorComponent
|
||||
extends ThemedComponent<CreateCollectionParentSelectorComponent> {
|
||||
|
||||
protected getComponentName(): string {
|
||||
return 'CreateCollectionParentSelectorComponent';
|
||||
}
|
||||
|
||||
protected importThemedComponent(themeName: string): Promise<any> {
|
||||
return import(`../../../../../themes/${themeName}/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component`);
|
||||
}
|
||||
|
||||
protected importUnthemedComponent(): Promise<any> {
|
||||
return import('./create-collection-parent-selector.component');
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {CreateCommunityParentSelectorComponent} from './create-community-parent-selector.component';
|
||||
import {ThemedComponent} from 'src/app/shared/theme-support/themed.component';
|
||||
|
||||
/**
|
||||
* Themed wrapper for CreateCommunityParentSelectorComponent
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-themed-create-community-parent-selector',
|
||||
styleUrls: [],
|
||||
templateUrl: '../../../theme-support/themed.component.html'
|
||||
})
|
||||
export class ThemedCreateCommunityParentSelectorComponent
|
||||
extends ThemedComponent<CreateCommunityParentSelectorComponent> {
|
||||
protected getComponentName(): string {
|
||||
return 'CreateCommunityParentSelectorComponent';
|
||||
}
|
||||
|
||||
protected importThemedComponent(themeName: string): Promise<any> {
|
||||
return import(`../../../../../themes/${themeName}/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component`);
|
||||
}
|
||||
|
||||
protected importUnthemedComponent(): Promise<any> {
|
||||
return import('./create-community-parent-selector.component');
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,31 @@
|
||||
import {Component, Input} from '@angular/core';
|
||||
import {CreateItemParentSelectorComponent} from './create-item-parent-selector.component';
|
||||
import {ThemedComponent} from 'src/app/shared/theme-support/themed.component';
|
||||
|
||||
/**
|
||||
* Themed wrapper for CreateItemParentSelectorComponent
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-themed-create-item-parent-selector',
|
||||
styleUrls: [],
|
||||
templateUrl: '../../../theme-support/themed.component.html'
|
||||
})
|
||||
export class ThemedCreateItemParentSelectorComponent
|
||||
extends ThemedComponent<CreateItemParentSelectorComponent> {
|
||||
@Input() entityType: string;
|
||||
|
||||
protected inAndOutputNames: (keyof CreateItemParentSelectorComponent & keyof this)[] = ['entityType'];
|
||||
|
||||
protected getComponentName(): string {
|
||||
return 'CreateItemParentSelectorComponent';
|
||||
}
|
||||
|
||||
protected importThemedComponent(themeName: string): Promise<any> {
|
||||
return import(`../../../../../themes/${themeName}/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component`);
|
||||
}
|
||||
|
||||
protected importUnthemedComponent(): Promise<any> {
|
||||
return import('./create-item-parent-selector.component');
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {EditCollectionSelectorComponent} from './edit-collection-selector.component';
|
||||
import {ThemedComponent} from 'src/app/shared/theme-support/themed.component';
|
||||
|
||||
/**
|
||||
* Themed wrapper for EditCollectionSelectorComponent
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-themed-edit-collection-selector',
|
||||
styleUrls: [],
|
||||
templateUrl: '../../../theme-support/themed.component.html'
|
||||
})
|
||||
export class ThemedEditCollectionSelectorComponent
|
||||
extends ThemedComponent<EditCollectionSelectorComponent> {
|
||||
protected getComponentName(): string {
|
||||
return 'EditCollectionSelectorComponent';
|
||||
}
|
||||
|
||||
protected importThemedComponent(themeName: string): Promise<any> {
|
||||
return import(`../../../../../themes/${themeName}/app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component`);
|
||||
}
|
||||
|
||||
protected importUnthemedComponent(): Promise<any> {
|
||||
return import('./edit-collection-selector.component');
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {EditCommunitySelectorComponent} from './edit-community-selector.component';
|
||||
import {ThemedComponent} from 'src/app/shared/theme-support/themed.component';
|
||||
|
||||
/**
|
||||
* Themed wrapper for EditCommunitySelectorComponent
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-themed-edit-community-selector',
|
||||
styleUrls: [],
|
||||
templateUrl: '../../../theme-support/themed.component.html'
|
||||
})
|
||||
export class ThemedEditCommunitySelectorComponent
|
||||
extends ThemedComponent<EditCommunitySelectorComponent> {
|
||||
protected getComponentName(): string {
|
||||
return 'EditCommunitySelectorComponent';
|
||||
}
|
||||
|
||||
protected importThemedComponent(themeName: string): Promise<any> {
|
||||
return import(`../../../../../themes/${themeName}/app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component`);
|
||||
}
|
||||
|
||||
protected importUnthemedComponent(): Promise<any> {
|
||||
return import('./edit-community-selector.component');
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {EditItemSelectorComponent} from './edit-item-selector.component';
|
||||
import {ThemedComponent} from 'src/app/shared/theme-support/themed.component';
|
||||
|
||||
/**
|
||||
* Themed wrapper for EditItemSelectorComponent
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-themed-edit-item-selector',
|
||||
styleUrls: [],
|
||||
templateUrl: '../../../theme-support/themed.component.html'
|
||||
})
|
||||
export class ThemedEditItemSelectorComponent
|
||||
extends ThemedComponent<EditItemSelectorComponent> {
|
||||
protected getComponentName(): string {
|
||||
return 'EditItemSelectorComponent';
|
||||
}
|
||||
|
||||
protected importThemedComponent(themeName: string): Promise<any> {
|
||||
return import(`../../../../../themes/${themeName}/app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component`);
|
||||
}
|
||||
|
||||
protected importUnthemedComponent(): Promise<any> {
|
||||
return import('./edit-item-selector.component');
|
||||
}
|
||||
|
||||
}
|
@@ -124,12 +124,21 @@ import { DSOSelectorComponent } from './dso-selector/dso-selector/dso-selector.c
|
||||
import {
|
||||
CreateCommunityParentSelectorComponent
|
||||
} from './dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component';
|
||||
import {
|
||||
ThemedCreateCommunityParentSelectorComponent
|
||||
} from './dso-selector/modal-wrappers/create-community-parent-selector/themed-create-community-parent-selector.component';
|
||||
import {
|
||||
CreateItemParentSelectorComponent
|
||||
} from './dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component';
|
||||
import {
|
||||
ThemedCreateItemParentSelectorComponent
|
||||
} from './dso-selector/modal-wrappers/create-item-parent-selector/themed-create-item-parent-selector.component';
|
||||
import {
|
||||
CreateCollectionParentSelectorComponent
|
||||
} from './dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component';
|
||||
import {
|
||||
ThemedCreateCollectionParentSelectorComponent
|
||||
} from './dso-selector/modal-wrappers/create-collection-parent-selector/themed-create-collection-parent-selector.component';
|
||||
import {
|
||||
CommunitySearchResultListElementComponent
|
||||
} from './object-list/search-result-list-element/community-search-result/community-search-result-list-element.component';
|
||||
@@ -139,12 +148,21 @@ import {
|
||||
import {
|
||||
EditItemSelectorComponent
|
||||
} from './dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component';
|
||||
import {
|
||||
ThemedEditItemSelectorComponent
|
||||
} from './dso-selector/modal-wrappers/edit-item-selector/themed-edit-item-selector.component';
|
||||
import {
|
||||
EditCommunitySelectorComponent
|
||||
} from './dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component';
|
||||
import {
|
||||
ThemedEditCommunitySelectorComponent
|
||||
} from './dso-selector/modal-wrappers/edit-community-selector/themed-edit-community-selector.component';
|
||||
import {
|
||||
EditCollectionSelectorComponent
|
||||
} from './dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component';
|
||||
import {
|
||||
ThemedEditCollectionSelectorComponent
|
||||
} from './dso-selector/modal-wrappers/edit-collection-selector/themed-edit-collection-selector.component';
|
||||
import {
|
||||
ItemListPreviewComponent
|
||||
} from './object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component';
|
||||
@@ -395,11 +413,17 @@ const COMPONENTS = [
|
||||
DsoInputSuggestionsComponent,
|
||||
DSOSelectorComponent,
|
||||
CreateCommunityParentSelectorComponent,
|
||||
ThemedCreateCommunityParentSelectorComponent,
|
||||
CreateCollectionParentSelectorComponent,
|
||||
ThemedCreateCollectionParentSelectorComponent,
|
||||
CreateItemParentSelectorComponent,
|
||||
ThemedCreateItemParentSelectorComponent,
|
||||
EditCommunitySelectorComponent,
|
||||
ThemedEditCommunitySelectorComponent,
|
||||
EditCollectionSelectorComponent,
|
||||
ThemedEditCollectionSelectorComponent,
|
||||
EditItemSelectorComponent,
|
||||
ThemedEditItemSelectorComponent,
|
||||
CommunitySearchResultListElementComponent,
|
||||
CollectionSearchResultListElementComponent,
|
||||
BrowseByComponent,
|
||||
@@ -491,11 +515,17 @@ const ENTRY_COMPONENTS = [
|
||||
StartsWithDateComponent,
|
||||
StartsWithTextComponent,
|
||||
CreateCommunityParentSelectorComponent,
|
||||
ThemedCreateCommunityParentSelectorComponent,
|
||||
CreateCollectionParentSelectorComponent,
|
||||
ThemedCreateCollectionParentSelectorComponent,
|
||||
CreateItemParentSelectorComponent,
|
||||
ThemedCreateItemParentSelectorComponent,
|
||||
EditCommunitySelectorComponent,
|
||||
ThemedEditCommunitySelectorComponent,
|
||||
EditCollectionSelectorComponent,
|
||||
ThemedEditCollectionSelectorComponent,
|
||||
EditItemSelectorComponent,
|
||||
ThemedEditItemSelectorComponent,
|
||||
PlainTextMetadataListElementComponent,
|
||||
ItemMetadataListElementComponent,
|
||||
MetadataRepresentationListElementComponent,
|
||||
|
@@ -24,9 +24,12 @@
|
||||
</ds-viewable-collection>
|
||||
<ds-themed-loading *ngIf="(isLoading$ | async)"
|
||||
message="{{'loading.search-results' | translate}}"></ds-themed-loading>
|
||||
<div *ngIf="!(isLoading$ | async) && entriesRD?.payload?.page?.length === 0" id="empty-external-entry-list">
|
||||
<div *ngIf="!(isLoading$ | async) && entriesRD?.payload?.page?.length === 0" data-test="empty-external-entry-list">
|
||||
<ds-alert [type]="'alert-info'">{{ 'search.results.empty' | translate }}</ds-alert>
|
||||
</div>
|
||||
<div *ngIf="!(isLoading$ | async) && entriesRD.statusCode === 500" data-test="empty-external-error-500">
|
||||
<ds-alert [type]="'alert-info'">{{ 'search.results.response.500' | translate }}</ds-alert>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div *ngIf="reload$.value.sourceId === ''" class="col-md-12">
|
||||
|
@@ -19,9 +19,15 @@ import { VarDirective } from '../../shared/utils/var.directive';
|
||||
import { routeServiceStub } from '../../shared/testing/route-service.stub';
|
||||
import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model';
|
||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||
import {
|
||||
createFailedRemoteDataObject$,
|
||||
createSuccessfulRemoteDataObject,
|
||||
createSuccessfulRemoteDataObject$
|
||||
} from '../../shared/remote-data.utils';
|
||||
import { ExternalSourceEntry } from '../../core/shared/external-source-entry.model';
|
||||
import { SubmissionImportExternalPreviewComponent } from './import-external-preview/submission-import-external-preview.component';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
|
||||
describe('SubmissionImportExternalComponent test suite', () => {
|
||||
let comp: SubmissionImportExternalComponent;
|
||||
@@ -44,7 +50,8 @@ describe('SubmissionImportExternalComponent test suite', () => {
|
||||
beforeEach(waitForAsync (() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
TranslateModule.forRoot()
|
||||
TranslateModule.forRoot(),
|
||||
BrowserAnimationsModule
|
||||
],
|
||||
declarations: [
|
||||
SubmissionImportExternalComponent,
|
||||
@@ -177,6 +184,326 @@ describe('SubmissionImportExternalComponent test suite', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('handle backend response for search query', () => {
|
||||
const paginatedData: any = {
|
||||
'timeCompleted': 1657009282990,
|
||||
'msToLive': 900000,
|
||||
'lastUpdated': 1657009282990,
|
||||
'state': 'Success',
|
||||
'errorMessage': null,
|
||||
'payload': {
|
||||
'type': {
|
||||
'value': 'paginated-list'
|
||||
},
|
||||
'pageInfo': {
|
||||
'elementsPerPage': 10,
|
||||
'totalElements': 11971608,
|
||||
'totalPages': 1197161,
|
||||
'currentPage': 1
|
||||
},
|
||||
'_links': {
|
||||
'first': {
|
||||
'href': 'https://example.com/server/api/integration/externalsources/scopus/entries?query=test&page=0&size=10&sort=id,asc'
|
||||
},
|
||||
'self': {
|
||||
'href': 'https://example.com/server/api/integration/externalsources/scopus/entries?sort=id,ASC&page=0&size=10&query=test'
|
||||
},
|
||||
'next': {
|
||||
'href': 'https://example.com/server/api/integration/externalsources/scopus/entries?query=test&page=1&size=10&sort=id,asc'
|
||||
},
|
||||
'last': {
|
||||
'href': 'https://example.com/server/api/integration/externalsources/scopus/entries?query=test&page=1197160&size=10&sort=id,asc'
|
||||
},
|
||||
'page': [
|
||||
{
|
||||
'href': 'https://example.com/server/api/integration/externalsources/scopus/entryValues/2-s2.0-85130258665'
|
||||
}
|
||||
]
|
||||
},
|
||||
'page': [
|
||||
{
|
||||
'id': '2-s2.0-85130258665',
|
||||
'type': 'externalSourceEntry',
|
||||
'display': 'Biological activities of endophytic fungi isolated from Annona muricata Linnaeus: a systematic review',
|
||||
'value': 'Biological activities of endophytic fungi isolated from Annona muricata Linnaeus: a systematic review',
|
||||
'externalSource': 'scopus',
|
||||
'metadata': {
|
||||
'dc.contributor.author': [
|
||||
{
|
||||
'uuid': 'cbceba09-4c12-4968-ab02-2f77a985b422',
|
||||
'language': null,
|
||||
'value': 'Silva I.M.M.',
|
||||
'place': -1,
|
||||
'authority': null,
|
||||
'confidence': -1
|
||||
}
|
||||
],
|
||||
'dc.date.issued': [
|
||||
{
|
||||
'uuid': 'e8d3c306-ce21-43e2-8a80-5f257cc3b7ea',
|
||||
'language': null,
|
||||
'value': '2024-01-01',
|
||||
'place': -1,
|
||||
'authority': null,
|
||||
'confidence': -1
|
||||
}
|
||||
],
|
||||
'dc.description.abstract': [
|
||||
{
|
||||
'uuid': 'c9ee4076-c602-4c1d-ab1a-60bbdd0dd511',
|
||||
'language': null,
|
||||
'value': 'This systematic review integrates the data available in the literature regarding the biological activities of the extracts of endophytic fungi isolated from Annona muricata and their secondary metabolites. The search was performed using four electronic databases, and studies’ quality was evaluated using an adapted assessment tool. The initial database search yielded 436 results; ten studies were selected for inclusion. The leaf was the most studied part of the plant (in nine studies); Periconia sp. was the most tested fungus (n = 4); the most evaluated biological activity was anticancer (n = 6), followed by antiviral (n = 3). Antibacterial, antifungal, and antioxidant activities were also tested. Terpenoids or terpenoid hybrid compounds were the most abundant chemical metabolites. Phenolic compounds, esters, alkaloids, saturated and unsaturated fatty acids, aromatic compounds, and peptides were also reported. The selected studies highlighted the biotechnological potentiality of the endophytic fungi extracts from A. muricata. Consequently, it can be considered a promising source of biological compounds with antioxidant effects and active against different microorganisms and cancer cells. Further research is needed involving different plant tissues, other microorganisms, such as SARS-CoV-2, and different cancer cells.',
|
||||
'place': -1,
|
||||
'authority': null,
|
||||
'confidence': -1
|
||||
}
|
||||
],
|
||||
'dc.identifier.doi': [
|
||||
{
|
||||
'uuid': '95ec26be-c1b4-4c4a-b12d-12421a4f181d',
|
||||
'language': null,
|
||||
'value': '10.1590/1519-6984.259525',
|
||||
'place': -1,
|
||||
'authority': null,
|
||||
'confidence': -1
|
||||
}
|
||||
],
|
||||
'dc.identifier.pmid': [
|
||||
{
|
||||
'uuid': 'd6913cd6-1007-4013-b486-3f07192bc739',
|
||||
'language': null,
|
||||
'value': '35588520',
|
||||
'place': -1,
|
||||
'authority': null,
|
||||
'confidence': -1
|
||||
}
|
||||
],
|
||||
'dc.identifier.scopus': [
|
||||
{
|
||||
'uuid': '6386a1f6-84ba-431d-a583-e16d19af8db0',
|
||||
'language': null,
|
||||
'value': '2-s2.0-85130258665',
|
||||
'place': -1,
|
||||
'authority': null,
|
||||
'confidence': -1
|
||||
}
|
||||
],
|
||||
'dc.relation.grantno': [
|
||||
{
|
||||
'uuid': 'bcafd7b0-827d-4abb-8608-95dc40a8e58a',
|
||||
'language': null,
|
||||
'value': 'undefined',
|
||||
'place': -1,
|
||||
'authority': null,
|
||||
'confidence': -1
|
||||
}
|
||||
],
|
||||
'dc.relation.ispartof': [
|
||||
{
|
||||
'uuid': '680819c8-c143-405f-9d09-f84d2d5cd338',
|
||||
'language': null,
|
||||
'value': 'Brazilian Journal of Biology',
|
||||
'place': -1,
|
||||
'authority': null,
|
||||
'confidence': -1
|
||||
}
|
||||
],
|
||||
'dc.relation.ispartofseries': [
|
||||
{
|
||||
'uuid': '06634104-127b-44f6-9dcc-efae24b74bd1',
|
||||
'language': null,
|
||||
'value': 'Brazilian Journal of Biology',
|
||||
'place': -1,
|
||||
'authority': null,
|
||||
'confidence': -1
|
||||
}
|
||||
],
|
||||
'dc.relation.issn': [
|
||||
{
|
||||
'uuid': '5f6cce46-2538-49e9-8ed0-a3988dcac6c5',
|
||||
'language': null,
|
||||
'value': '15196984',
|
||||
'place': -1,
|
||||
'authority': null,
|
||||
'confidence': -1
|
||||
}
|
||||
],
|
||||
'dc.subject': [
|
||||
{
|
||||
'uuid': '0b6fbc77-de54-4f4a-b317-3d74a429f22a',
|
||||
'language': null,
|
||||
'value': 'biological products | biotechnology | mycology | soursop',
|
||||
'place': -1,
|
||||
'authority': null,
|
||||
'confidence': -1
|
||||
}
|
||||
],
|
||||
'dc.title': [
|
||||
{
|
||||
'uuid': '4c0fa3d3-1a8c-4302-a772-4a4d0408df35',
|
||||
'language': null,
|
||||
'value': 'Biological activities of endophytic fungi isolated from Annona muricata Linnaeus: a systematic review',
|
||||
'place': -1,
|
||||
'authority': null,
|
||||
'confidence': -1
|
||||
}
|
||||
],
|
||||
'dc.type': [
|
||||
{
|
||||
'uuid': '5b6e0337-6f79-4574-a720-536816d1dc6e',
|
||||
'language': null,
|
||||
'value': 'Journal',
|
||||
'place': -1,
|
||||
'authority': null,
|
||||
'confidence': -1
|
||||
}
|
||||
],
|
||||
'oaire.citation.volume': [
|
||||
{
|
||||
'uuid': 'b88b0246-61a9-4aca-917f-68afc8ead7d8',
|
||||
'language': null,
|
||||
'value': '84',
|
||||
'place': -1,
|
||||
'authority': null,
|
||||
'confidence': -1
|
||||
}
|
||||
],
|
||||
'oairecerif.affiliation.orgunit': [
|
||||
{
|
||||
'uuid': '487c0fbc-3622-4cc7-a5fa-4edf780c6a21',
|
||||
'language': null,
|
||||
'value': 'Universidade Federal do Reconcavo da Bahia',
|
||||
'place': -1,
|
||||
'authority': null,
|
||||
'confidence': -1
|
||||
}
|
||||
],
|
||||
'oairecerif.citation.number': [
|
||||
{
|
||||
'uuid': '90808bdd-f456-4ba3-91aa-b82fb3c453f6',
|
||||
'language': null,
|
||||
'value': 'e259525',
|
||||
'place': -1,
|
||||
'authority': null,
|
||||
'confidence': -1
|
||||
}
|
||||
],
|
||||
'person.identifier.orcid': [
|
||||
{
|
||||
'uuid': 'e533d0d2-cf26-4c3e-b5ae-cabf497dfb6b',
|
||||
'language': null,
|
||||
'value': '#PLACEHOLDER_PARENT_METADATA_VALUE#',
|
||||
'place': -1,
|
||||
'authority': null,
|
||||
'confidence': -1
|
||||
}
|
||||
],
|
||||
'person.identifier.scopus-author-id': [
|
||||
{
|
||||
'uuid': '4faf0be5-0226-4d4f-92a0-938397c4ec02',
|
||||
'language': null,
|
||||
'value': '42561627000',
|
||||
'place': -1,
|
||||
'authority': null,
|
||||
'confidence': -1
|
||||
}
|
||||
]
|
||||
},
|
||||
'_links': {
|
||||
'self': {
|
||||
'href': 'https://example.com/server/api/integration/externalsources/scopus/entryValues/2-s2.0-85130258665'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
'statusCode': 200
|
||||
};
|
||||
const errorObj = {
|
||||
errorMessage: 'Http failure response for ' +
|
||||
'https://example.com/server/api/integration/externalsources/pubmed/entries?sort=id,ASC&page=0&size=10&query=test: 500 OK',
|
||||
statusCode: 500,
|
||||
timeCompleted: 1656950434666,
|
||||
errors: [{
|
||||
'message': 'Internal Server Error', 'paths': ['/server/api/integration/externalsources/pubmed/entries']
|
||||
}]
|
||||
};
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SubmissionImportExternalComponent);
|
||||
comp = fixture.componentInstance;
|
||||
compAsAny = comp;
|
||||
scheduler = getTestScheduler();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fixture.destroy();
|
||||
comp = null;
|
||||
compAsAny = null;
|
||||
});
|
||||
|
||||
it('REST endpoint returns a 200 response with valid content', () => {
|
||||
mockExternalSourceService.getExternalSourceEntries.and.returnValue(createSuccessfulRemoteDataObject$(paginatedData.payload));
|
||||
const expectedEntries = createSuccessfulRemoteDataObject(paginatedData.payload);
|
||||
spyOn(routeServiceStub, 'getQueryParameterValue').and.callFake((param) => {
|
||||
if (param === 'entity') {
|
||||
return observableOf('Publication');
|
||||
} else if (param === 'sourceId') {
|
||||
return observableOf('scopus');
|
||||
} else if (param === 'query') {
|
||||
return observableOf('test');
|
||||
}
|
||||
return observableOf({});
|
||||
});
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(comp.isLoading$.value).toBe(false);
|
||||
expect(comp.entriesRD$.value).toEqual(expectedEntries);
|
||||
const viewableCollection = fixture.debugElement.query(By.css('ds-viewable-collection'));
|
||||
expect(viewableCollection).toBeTruthy();
|
||||
});
|
||||
|
||||
it('REST endpoint returns a 200 response with no results', () => {
|
||||
mockExternalSourceService.getExternalSourceEntries.and.returnValue(createSuccessfulRemoteDataObject$(createPaginatedList([])));
|
||||
const expectedEntries = createSuccessfulRemoteDataObject(createPaginatedList([]));
|
||||
spyOn(routeServiceStub, 'getQueryParameterValue').and.callFake((param) => {
|
||||
if (param === 'entity') {
|
||||
return observableOf('Publication');
|
||||
}
|
||||
return observableOf({});
|
||||
});
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(comp.isLoading$.value).toBe(false);
|
||||
expect(comp.entriesRD$.value).toEqual(expectedEntries);
|
||||
const noDataAlert = fixture.debugElement.query(By.css('[data-test="empty-external-entry-list"]'));
|
||||
expect(noDataAlert).toBeTruthy();
|
||||
});
|
||||
|
||||
it('REST endpoint returns a 500 error', () => {
|
||||
mockExternalSourceService.getExternalSourceEntries.and.returnValue(createFailedRemoteDataObject$(
|
||||
errorObj.errorMessage,
|
||||
errorObj.statusCode,
|
||||
errorObj.timeCompleted
|
||||
));
|
||||
spyOn(routeServiceStub, 'getQueryParameterValue').and.callFake((param) => {
|
||||
if (param === 'entity') {
|
||||
return observableOf('Publication');
|
||||
} else if (param === 'sourceId') {
|
||||
return observableOf('pubmed');
|
||||
} else if (param === 'query') {
|
||||
return observableOf('test');
|
||||
}
|
||||
return observableOf({});
|
||||
});
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(comp.isLoading$.value).toBe(false);
|
||||
expect(comp.entriesRD$.value.statusCode).toEqual(500);
|
||||
const noDataAlert = fixture.debugElement.query(By.css('[data-test="empty-external-error-500"]'));
|
||||
expect(noDataAlert).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// declare a test component
|
||||
|
@@ -2983,6 +2983,22 @@
|
||||
|
||||
"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)",
|
||||
@@ -3003,6 +3019,25 @@
|
||||
|
||||
"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",
|
||||
|
||||
@@ -3583,6 +3618,7 @@
|
||||
|
||||
"search.results.view-result": "View",
|
||||
|
||||
"search.results.response.500": "An error occurred during query execution, please try again later",
|
||||
|
||||
"default.search.results.head": "Search Results",
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
10252
src/assets/i18n/tr.json5
10252
src/assets/i18n/tr.json5
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,9 @@ $fa-font-path: "^assets/fonts" !default;
|
||||
/* Images */
|
||||
$image-path: "../assets/images" !default;
|
||||
|
||||
// enable-responsive-font-sizes allows text to scale more naturally across device and viewport sizes
|
||||
$enable-responsive-font-sizes: true;
|
||||
|
||||
/** Bootstrap Variables **/
|
||||
/* Colors */
|
||||
$gray-700: #495057 !default; // Bootstrap $gray-700
|
||||
|
@@ -0,0 +1,11 @@
|
||||
<div>
|
||||
<div class="modal-header">{{'dso-selector.'+ action + '.' + objectType.toString().toLowerCase() + '.head' | translate}}
|
||||
<button type="button" class="close" (click)="close()" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<h5 *ngIf="header" class="px-2">{{header | translate}}</h5>
|
||||
<ds-dso-selector [currentDSOId]="dsoRD?.payload.uuid" [types]="selectorTypes" (onSelect)="selectObject($event)"></ds-dso-selector>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,13 @@
|
||||
import { Component } from '@angular/core';
|
||||
import {
|
||||
CreateCollectionParentSelectorComponent as BaseComponent
|
||||
} from '../../../../../../../app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-create-collection-parent-selector',
|
||||
// styleUrls: ['./create-collection-parent-selector.component.scss'],
|
||||
// templateUrl: './create-collection-parent-selector.component.html',
|
||||
templateUrl: '../../../../../../../app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.html',
|
||||
})
|
||||
export class CreateCollectionParentSelectorComponent extends BaseComponent {
|
||||
}
|
@@ -0,0 +1,19 @@
|
||||
<div>
|
||||
<div class="modal-header">{{'dso-selector.'+ action + '.' + objectType.toString().toLowerCase() + '.head' | translate}}
|
||||
<button type="button" class="close" (click)="close()" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<button class="btn btn-outline-primary btn-lg btn-block" (click)="selectObject(undefined)">{{'dso-selector.create.community.top-level' | translate}}</button>
|
||||
<h3 class="position-relative py-1 my-3 font-weight-normal">
|
||||
<hr>
|
||||
<div id="create-community-or-separator" class="text-center position-absolute w-100">
|
||||
<span class="px-4 bg-white">or</span>
|
||||
</div>
|
||||
</h3>
|
||||
|
||||
<h5 class="px-2">{{'dso-selector.create.community.sub-level' | translate}}</h5>
|
||||
<ds-dso-selector [currentDSOId]="dsoRD?.payload.uuid" [types]="selectorTypes" (onSelect)="selectObject($event)"></ds-dso-selector>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,3 @@
|
||||
#create-community-or-separator {
|
||||
top: 0;
|
||||
}
|
@@ -0,0 +1,14 @@
|
||||
import { Component } from '@angular/core';
|
||||
import {
|
||||
CreateCommunityParentSelectorComponent as BaseComponent
|
||||
} from '../../../../../../../app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-create-community-parent-selector',
|
||||
// styleUrls: ['./create-community-parent-selector.component.scss'],
|
||||
styleUrls: ['../../../../../../../app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.scss'],
|
||||
// templateUrl: './create-community-parent-selector.component.html',
|
||||
templateUrl: '../../../../../../../app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.html',
|
||||
})
|
||||
export class CreateCommunityParentSelectorComponent extends BaseComponent {
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
<div>
|
||||
<div class="modal-header">{{'dso-selector.'+ action + '.' + objectType.toString().toLowerCase() + '.head' | translate}}
|
||||
<button type="button" class="close" (click)="close()" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div [innerHTML]="'dso-selector.create.item.intro' | translate"></div>
|
||||
<h5 *ngIf="header" class="px-2">{{header | translate}}</h5>
|
||||
<ds-authorized-collection-selector [currentDSOId]="dsoRD?.payload.uuid"
|
||||
[entityType]="entityType"
|
||||
[types]="selectorTypes"
|
||||
(onSelect)="selectObject($event)"></ds-authorized-collection-selector>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,13 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {
|
||||
CreateItemParentSelectorComponent as BaseComponent
|
||||
} from '../../../../../../../app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-create-item-parent-selector',
|
||||
// styleUrls: ['./create-item-parent-selector.component.scss'],
|
||||
// templateUrl: './create-item-parent-selector.component.html',
|
||||
templateUrl: '../../../../../../../app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.html',
|
||||
})
|
||||
export class CreateItemParentSelectorComponent extends BaseComponent {
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
<div>
|
||||
<div class="modal-header">{{'dso-selector.'+ action + '.' + objectType.toString().toLowerCase() + '.head' | translate}}
|
||||
<button type="button" class="close" (click)="close()" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<h5 *ngIf="header" class="px-2">{{header | translate}}</h5>
|
||||
<ds-dso-selector [currentDSOId]="dsoRD?.payload.uuid" [types]="selectorTypes" (onSelect)="selectObject($event)"></ds-dso-selector>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,13 @@
|
||||
import { Component } from '@angular/core';
|
||||
import {
|
||||
EditCollectionSelectorComponent as BaseComponent
|
||||
} from '../../../../../../../app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-edit-collection-selector',
|
||||
// styleUrls: ['./edit-collection-selector.component.scss'],
|
||||
// templateUrl: './edit-collection-selector.component.html',
|
||||
templateUrl: '../../../../../../../app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.html',
|
||||
})
|
||||
export class EditCollectionSelectorComponent extends BaseComponent {
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
<div>
|
||||
<div class="modal-header">{{'dso-selector.'+ action + '.' + objectType.toString().toLowerCase() + '.head' | translate}}
|
||||
<button type="button" class="close" (click)="close()" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<h5 *ngIf="header" class="px-2">{{header | translate}}</h5>
|
||||
<ds-dso-selector [currentDSOId]="dsoRD?.payload.uuid" [types]="selectorTypes" (onSelect)="selectObject($event)"></ds-dso-selector>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,13 @@
|
||||
import { Component } from '@angular/core';
|
||||
import {
|
||||
EditCommunitySelectorComponent as BaseComponent
|
||||
} from '../../../../../../../app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-edit-item-selector',
|
||||
// styleUrls: ['./edit-community-selector.component.scss'],
|
||||
// templateUrl: './edit-community-selector.component.html',
|
||||
templateUrl: '../../../../../../../app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.html',
|
||||
})
|
||||
export class EditCommunitySelectorComponent extends BaseComponent {
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
<div>
|
||||
<div class="modal-header">{{'dso-selector.'+ action + '.' + objectType.toString().toLowerCase() + '.head' | translate}}
|
||||
<button type="button" class="close" (click)="close()" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<h5 *ngIf="header" class="px-2">{{header | translate}}</h5>
|
||||
<ds-dso-selector [currentDSOId]="dsoRD?.payload.uuid" [types]="selectorTypes" (onSelect)="selectObject($event)"></ds-dso-selector>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,13 @@
|
||||
import { Component } from '@angular/core';
|
||||
import {
|
||||
EditItemSelectorComponent as BaseComponent
|
||||
} from 'src/app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-edit-item-selector',
|
||||
// styleUrls: ['./edit-item-selector.component.scss'],
|
||||
// templateUrl: './edit-item-selector.component.html',
|
||||
templateUrl: '../../../../../../../app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.html',
|
||||
})
|
||||
export class EditItemSelectorComponent extends BaseComponent {
|
||||
}
|
@@ -21,6 +21,24 @@ import {
|
||||
} from './app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component';
|
||||
import { UntypedItemComponent } from './app/item-page/simple/item-types/untyped-item/untyped-item.component';
|
||||
import { ItemSharedModule } from '../../app/item-page/item-shared.module';
|
||||
import {
|
||||
CreateCollectionParentSelectorComponent
|
||||
} from './app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component';
|
||||
import {
|
||||
CreateCommunityParentSelectorComponent
|
||||
} from './app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component';
|
||||
import {
|
||||
CreateItemParentSelectorComponent
|
||||
} from './app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component';
|
||||
import {
|
||||
EditCollectionSelectorComponent
|
||||
} from './app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component';
|
||||
import {
|
||||
EditCommunitySelectorComponent
|
||||
} from './app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component';
|
||||
import {
|
||||
EditItemSelectorComponent
|
||||
} from './app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component';
|
||||
|
||||
/**
|
||||
* Add components that use a custom decorator to ENTRY_COMPONENTS as well as DECLARATIONS.
|
||||
@@ -41,6 +59,12 @@ const DECLARATIONS = [
|
||||
HeaderNavbarWrapperComponent,
|
||||
NavbarComponent,
|
||||
FooterComponent,
|
||||
CreateCollectionParentSelectorComponent,
|
||||
CreateCommunityParentSelectorComponent,
|
||||
CreateItemParentSelectorComponent,
|
||||
EditCollectionSelectorComponent,
|
||||
EditCommunitySelectorComponent,
|
||||
EditItemSelectorComponent,
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
Reference in New Issue
Block a user