Merge remote-tracking branch 'origin/main' into w2p-92900_Admin_options_dont_appear_after_Shibboleth_authentication_PR

This commit is contained in:
Yury Bondarenko
2022-09-08 17:53:04 +02:00
44 changed files with 11518 additions and 8727 deletions

16
.gitattributes vendored
View File

@@ -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 * 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
View File

0
src/app/app.module.ts Executable file → Normal file
View File

View File

@@ -16,24 +16,24 @@ import { filter, find, map, take } from 'rxjs/operators';
import { hasValue } from './shared/empty.util'; import { hasValue } from './shared/empty.util';
import { FeatureID } from './core/data/feature-authorization/feature-id'; import { FeatureID } from './core/data/feature-authorization/feature-id';
import { import {
CreateCommunityParentSelectorComponent ThemedCreateCommunityParentSelectorComponent
} from './shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component'; } 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 { OnClickMenuItemModel } from './shared/menu/menu-item/models/onclick.model';
import { import {
CreateCollectionParentSelectorComponent ThemedCreateCollectionParentSelectorComponent
} from './shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component'; } from './shared/dso-selector/modal-wrappers/create-collection-parent-selector/themed-create-collection-parent-selector.component';
import { import {
CreateItemParentSelectorComponent ThemedCreateItemParentSelectorComponent
} from './shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component'; } from './shared/dso-selector/modal-wrappers/create-item-parent-selector/themed-create-item-parent-selector.component';
import { import {
EditCommunitySelectorComponent ThemedEditCommunitySelectorComponent
} from './shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component'; } from './shared/dso-selector/modal-wrappers/edit-community-selector/themed-edit-community-selector.component';
import { import {
EditCollectionSelectorComponent ThemedEditCollectionSelectorComponent
} from './shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component'; } from './shared/dso-selector/modal-wrappers/edit-collection-selector/themed-edit-collection-selector.component';
import { import {
EditItemSelectorComponent ThemedEditItemSelectorComponent
} from './shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component'; } from './shared/dso-selector/modal-wrappers/edit-item-selector/themed-edit-item-selector.component';
import { import {
ExportMetadataSelectorComponent ExportMetadataSelectorComponent
} from './shared/dso-selector/modal-wrappers/export-metadata-selector/export-metadata-selector.component'; } 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, type: MenuItemType.ONCLICK,
text: 'menu.section.new_community', text: 'menu.section.new_community',
function: () => { function: () => {
this.modalService.open(CreateCommunityParentSelectorComponent); this.modalService.open(ThemedCreateCommunityParentSelectorComponent);
} }
} as OnClickMenuItemModel, } as OnClickMenuItemModel,
}, },
@@ -201,7 +201,7 @@ export class MenuResolver implements Resolve<boolean> {
type: MenuItemType.ONCLICK, type: MenuItemType.ONCLICK,
text: 'menu.section.new_collection', text: 'menu.section.new_collection',
function: () => { function: () => {
this.modalService.open(CreateCollectionParentSelectorComponent); this.modalService.open(ThemedCreateCollectionParentSelectorComponent);
} }
} as OnClickMenuItemModel, } as OnClickMenuItemModel,
}, },
@@ -214,7 +214,7 @@ export class MenuResolver implements Resolve<boolean> {
type: MenuItemType.ONCLICK, type: MenuItemType.ONCLICK,
text: 'menu.section.new_item', text: 'menu.section.new_item',
function: () => { function: () => {
this.modalService.open(CreateItemParentSelectorComponent); this.modalService.open(ThemedCreateItemParentSelectorComponent);
} }
} as OnClickMenuItemModel, } as OnClickMenuItemModel,
}, },
@@ -263,7 +263,7 @@ export class MenuResolver implements Resolve<boolean> {
type: MenuItemType.ONCLICK, type: MenuItemType.ONCLICK,
text: 'menu.section.edit_community', text: 'menu.section.edit_community',
function: () => { function: () => {
this.modalService.open(EditCommunitySelectorComponent); this.modalService.open(ThemedEditCommunitySelectorComponent);
} }
} as OnClickMenuItemModel, } as OnClickMenuItemModel,
}, },
@@ -276,7 +276,7 @@ export class MenuResolver implements Resolve<boolean> {
type: MenuItemType.ONCLICK, type: MenuItemType.ONCLICK,
text: 'menu.section.edit_collection', text: 'menu.section.edit_collection',
function: () => { function: () => {
this.modalService.open(EditCollectionSelectorComponent); this.modalService.open(ThemedEditCollectionSelectorComponent);
} }
} as OnClickMenuItemModel, } as OnClickMenuItemModel,
}, },
@@ -289,7 +289,7 @@ export class MenuResolver implements Resolve<boolean> {
type: MenuItemType.ONCLICK, type: MenuItemType.ONCLICK,
text: 'menu.section.edit_item', text: 'menu.section.edit_item',
function: () => { function: () => {
this.modalService.open(EditItemSelectorComponent); this.modalService.open(ThemedEditItemSelectorComponent);
} }
} as OnClickMenuItemModel, } as OnClickMenuItemModel,
}, },

View File

@@ -1,53 +1,99 @@
<div class="container" *ngVar="(processRD$ | async)?.payload as process"> <div class="container" *ngVar="(processRD$ | async)?.payload as process">
<div class="d-flex"> <div class="d-flex">
<h2 class="flex-grow-1">{{'process.detail.title' | translate:{id: process?.processId, name: process?.scriptName} }}</h2> <h2 class="flex-grow-1">{{'process.detail.title' | translate:{
<div> id: process?.processId,
<button class="btn btn-lg btn-success " routerLink="/processes/new" [queryParams]="{id: process?.processId}"><i class="fas fa-plus pr-2"></i>{{'process.detail.create' | translate}}</button> name: process?.scriptName
</div> } }}</h2>
</div> </div>
<ds-process-detail-field id="process-name" [title]="'process.detail.script'"> <ds-process-detail-field id="process-name" [title]="'process.detail.script'">
<div>{{ process?.scriptName }}</div> <div>{{ process?.scriptName }}</div>
</ds-process-detail-field> </ds-process-detail-field>
<ds-process-detail-field *ngIf="process?.parameters && process?.parameters?.length > 0" id="process-arguments" [title]="'process.detail.arguments'"> <ds-process-detail-field *ngIf="process?.parameters && process?.parameters?.length > 0" id="process-arguments"
[title]="'process.detail.arguments'">
<div *ngFor="let argument of process?.parameters">{{ argument?.name }} {{ argument?.value }}</div> <div *ngFor="let argument of process?.parameters">{{ argument?.name }} {{ argument?.value }}</div>
</ds-process-detail-field> </ds-process-detail-field>
<div *ngVar="(filesRD$ | async)?.payload?.page as files"> <div *ngVar="(filesRD$ | async)?.payload?.page as files">
<ds-process-detail-field *ngIf="files && files?.length > 0" id="process-files" [title]="'process.detail.output-files'"> <ds-process-detail-field *ngIf="files && files?.length > 0" id="process-files"
<ds-file-download-link *ngFor="let file of files; let last=last;" [bitstream]="file"> [title]="'process.detail.output-files'">
<span>{{getFileName(file)}}</span> <ds-file-download-link *ngFor="let file of files; let last=last;" [bitstream]="file">
<span>({{(file?.sizeBytes) | dsFileSize }})</span> <span>{{getFileName(file)}}</span>
</ds-file-download-link> <span>({{(file?.sizeBytes) | dsFileSize }})</span>
</ds-file-download-link>
</ds-process-detail-field> </ds-process-detail-field>
</div> </div>
<ds-process-detail-field *ngIf="process && process.startTime" id="process-start-time" [title]="'process.detail.start-time' | translate"> <ds-process-detail-field *ngIf="process && process.startTime" id="process-start-time"
[title]="'process.detail.start-time' | translate">
<div>{{ process.startTime | date:dateFormat:'UTC' }}</div> <div>{{ process.startTime | date:dateFormat:'UTC' }}</div>
</ds-process-detail-field> </ds-process-detail-field>
<ds-process-detail-field *ngIf="process && process.endTime" id="process-end-time" [title]="'process.detail.end-time' | translate"> <ds-process-detail-field *ngIf="process && process.endTime" id="process-end-time"
[title]="'process.detail.end-time' | translate">
<div>{{ process.endTime | date:dateFormat:'UTC' }}</div> <div>{{ process.endTime | date:dateFormat:'UTC' }}</div>
</ds-process-detail-field> </ds-process-detail-field>
<ds-process-detail-field *ngIf="process && process.processStatus" id="process-status" [title]="'process.detail.status' | translate"> <ds-process-detail-field *ngIf="process && process.processStatus" id="process-status"
[title]="'process.detail.status' | translate">
<div>{{ process.processStatus }}</div> <div>{{ process.processStatus }}</div>
</ds-process-detail-field> </ds-process-detail-field>
<ds-process-detail-field *ngIf="isProcessFinished(process)" id="process-output" [title]="'process.detail.output'"> <ds-process-detail-field *ngIf="isProcessFinished(process)" id="process-output" [title]="'process.detail.output'">
<button *ngIf="!showOutputLogs && process?._links?.output?.href != undefined" id="showOutputButton" class="btn btn-primary" (click)="showProcessOutputLogs()"> <button *ngIf="!showOutputLogs && process?._links?.output?.href != undefined" id="showOutputButton"
{{ 'process.detail.logs.button' | translate }} class="btn btn-primary" (click)="showProcessOutputLogs()">
</button> {{ 'process.detail.logs.button' | translate }}
<ds-themed-loading *ngIf="retrievingOutputLogs$ | async" class="ds-themed-loading" message="{{ 'process.detail.logs.loading' | translate }}"></ds-themed-loading> </button>
<pre class="font-weight-bold text-secondary bg-light p-3" <ds-themed-loading *ngIf="retrievingOutputLogs$ | async" class="ds-themed-loading"
*ngIf="showOutputLogs && (outputLogs$ | async)?.length > 0">{{ (outputLogs$ | async) }}</pre> message="{{ 'process.detail.logs.loading' | translate }}"></ds-themed-loading>
<p id="no-output-logs-message" *ngIf="(!(retrievingOutputLogs$ | async) && showOutputLogs) <pre class="font-weight-bold text-secondary bg-light p-3"
*ngIf="showOutputLogs && (outputLogs$ | async)?.length > 0">{{ (outputLogs$ | async) }}</pre>
<p id="no-output-logs-message" *ngIf="(!(retrievingOutputLogs$ | async) && showOutputLogs)
&& !(outputLogs$ | async) || (outputLogs$ | async)?.length == 0 || !process._links.output"> && !(outputLogs$ | async) || (outputLogs$ | async)?.length == 0 || !process._links.output">
{{ 'process.detail.logs.none' | translate }} {{ 'process.detail.logs.none' | translate }}
</p> </p>
</ds-process-detail-field>
<ds-process-detail-field id="process-actions" [title]="'process.detail.actions'">
<button class="btn btn-success mr-2" routerLink="/processes/new" [queryParams]="{id: process?.processId}"><i
class="fas fa-plus pr-2"></i>{{'process.detail.create' | translate}}</button>
<button *ngIf="isProcessFinished(process)" id="delete" class="btn btn-danger"
(click)="openDeleteModal(deleteModal)">
<i class="fas fa-trash pr-2"></i>{{ 'process.detail.delete.button' | translate }}
</button>
</ds-process-detail-field> </ds-process-detail-field>
<div style="text-align: right;"> <div style="text-align: right;">
<a class="btn btn-outline-secondary mt-3" [routerLink]="'/processes'">{{'process.detail.back' | translate}}</a> <a class="btn btn-outline-secondary mt-3" [routerLink]="'/processes'">{{'process.detail.back' | translate}}</a>
</div> </div>
</div> </div>
<ng-template #deleteModal >
<div *ngVar="(processRD$ | async)?.payload as process">
<div class="modal-header">
<div>
<h4>{{'process.detail.delete.header' | translate }}</h4>
</div>
<button type="button" class="close"
(click)="closeModal()" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<div>{{'process.detail.delete.body' | translate }}</div>
<div class="mt-4">
<button class="btn btn-primary mr-2" (click)="closeModal()">{{'process.detail.delete.cancel' | translate}}</button>
<button id="delete-confirm" class="btn btn-danger"
(click)="deleteProcess(process)">{{ 'process.detail.delete.confirm' | translate }}
</button>
</div>
</div>
</div>
</ng-template>

View File

@@ -19,15 +19,23 @@ import { RouterTestingModule } from '@angular/router/testing';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { ProcessDetailFieldComponent } from './process-detail-field/process-detail-field.component'; import { ProcessDetailFieldComponent } from './process-detail-field/process-detail-field.component';
import { Process } from '../processes/process.model'; import { Process } from '../processes/process.model';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { of as observableOf } from 'rxjs'; import { of as observableOf } from 'rxjs';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { FileSizePipe } from '../../shared/utils/file-size-pipe'; import { FileSizePipe } from '../../shared/utils/file-size-pipe';
import { Bitstream } from '../../core/shared/bitstream.model'; import { Bitstream } from '../../core/shared/bitstream.model';
import { ProcessDataService } from '../../core/data/processes/process-data.service'; import { ProcessDataService } from '../../core/data/processes/process-data.service';
import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import {
createFailedRemoteDataObject$,
createSuccessfulRemoteDataObject,
createSuccessfulRemoteDataObject$
} from '../../shared/remote-data.utils';
import { createPaginatedList } from '../../shared/testing/utils.test'; import { createPaginatedList } from '../../shared/testing/utils.test';
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { getProcessListRoute } from '../process-page-routing.paths';
describe('ProcessDetailComponent', () => { describe('ProcessDetailComponent', () => {
let component: ProcessDetailComponent; let component: ProcessDetailComponent;
@@ -44,6 +52,11 @@ describe('ProcessDetailComponent', () => {
let processOutput; let processOutput;
let modalService;
let notificationsService;
let router;
function init() { function init() {
processOutput = 'Process Started'; processOutput = 'Process Started';
process = Object.assign(new Process(), { process = Object.assign(new Process(), {
@@ -93,7 +106,8 @@ describe('ProcessDetailComponent', () => {
} }
}); });
processService = jasmine.createSpyObj('processService', { processService = jasmine.createSpyObj('processService', {
getFiles: createSuccessfulRemoteDataObject$(createPaginatedList(files)) getFiles: createSuccessfulRemoteDataObject$(createPaginatedList(files)),
delete: createSuccessfulRemoteDataObject$(null)
}); });
bitstreamDataService = jasmine.createSpyObj('bitstreamDataService', { bitstreamDataService = jasmine.createSpyObj('bitstreamDataService', {
findByHref: createSuccessfulRemoteDataObject$(logBitstream) findByHref: createSuccessfulRemoteDataObject$(logBitstream)
@@ -104,13 +118,23 @@ describe('ProcessDetailComponent', () => {
httpClient = jasmine.createSpyObj('httpClient', { httpClient = jasmine.createSpyObj('httpClient', {
get: observableOf(processOutput) get: observableOf(processOutput)
}); });
modalService = jasmine.createSpyObj('modalService', {
open: {}
});
notificationsService = new NotificationsServiceStub();
router = jasmine.createSpyObj('router', {
navigateByUrl:{}
});
} }
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
init(); init();
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [ProcessDetailComponent, ProcessDetailFieldComponent, VarDirective, FileSizePipe], declarations: [ProcessDetailComponent, ProcessDetailFieldComponent, VarDirective, FileSizePipe],
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])], imports: [TranslateModule.forRoot()],
providers: [ providers: [
{ {
provide: ActivatedRoute, provide: ActivatedRoute,
@@ -121,6 +145,9 @@ describe('ProcessDetailComponent', () => {
{ provide: DSONameService, useValue: nameService }, { provide: DSONameService, useValue: nameService },
{ provide: AuthService, useValue: new AuthServiceMock() }, { provide: AuthService, useValue: new AuthServiceMock() },
{ provide: HttpClient, useValue: httpClient }, { provide: HttpClient, useValue: httpClient },
{ provide: NgbModal, useValue: modalService },
{ provide: NotificationsService, useValue: notificationsService },
{ provide: Router, useValue: router },
], ],
schemas: [CUSTOM_ELEMENTS_SCHEMA] schemas: [CUSTOM_ELEMENTS_SCHEMA]
}).compileComponents(); }).compileComponents();
@@ -207,4 +234,34 @@ describe('ProcessDetailComponent', () => {
}); });
}); });
describe('openDeleteModal', () => {
it('should open the modal', () => {
component.openDeleteModal({});
expect(modalService.open).toHaveBeenCalledWith({});
});
});
describe('deleteProcess', () => {
it('should delete the process and navigate back to the overview page on success', () => {
spyOn(component, 'closeModal');
component.deleteProcess(process);
expect(processService.delete).toHaveBeenCalledWith(process.processId);
expect(notificationsService.success).toHaveBeenCalled();
expect(component.closeModal).toHaveBeenCalled();
expect(router.navigateByUrl).toHaveBeenCalledWith(getProcessListRoute());
});
it('should delete the process and not navigate on error', () => {
(processService.delete as jasmine.Spy).and.returnValue(createFailedRemoteDataObject$());
spyOn(component, 'closeModal');
component.deleteProcess(process);
expect(processService.delete).toHaveBeenCalledWith(process.processId);
expect(notificationsService.error).toHaveBeenCalled();
expect(component.closeModal).not.toHaveBeenCalled();
expect(router.navigateByUrl).not.toHaveBeenCalled();
});
});
}); });

View File

@@ -12,8 +12,9 @@ import { RemoteData } from '../../core/data/remote-data';
import { Bitstream } from '../../core/shared/bitstream.model'; import { Bitstream } from '../../core/shared/bitstream.model';
import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { import {
getFirstSucceededRemoteDataPayload, getFirstCompletedRemoteData,
getFirstSucceededRemoteData getFirstSucceededRemoteData,
getFirstSucceededRemoteDataPayload
} from '../../core/shared/operators'; } from '../../core/shared/operators';
import { URLCombiner } from '../../core/url-combiner/url-combiner'; import { URLCombiner } from '../../core/url-combiner/url-combiner';
import { AlertType } from '../../shared/alert/aletr-type'; import { AlertType } from '../../shared/alert/aletr-type';
@@ -21,6 +22,10 @@ import { hasValue } from '../../shared/empty.util';
import { ProcessStatus } from '../processes/process-status.model'; import { ProcessStatus } from '../processes/process-status.model';
import { Process } from '../processes/process.model'; import { Process } from '../processes/process.model';
import { redirectOn4xx } from '../../core/shared/authorized.operators'; import { redirectOn4xx } from '../../core/shared/authorized.operators';
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
import { getProcessListRoute } from '../process-page-routing.paths';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core';
@Component({ @Component({
selector: 'ds-process-detail', selector: 'ds-process-detail',
@@ -71,6 +76,11 @@ export class ProcessDetailComponent implements OnInit {
*/ */
dateFormat = 'yyyy-MM-dd HH:mm:ss ZZZZ'; dateFormat = 'yyyy-MM-dd HH:mm:ss ZZZZ';
/**
* Reference to NgbModal
*/
protected modalRef: NgbModalRef;
constructor(protected route: ActivatedRoute, constructor(protected route: ActivatedRoute,
protected router: Router, protected router: Router,
protected processService: ProcessDataService, protected processService: ProcessDataService,
@@ -78,7 +88,11 @@ export class ProcessDetailComponent implements OnInit {
protected nameService: DSONameService, protected nameService: DSONameService,
private zone: NgZone, private zone: NgZone,
protected authService: AuthService, protected authService: AuthService,
protected http: HttpClient) { protected http: HttpClient,
protected modalService: NgbModal,
protected notificationsService: NotificationsService,
protected translateService: TranslateService
) {
} }
/** /**
@@ -172,4 +186,36 @@ export class ProcessDetailComponent implements OnInit {
|| process.processStatus.toString() === ProcessStatus[ProcessStatus.FAILED].toString())); || process.processStatus.toString() === ProcessStatus[ProcessStatus.FAILED].toString()));
} }
/**
* Delete the current process
* @param process
*/
deleteProcess(process: Process) {
this.processService.delete(process.processId).pipe(
getFirstCompletedRemoteData()
).subscribe((rd) => {
if (rd.hasSucceeded) {
this.notificationsService.success(this.translateService.get('process.detail.delete.success'));
this.closeModal();
this.router.navigateByUrl(getProcessListRoute());
} else {
this.notificationsService.error(this.translateService.get('process.detail.delete.error'));
}
});
}
/**
* Open a given modal.
* @param content - the modal content.
*/
openDeleteModal(content) {
this.modalRef = this.modalService.open(content);
}
/**
* Close the modal.
*/
closeModal() {
this.modalRef.close();
}
} }

View File

@@ -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']);
});
});
});

View 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);
});
}
}

View File

@@ -1,7 +1,19 @@
<div class="container"> <div class="container">
<div class="d-flex"> <div class="d-flex">
<h2 class="flex-grow-1">{{'process.overview.title' | translate}}</h2> <h2 class="flex-grow-1">{{'process.overview.title' | translate}}</h2>
<button class="btn btn-lg btn-success " routerLink="/processes/new"><i class="fas fa-plus pr-2"></i>{{'process.overview.new' | translate}}</button> </div>
<div class="d-flex justify-content-end">
<button *ngIf="processBulkDeleteService.hasSelected()" class="btn btn-primary mr-2"
(click)="processBulkDeleteService.clearAllProcesses()"><i
class="fas fa-undo pr-2"></i>{{'process.overview.delete.clear' | translate }}
</button>
<button *ngIf="processBulkDeleteService.hasSelected()" class="btn btn-danger mr-2"
(click)="openDeleteModal(deleteModal)"><i
class="fas fa-trash pr-2"></i>{{'process.overview.delete' | translate: {count: processBulkDeleteService.getAmountOfSelectedProcesses()} }}
</button>
<button class="btn btn-success" routerLink="/processes/new"><i
class="fas fa-plus pr-2"></i>{{'process.overview.new' | translate}}</button>
</div> </div>
<ds-pagination *ngIf="(processesRD$ | async)?.payload?.totalElements > 0" <ds-pagination *ngIf="(processesRD$ | async)?.payload?.totalElements > 0"
[paginationOptions]="pageConfig" [paginationOptions]="pageConfig"
@@ -19,19 +31,61 @@
<th scope="col">{{'process.overview.table.start' | translate}}</th> <th scope="col">{{'process.overview.table.start' | translate}}</th>
<th scope="col">{{'process.overview.table.finish' | translate}}</th> <th scope="col">{{'process.overview.table.finish' | translate}}</th>
<th scope="col">{{'process.overview.table.status' | translate}}</th> <th scope="col">{{'process.overview.table.status' | translate}}</th>
<th scope="col">{{'process.overview.table.actions' | translate}}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr *ngFor="let process of (processesRD$ | async)?.payload?.page"> <tr *ngFor="let process of (processesRD$ | async)?.payload?.page"
[class.table-danger]="processBulkDeleteService.isToBeDeleted(process.processId)">
<td><a [routerLink]="['/processes/', process.processId]">{{process.processId}}</a></td> <td><a [routerLink]="['/processes/', process.processId]">{{process.processId}}</a></td>
<td><a [routerLink]="['/processes/', process.processId]">{{process.scriptName}}</a></td> <td><a [routerLink]="['/processes/', process.processId]">{{process.scriptName}}</a></td>
<td *ngVar="(getEpersonName(process.userId) | async) as ePersonName">{{ePersonName}}</td> <td *ngVar="(getEpersonName(process.userId) | async) as ePersonName">{{ePersonName}}</td>
<td>{{process.startTime | date:dateFormat:'UTC'}}</td> <td>{{process.startTime | date:dateFormat:'UTC'}}</td>
<td>{{process.endTime | date:dateFormat:'UTC'}}</td> <td>{{process.endTime | date:dateFormat:'UTC'}}</td>
<td>{{process.processStatus}}</td> <td>{{process.processStatus}}</td>
<td>
<button class="btn btn-outline-danger"
(click)="processBulkDeleteService.toggleDelete(process.processId)"><i
class="fas fa-trash"></i></button>
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>
</ds-pagination> </ds-pagination>
</div> </div>
<ng-template #deleteModal>
<div>
<div class="modal-header">
<div>
<h4>{{'process.overview.delete.header' | translate }}</h4>
</div>
<button type="button" class="close"
(click)="closeModal()" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<div *ngIf="!(processBulkDeleteService.isProcessing$() |async)">{{'process.overview.delete.body' | translate: {count: processBulkDeleteService.getAmountOfSelectedProcesses()} }}</div>
<div *ngIf="processBulkDeleteService.isProcessing$() |async" class="alert alert-info">
<span class="spinner-border spinner-border-sm spinner-button" role="status" aria-hidden="true"></span>
<span> {{ 'process.overview.delete.processing' | translate: {count: processBulkDeleteService.getAmountOfSelectedProcesses()} }}</span>
</div>
<div class="mt-4">
<button class="btn btn-primary mr-2" [disabled]="processBulkDeleteService.isProcessing$() |async"
(click)="closeModal()">{{'process.detail.delete.cancel' | translate}}</button>
<button id="delete-confirm" class="btn btn-danger"
[disabled]="processBulkDeleteService.isProcessing$() |async"
(click)="deleteSelected()">{{ 'process.overview.delete' | translate: {count: processBulkDeleteService.getAmountOfSelectedProcesses()} }}
</button>
</div>
</div>
</div>
</ng-template>

View File

@@ -1,5 +1,5 @@
import { ProcessOverviewComponent } from './process-overview.component'; import { ProcessOverviewComponent } from './process-overview.component';
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { VarDirective } from '../../shared/utils/var.directive'; import { VarDirective } from '../../shared/utils/var.directive';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { RouterTestingModule } from '@angular/router/testing'; import { RouterTestingModule } from '@angular/router/testing';
@@ -13,11 +13,11 @@ import { ProcessStatus } from '../processes/process-status.model';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { createPaginatedList } from '../../shared/testing/utils.test'; import { createPaginatedList } from '../../shared/testing/utils.test';
import { PaginationService } from '../../core/pagination/pagination.service'; import { PaginationService } from '../../core/pagination/pagination.service';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub'; import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
import { FindListOptions } from '../../core/data/find-list-options.model';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { BehaviorSubject } from 'rxjs';
import { ProcessBulkDeleteService } from './process-bulk-delete.service';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
describe('ProcessOverviewComponent', () => { describe('ProcessOverviewComponent', () => {
let component: ProcessOverviewComponent; let component: ProcessOverviewComponent;
@@ -30,6 +30,9 @@ describe('ProcessOverviewComponent', () => {
let processes: Process[]; let processes: Process[];
let ePerson: EPerson; let ePerson: EPerson;
let processBulkDeleteService;
let modalService;
const pipe = new DatePipe('en-US'); const pipe = new DatePipe('en-US');
function init() { function init() {
@@ -80,6 +83,29 @@ describe('ProcessOverviewComponent', () => {
}); });
paginationService = new PaginationServiceStub(); paginationService = new PaginationServiceStub();
processBulkDeleteService = jasmine.createSpyObj('processBulkDeleteService', {
clearAllProcesses: {},
deleteSelectedProcesses: {},
isProcessing$: new BehaviorSubject(false),
hasSelected: true,
isToBeDeleted: true,
toggleDelete: {},
getAmountOfSelectedProcesses: 5
});
(processBulkDeleteService.isToBeDeleted as jasmine.Spy).and.callFake((id) => {
if (id === 2) {
return true;
} else {
return false;
}
});
modalService = jasmine.createSpyObj('modalService', {
open: {}
});
} }
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
@@ -90,7 +116,9 @@ describe('ProcessOverviewComponent', () => {
providers: [ providers: [
{ provide: ProcessDataService, useValue: processService }, { provide: ProcessDataService, useValue: processService },
{ provide: EPersonDataService, useValue: ePersonService }, { provide: EPersonDataService, useValue: ePersonService },
{ provide: PaginationService, useValue: paginationService } { provide: PaginationService, useValue: paginationService },
{ provide: ProcessBulkDeleteService, useValue: processBulkDeleteService },
{ provide: NgbModal, useValue: modalService },
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).compileComponents(); }).compileComponents();
@@ -154,5 +182,71 @@ describe('ProcessOverviewComponent', () => {
expect(el.textContent).toContain(processes[index].processStatus); expect(el.textContent).toContain(processes[index].processStatus);
}); });
}); });
it('should display a delete button in the seventh column', () => {
rowElements.forEach((rowElement, index) => {
const el = rowElement.query(By.css('td:nth-child(7)'));
expect(el.nativeElement.innerHTML).toContain('fas fa-trash');
el.query(By.css('button')).triggerEventHandler('click', null);
expect(processBulkDeleteService.toggleDelete).toHaveBeenCalledWith(processes[index].processId);
});
});
it('should indicate a row that has been selected for deletion', () => {
const deleteRow = fixture.debugElement.query(By.css('.table-danger'));
expect(deleteRow.nativeElement.innerHTML).toContain('/processes/' + processes[1].processId);
});
});
describe('overview buttons', () => {
it('should show a button to clear selected processes when there are selected processes', () => {
const clearButton = fixture.debugElement.query(By.css('.btn-primary'));
expect(clearButton.nativeElement.innerHTML).toContain('process.overview.delete.clear');
clearButton.triggerEventHandler('click', null);
expect(processBulkDeleteService.clearAllProcesses).toHaveBeenCalled();
});
it('should not show a button to clear selected processes when there are no selected processes', () => {
(processBulkDeleteService.hasSelected as jasmine.Spy).and.returnValue(false);
fixture.detectChanges();
const clearButton = fixture.debugElement.query(By.css('.btn-primary'));
expect(clearButton).toBeNull();
});
it('should show a button to open the delete modal when there are selected processes', () => {
spyOn(component, 'openDeleteModal');
const deleteButton = fixture.debugElement.query(By.css('.btn-danger'));
expect(deleteButton.nativeElement.innerHTML).toContain('process.overview.delete');
deleteButton.triggerEventHandler('click', null);
expect(component.openDeleteModal).toHaveBeenCalled();
});
it('should not show a button to clear selected processes when there are no selected processes', () => {
(processBulkDeleteService.hasSelected as jasmine.Spy).and.returnValue(false);
fixture.detectChanges();
const deleteButton = fixture.debugElement.query(By.css('.btn-danger'));
expect(deleteButton).toBeNull();
});
});
describe('openDeleteModal', () => {
it('should open the modal', () => {
component.openDeleteModal({});
expect(modalService.open).toHaveBeenCalledWith({});
});
});
describe('deleteSelected', () => {
it('should call the deleteSelectedProcesses method on the processBulkDeleteService and close the modal when processing is done', () => {
spyOn(component, 'closeModal');
spyOn(component, 'setProcesses');
component.deleteSelected();
expect(processBulkDeleteService.deleteSelectedProcesses).toHaveBeenCalled();
expect(component.closeModal).toHaveBeenCalled();
expect(component.setProcesses).toHaveBeenCalled();
});
}); });
}); });

View File

@@ -1,5 +1,5 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnDestroy, OnInit } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable, Subscription } from 'rxjs';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { PaginatedList } from '../../core/data/paginated-list.model'; import { PaginatedList } from '../../core/data/paginated-list.model';
import { Process } from '../processes/process.model'; import { Process } from '../processes/process.model';
@@ -11,6 +11,9 @@ import { map, switchMap } from 'rxjs/operators';
import { ProcessDataService } from '../../core/data/processes/process-data.service'; import { ProcessDataService } from '../../core/data/processes/process-data.service';
import { PaginationService } from '../../core/pagination/pagination.service'; import { PaginationService } from '../../core/pagination/pagination.service';
import { FindListOptions } from '../../core/data/find-list-options.model'; import { FindListOptions } from '../../core/data/find-list-options.model';
import { ProcessBulkDeleteService } from './process-bulk-delete.service';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { hasValue } from '../../shared/empty.util';
@Component({ @Component({
selector: 'ds-process-overview', selector: 'ds-process-overview',
@@ -19,7 +22,7 @@ import { FindListOptions } from '../../core/data/find-list-options.model';
/** /**
* Component displaying a list of all processes in a paginated table * Component displaying a list of all processes in a paginated table
*/ */
export class ProcessOverviewComponent implements OnInit { export class ProcessOverviewComponent implements OnInit, OnDestroy {
/** /**
* List of all processes * List of all processes
@@ -46,13 +49,22 @@ export class ProcessOverviewComponent implements OnInit {
*/ */
dateFormat = 'yyyy-MM-dd HH:mm:ss'; dateFormat = 'yyyy-MM-dd HH:mm:ss';
processesToDelete: string[] = [];
private modalRef: any;
isProcessingSub: Subscription;
constructor(protected processService: ProcessDataService, constructor(protected processService: ProcessDataService,
protected paginationService: PaginationService, protected paginationService: PaginationService,
protected ePersonService: EPersonDataService) { protected ePersonService: EPersonDataService,
protected modalService: NgbModal,
public processBulkDeleteService: ProcessBulkDeleteService,
) {
} }
ngOnInit(): void { ngOnInit(): void {
this.setProcesses(); this.setProcesses();
this.processBulkDeleteService.clearAllProcesses();
} }
/** /**
@@ -60,7 +72,7 @@ export class ProcessOverviewComponent implements OnInit {
*/ */
setProcesses() { setProcesses() {
this.processesRD$ = this.paginationService.getFindListOptions(this.pageConfig.id, this.config).pipe( this.processesRD$ = this.paginationService.getFindListOptions(this.pageConfig.id, this.config).pipe(
switchMap((config) => this.processService.findAll(config)) switchMap((config) => this.processService.findAll(config, true, false))
); );
} }
@@ -74,8 +86,46 @@ export class ProcessOverviewComponent implements OnInit {
map((eperson: EPerson) => eperson.name) map((eperson: EPerson) => eperson.name)
); );
} }
ngOnDestroy(): void { ngOnDestroy(): void {
this.paginationService.clearPagination(this.pageConfig.id); this.paginationService.clearPagination(this.pageConfig.id);
if (hasValue(this.isProcessingSub)) {
this.isProcessingSub.unsubscribe();
}
} }
/**
* Open a given modal.
* @param content - the modal content.
*/
openDeleteModal(content) {
this.modalRef = this.modalService.open(content);
}
/**
* Close the modal.
*/
closeModal() {
this.modalRef.close();
}
/**
* Delete the previously selected processes using the processBulkDeleteService
* After the deletion has started, subscribe to the isProcessing$ and when it is set
* to false after the processing is done, close the modal and reinitialise the processes
*/
deleteSelected() {
this.processBulkDeleteService.deleteSelectedProcesses();
if (hasValue(this.isProcessingSub)) {
this.isProcessingSub.unsubscribe();
}
this.isProcessingSub = this.processBulkDeleteService.isProcessing$()
.subscribe((isProcessing) => {
if (!isProcessing) {
this.closeModal();
this.setProcesses();
}
});
}
} }

View File

@@ -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');
}
}

View File

@@ -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');
}
}

View File

@@ -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');
}
}

View File

@@ -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');
}
}

View File

@@ -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');
}
}

View File

@@ -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');
}
}

View File

@@ -124,12 +124,21 @@ import { DSOSelectorComponent } from './dso-selector/dso-selector/dso-selector.c
import { import {
CreateCommunityParentSelectorComponent CreateCommunityParentSelectorComponent
} from './dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component'; } 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 { import {
CreateItemParentSelectorComponent CreateItemParentSelectorComponent
} from './dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component'; } 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 { import {
CreateCollectionParentSelectorComponent CreateCollectionParentSelectorComponent
} from './dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component'; } 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 { import {
CommunitySearchResultListElementComponent CommunitySearchResultListElementComponent
} from './object-list/search-result-list-element/community-search-result/community-search-result-list-element.component'; } from './object-list/search-result-list-element/community-search-result/community-search-result-list-element.component';
@@ -139,12 +148,21 @@ import {
import { import {
EditItemSelectorComponent EditItemSelectorComponent
} from './dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component'; } 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 { import {
EditCommunitySelectorComponent EditCommunitySelectorComponent
} from './dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component'; } 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 { import {
EditCollectionSelectorComponent EditCollectionSelectorComponent
} from './dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component'; } 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 { import {
ItemListPreviewComponent ItemListPreviewComponent
} from './object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component'; } from './object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component';
@@ -395,11 +413,17 @@ const COMPONENTS = [
DsoInputSuggestionsComponent, DsoInputSuggestionsComponent,
DSOSelectorComponent, DSOSelectorComponent,
CreateCommunityParentSelectorComponent, CreateCommunityParentSelectorComponent,
ThemedCreateCommunityParentSelectorComponent,
CreateCollectionParentSelectorComponent, CreateCollectionParentSelectorComponent,
ThemedCreateCollectionParentSelectorComponent,
CreateItemParentSelectorComponent, CreateItemParentSelectorComponent,
ThemedCreateItemParentSelectorComponent,
EditCommunitySelectorComponent, EditCommunitySelectorComponent,
ThemedEditCommunitySelectorComponent,
EditCollectionSelectorComponent, EditCollectionSelectorComponent,
ThemedEditCollectionSelectorComponent,
EditItemSelectorComponent, EditItemSelectorComponent,
ThemedEditItemSelectorComponent,
CommunitySearchResultListElementComponent, CommunitySearchResultListElementComponent,
CollectionSearchResultListElementComponent, CollectionSearchResultListElementComponent,
BrowseByComponent, BrowseByComponent,
@@ -491,11 +515,17 @@ const ENTRY_COMPONENTS = [
StartsWithDateComponent, StartsWithDateComponent,
StartsWithTextComponent, StartsWithTextComponent,
CreateCommunityParentSelectorComponent, CreateCommunityParentSelectorComponent,
ThemedCreateCommunityParentSelectorComponent,
CreateCollectionParentSelectorComponent, CreateCollectionParentSelectorComponent,
ThemedCreateCollectionParentSelectorComponent,
CreateItemParentSelectorComponent, CreateItemParentSelectorComponent,
ThemedCreateItemParentSelectorComponent,
EditCommunitySelectorComponent, EditCommunitySelectorComponent,
ThemedEditCommunitySelectorComponent,
EditCollectionSelectorComponent, EditCollectionSelectorComponent,
ThemedEditCollectionSelectorComponent,
EditItemSelectorComponent, EditItemSelectorComponent,
ThemedEditItemSelectorComponent,
PlainTextMetadataListElementComponent, PlainTextMetadataListElementComponent,
ItemMetadataListElementComponent, ItemMetadataListElementComponent,
MetadataRepresentationListElementComponent, MetadataRepresentationListElementComponent,

View File

@@ -24,9 +24,12 @@
</ds-viewable-collection> </ds-viewable-collection>
<ds-themed-loading *ngIf="(isLoading$ | async)" <ds-themed-loading *ngIf="(isLoading$ | async)"
message="{{'loading.search-results' | translate}}"></ds-themed-loading> 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> <ds-alert [type]="'alert-info'">{{ 'search.results.empty' | translate }}</ds-alert>
</div> </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> </ng-container>
</div> </div>
<div *ngIf="reload$.value.sourceId === ''" class="col-md-12"> <div *ngIf="reload$.value.sourceId === ''" class="col-md-12">

View File

@@ -19,9 +19,15 @@ import { VarDirective } from '../../shared/utils/var.directive';
import { routeServiceStub } from '../../shared/testing/route-service.stub'; import { routeServiceStub } from '../../shared/testing/route-service.stub';
import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model'; import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-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 { ExternalSourceEntry } from '../../core/shared/external-source-entry.model';
import { SubmissionImportExternalPreviewComponent } from './import-external-preview/submission-import-external-preview.component'; 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', () => { describe('SubmissionImportExternalComponent test suite', () => {
let comp: SubmissionImportExternalComponent; let comp: SubmissionImportExternalComponent;
@@ -44,7 +50,8 @@ describe('SubmissionImportExternalComponent test suite', () => {
beforeEach(waitForAsync (() => { beforeEach(waitForAsync (() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ imports: [
TranslateModule.forRoot() TranslateModule.forRoot(),
BrowserAnimationsModule
], ],
declarations: [ declarations: [
SubmissionImportExternalComponent, 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 // declare a test component

View File

@@ -2983,6 +2983,22 @@
"process.detail.create" : "Create similar process", "process.detail.create" : "Create similar process",
"process.detail.actions": "Actions",
"process.detail.delete.button": "Delete process",
"process.detail.delete.header": "Delete process",
"process.detail.delete.body": "Are you sure you want to delete the current process?",
"process.detail.delete.cancel": "Cancel",
"process.detail.delete.confirm": "Delete process",
"process.detail.delete.success": "The process was successfully deleted.",
"process.detail.delete.error": "Something went wrong when deleting the process",
"process.overview.table.finish" : "Finish time (UTC)", "process.overview.table.finish" : "Finish time (UTC)",
@@ -3003,6 +3019,25 @@
"process.overview.new": "New", "process.overview.new": "New",
"process.overview.table.actions": "Actions",
"process.overview.delete": "Delete {{count}} processes",
"process.overview.delete.clear": "Clear delete selection",
"process.overview.delete.processing": "{{count}} process(es) are being deleted. Please wait for the deletion to fully complete. Note that this can take a while.",
"process.overview.delete.body": "Are you sure you want to delete {{count}} process(es)?",
"process.overview.delete.header": "Delete processes",
"process.bulk.delete.error.head": "Error on deleteing process",
"process.bulk.delete.error.body": "The process with ID {{processId}} could not be deleted. The remaining processes will continue being deleted. ",
"process.bulk.delete.success": "{{count}} process(es) have been succesfully deleted",
"profile.breadcrumbs": "Update Profile", "profile.breadcrumbs": "Update Profile",
@@ -3583,6 +3618,7 @@
"search.results.view-result": "View", "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", "default.search.results.head": "Search Results",

File diff suppressed because it is too large Load Diff

View File

@@ -12,6 +12,9 @@ $fa-font-path: "^assets/fonts" !default;
/* Images */ /* Images */
$image-path: "../assets/images" !default; $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 **/ /** Bootstrap Variables **/
/* Colors */ /* Colors */
$gray-700: #495057 !default; // Bootstrap $gray-700 $gray-700: #495057 !default; // Bootstrap $gray-700

View File

@@ -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>

View File

@@ -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 {
}

View File

@@ -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>

View File

@@ -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 {
}

View File

@@ -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>

View File

@@ -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 {
}

View File

@@ -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>

View File

@@ -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 {
}

View File

@@ -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>

View File

@@ -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 {
}

View File

@@ -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>

View File

@@ -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 {
}

View File

@@ -21,6 +21,24 @@ import {
} from './app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component'; } 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 { UntypedItemComponent } from './app/item-page/simple/item-types/untyped-item/untyped-item.component';
import { ItemSharedModule } from '../../app/item-page/item-shared.module'; 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. * Add components that use a custom decorator to ENTRY_COMPONENTS as well as DECLARATIONS.
@@ -41,6 +59,12 @@ const DECLARATIONS = [
HeaderNavbarWrapperComponent, HeaderNavbarWrapperComponent,
NavbarComponent, NavbarComponent,
FooterComponent, FooterComponent,
CreateCollectionParentSelectorComponent,
CreateCommunityParentSelectorComponent,
CreateItemParentSelectorComponent,
EditCollectionSelectorComponent,
EditCommunitySelectorComponent,
EditItemSelectorComponent,
]; ];
@NgModule({ @NgModule({