mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
Merge pull request #1827 from 4Science/CST-6685
Create new batch export/import page to/from ZIP
This commit is contained in:
@@ -0,0 +1,35 @@
|
|||||||
|
<div class="container">
|
||||||
|
<h2 id="header">{{'admin.batch-import.page.header' | translate}}</h2>
|
||||||
|
<p>{{'admin.batch-import.page.help' | translate}}</p>
|
||||||
|
<p *ngIf="dso">
|
||||||
|
selected collection: <b>{{getDspaceObjectName()}}</b>
|
||||||
|
<a href="javascript:void(0)" (click)="removeDspaceObject()">{{'admin.batch-import.page.remove' | translate}}</a>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<button class="btn btn-primary" (click)="this.selectCollection();">{{'admin.metadata-import.page.button.select-collection' | translate}}</button>
|
||||||
|
</p>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" id="validateOnly" [(ngModel)]="validateOnly">
|
||||||
|
<label class="form-check-label" for="validateOnly">
|
||||||
|
{{'admin.metadata-import.page.validateOnly' | translate}}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<small id="validateOnlyHelpBlock" class="form-text text-muted">
|
||||||
|
{{'admin.batch-import.page.validateOnly.hint' | translate}}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ds-file-dropzone-no-uploader
|
||||||
|
(onFileAdded)="setFile($event)"
|
||||||
|
[dropMessageLabel]="'admin.batch-import.page.dropMsg'"
|
||||||
|
[dropMessageLabelReplacement]="'admin.batch-import.page.dropMsgReplace'">
|
||||||
|
</ds-file-dropzone-no-uploader>
|
||||||
|
|
||||||
|
<div class="space-children-mr">
|
||||||
|
<button class="btn btn-secondary" id="backButton"
|
||||||
|
(click)="this.onReturn();">{{'admin.metadata-import.page.button.return' | translate}}</button>
|
||||||
|
<button class="btn btn-primary" id="proceedButton"
|
||||||
|
(click)="this.importMetadata();">{{'admin.metadata-import.page.button.proceed' | translate}}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -0,0 +1,151 @@
|
|||||||
|
import { ComponentFixture, fakeAsync, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
import { BatchImportPageComponent } from './batch-import-page.component';
|
||||||
|
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
|
||||||
|
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { FileValueAccessorDirective } from '../../shared/utils/file-value-accessor.directive';
|
||||||
|
import { FileValidator } from '../../shared/utils/require-file.validator';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import {
|
||||||
|
BATCH_IMPORT_SCRIPT_NAME,
|
||||||
|
ScriptDataService
|
||||||
|
} from '../../core/data/processes/script-data.service';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { Location } from '@angular/common';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { ProcessParameter } from '../../process-page/processes/process-parameter.model';
|
||||||
|
|
||||||
|
describe('BatchImportPageComponent', () => {
|
||||||
|
let component: BatchImportPageComponent;
|
||||||
|
let fixture: ComponentFixture<BatchImportPageComponent>;
|
||||||
|
|
||||||
|
let notificationService: NotificationsServiceStub;
|
||||||
|
let scriptService: any;
|
||||||
|
let router;
|
||||||
|
let locationStub;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
notificationService = new NotificationsServiceStub();
|
||||||
|
scriptService = jasmine.createSpyObj('scriptService',
|
||||||
|
{
|
||||||
|
invoke: createSuccessfulRemoteDataObject$({ processId: '46' })
|
||||||
|
}
|
||||||
|
);
|
||||||
|
router = jasmine.createSpyObj('router', {
|
||||||
|
navigateByUrl: jasmine.createSpy('navigateByUrl')
|
||||||
|
});
|
||||||
|
locationStub = jasmine.createSpyObj('location', {
|
||||||
|
back: jasmine.createSpy('back')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
init();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
FormsModule,
|
||||||
|
TranslateModule.forRoot(),
|
||||||
|
RouterTestingModule.withRoutes([])
|
||||||
|
],
|
||||||
|
declarations: [BatchImportPageComponent, FileValueAccessorDirective, FileValidator],
|
||||||
|
providers: [
|
||||||
|
{ provide: NotificationsService, useValue: notificationService },
|
||||||
|
{ provide: ScriptDataService, useValue: scriptService },
|
||||||
|
{ provide: Router, useValue: router },
|
||||||
|
{ provide: Location, useValue: locationStub },
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(BatchImportPageComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if back button is pressed', () => {
|
||||||
|
beforeEach(fakeAsync(() => {
|
||||||
|
const proceed = fixture.debugElement.query(By.css('#backButton')).nativeElement;
|
||||||
|
proceed.click();
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
it('should do location.back', () => {
|
||||||
|
expect(locationStub.back).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if file is set', () => {
|
||||||
|
let fileMock: File;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fileMock = new File([''], 'filename.zip', { type: 'application/zip' });
|
||||||
|
component.setFile(fileMock);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if proceed button is pressed without validate only', () => {
|
||||||
|
beforeEach(fakeAsync(() => {
|
||||||
|
component.validateOnly = false;
|
||||||
|
const proceed = fixture.debugElement.query(By.css('#proceedButton')).nativeElement;
|
||||||
|
proceed.click();
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
it('metadata-import script is invoked with --zip fileName and the mockFile', () => {
|
||||||
|
const parameterValues: ProcessParameter[] = [
|
||||||
|
Object.assign(new ProcessParameter(), { name: '--zip', value: 'filename.zip' }),
|
||||||
|
];
|
||||||
|
parameterValues.push(Object.assign(new ProcessParameter(), { name: '--add' }));
|
||||||
|
expect(scriptService.invoke).toHaveBeenCalledWith(BATCH_IMPORT_SCRIPT_NAME, parameterValues, [fileMock]);
|
||||||
|
});
|
||||||
|
it('success notification is shown', () => {
|
||||||
|
expect(notificationService.success).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
it('redirected to process page', () => {
|
||||||
|
expect(router.navigateByUrl).toHaveBeenCalledWith('/processes/46');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if proceed button is pressed with validate only', () => {
|
||||||
|
beforeEach(fakeAsync(() => {
|
||||||
|
component.validateOnly = true;
|
||||||
|
const proceed = fixture.debugElement.query(By.css('#proceedButton')).nativeElement;
|
||||||
|
proceed.click();
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
it('metadata-import script is invoked with --zip fileName and the mockFile and -v validate-only', () => {
|
||||||
|
const parameterValues: ProcessParameter[] = [
|
||||||
|
Object.assign(new ProcessParameter(), { name: '--zip', value: 'filename.zip' }),
|
||||||
|
Object.assign(new ProcessParameter(), { name: '--add' }),
|
||||||
|
Object.assign(new ProcessParameter(), { name: '-v', value: true }),
|
||||||
|
];
|
||||||
|
expect(scriptService.invoke).toHaveBeenCalledWith(BATCH_IMPORT_SCRIPT_NAME, parameterValues, [fileMock]);
|
||||||
|
});
|
||||||
|
it('success notification is shown', () => {
|
||||||
|
expect(notificationService.success).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
it('redirected to process page', () => {
|
||||||
|
expect(router.navigateByUrl).toHaveBeenCalledWith('/processes/46');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if proceed is pressed; but script invoke fails', () => {
|
||||||
|
beforeEach(fakeAsync(() => {
|
||||||
|
jasmine.getEnv().allowRespy(true);
|
||||||
|
spyOn(scriptService, 'invoke').and.returnValue(createFailedRemoteDataObject$('Error', 500));
|
||||||
|
const proceed = fixture.debugElement.query(By.css('#proceedButton')).nativeElement;
|
||||||
|
proceed.click();
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
it('error notification is shown', () => {
|
||||||
|
expect(notificationService.error).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,124 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { Location } from '@angular/common';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { BATCH_IMPORT_SCRIPT_NAME, ScriptDataService } from '../../core/data/processes/script-data.service';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { ProcessParameter } from '../../process-page/processes/process-parameter.model';
|
||||||
|
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
|
||||||
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
|
import { Process } from '../../process-page/processes/process.model';
|
||||||
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
|
import { getProcessDetailRoute } from '../../process-page/process-page-routing.paths';
|
||||||
|
import {
|
||||||
|
ImportBatchSelectorComponent
|
||||||
|
} from '../../shared/dso-selector/modal-wrappers/import-batch-selector/import-batch-selector.component';
|
||||||
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { take } from 'rxjs/operators';
|
||||||
|
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||||
|
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-batch-import-page',
|
||||||
|
templateUrl: './batch-import-page.component.html'
|
||||||
|
})
|
||||||
|
export class BatchImportPageComponent {
|
||||||
|
/**
|
||||||
|
* The current value of the file
|
||||||
|
*/
|
||||||
|
fileObject: File;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The validate only flag
|
||||||
|
*/
|
||||||
|
validateOnly = true;
|
||||||
|
/**
|
||||||
|
* dso object for community or collection
|
||||||
|
*/
|
||||||
|
dso: DSpaceObject = null;
|
||||||
|
|
||||||
|
public constructor(private location: Location,
|
||||||
|
protected translate: TranslateService,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
private scriptDataService: ScriptDataService,
|
||||||
|
private router: Router,
|
||||||
|
private modalService: NgbModal,
|
||||||
|
private dsoNameService: DSONameService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set file
|
||||||
|
* @param file
|
||||||
|
*/
|
||||||
|
setFile(file) {
|
||||||
|
this.fileObject = file;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When return button is pressed go to previous location
|
||||||
|
*/
|
||||||
|
public onReturn() {
|
||||||
|
this.location.back();
|
||||||
|
}
|
||||||
|
|
||||||
|
public selectCollection() {
|
||||||
|
const modalRef = this.modalService.open(ImportBatchSelectorComponent);
|
||||||
|
modalRef.componentInstance.response.pipe(take(1)).subscribe((dso) => {
|
||||||
|
this.dso = dso || null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts import-metadata script with --zip fileName (and the selected file)
|
||||||
|
*/
|
||||||
|
public importMetadata() {
|
||||||
|
if (this.fileObject == null) {
|
||||||
|
this.notificationsService.error(this.translate.get('admin.metadata-import.page.error.addFile'));
|
||||||
|
} else {
|
||||||
|
const parameterValues: ProcessParameter[] = [
|
||||||
|
Object.assign(new ProcessParameter(), { name: '--zip', value: this.fileObject.name }),
|
||||||
|
Object.assign(new ProcessParameter(), { name: '--add' })
|
||||||
|
];
|
||||||
|
if (this.dso) {
|
||||||
|
parameterValues.push(Object.assign(new ProcessParameter(), { name: '--collection', value: this.dso.uuid }));
|
||||||
|
}
|
||||||
|
if (this.validateOnly) {
|
||||||
|
parameterValues.push(Object.assign(new ProcessParameter(), { name: '-v', value: true }));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.scriptDataService.invoke(BATCH_IMPORT_SCRIPT_NAME, parameterValues, [this.fileObject]).pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
).subscribe((rd: RemoteData<Process>) => {
|
||||||
|
if (rd.hasSucceeded) {
|
||||||
|
const title = this.translate.get('process.new.notification.success.title');
|
||||||
|
const content = this.translate.get('process.new.notification.success.content');
|
||||||
|
this.notificationsService.success(title, content);
|
||||||
|
if (isNotEmpty(rd.payload)) {
|
||||||
|
this.router.navigateByUrl(getProcessDetailRoute(rd.payload.processId));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const title = this.translate.get('process.new.notification.error.title');
|
||||||
|
const content = this.translate.get('process.new.notification.error.content');
|
||||||
|
this.notificationsService.error(title, content);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* return selected dspace object name
|
||||||
|
*/
|
||||||
|
getDspaceObjectName(): string {
|
||||||
|
if (this.dso) {
|
||||||
|
return this.dsoNameService.getName(this.dso);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* remove selected dso object
|
||||||
|
*/
|
||||||
|
removeDspaceObject(): void {
|
||||||
|
this.dso = null;
|
||||||
|
}
|
||||||
|
}
|
@@ -7,6 +7,7 @@ import { AdminWorkflowPageComponent } from './admin-workflow-page/admin-workflow
|
|||||||
import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service';
|
import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service';
|
||||||
import { AdminCurationTasksComponent } from './admin-curation-tasks/admin-curation-tasks.component';
|
import { AdminCurationTasksComponent } from './admin-curation-tasks/admin-curation-tasks.component';
|
||||||
import { REGISTRIES_MODULE_PATH } from './admin-routing-paths';
|
import { REGISTRIES_MODULE_PATH } from './admin-routing-paths';
|
||||||
|
import { BatchImportPageComponent } from './admin-import-batch-page/batch-import-page.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -40,6 +41,12 @@ import { REGISTRIES_MODULE_PATH } from './admin-routing-paths';
|
|||||||
component: MetadataImportPageComponent,
|
component: MetadataImportPageComponent,
|
||||||
data: { title: 'admin.metadata-import.title', breadcrumbKey: 'admin.metadata-import' }
|
data: { title: 'admin.metadata-import.title', breadcrumbKey: 'admin.metadata-import' }
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'batch-import',
|
||||||
|
resolve: { breadcrumb: I18nBreadcrumbResolver },
|
||||||
|
component: BatchImportPageComponent,
|
||||||
|
data: { title: 'admin.batch-import.title', breadcrumbKey: 'admin.batch-import' }
|
||||||
|
},
|
||||||
])
|
])
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
|
@@ -9,6 +9,7 @@ import { AdminWorkflowModuleModule } from './admin-workflow-page/admin-workflow.
|
|||||||
import { AdminSearchModule } from './admin-search-page/admin-search.module';
|
import { AdminSearchModule } from './admin-search-page/admin-search.module';
|
||||||
import { AdminSidebarSectionComponent } from './admin-sidebar/admin-sidebar-section/admin-sidebar-section.component';
|
import { AdminSidebarSectionComponent } from './admin-sidebar/admin-sidebar-section/admin-sidebar-section.component';
|
||||||
import { ExpandableAdminSidebarSectionComponent } from './admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component';
|
import { ExpandableAdminSidebarSectionComponent } from './admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component';
|
||||||
|
import { BatchImportPageComponent } from './admin-import-batch-page/batch-import-page.component';
|
||||||
|
|
||||||
const ENTRY_COMPONENTS = [
|
const ENTRY_COMPONENTS = [
|
||||||
// put only entry components that use custom decorator
|
// put only entry components that use custom decorator
|
||||||
@@ -28,7 +29,8 @@ const ENTRY_COMPONENTS = [
|
|||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
AdminCurationTasksComponent,
|
AdminCurationTasksComponent,
|
||||||
MetadataImportPageComponent
|
MetadataImportPageComponent,
|
||||||
|
BatchImportPageComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class AdminModule {
|
export class AdminModule {
|
||||||
|
@@ -24,6 +24,8 @@ import { dataService } from '../base/data-service.decorator';
|
|||||||
|
|
||||||
export const METADATA_IMPORT_SCRIPT_NAME = 'metadata-import';
|
export const METADATA_IMPORT_SCRIPT_NAME = 'metadata-import';
|
||||||
export const METADATA_EXPORT_SCRIPT_NAME = 'metadata-export';
|
export const METADATA_EXPORT_SCRIPT_NAME = 'metadata-export';
|
||||||
|
export const BATCH_IMPORT_SCRIPT_NAME = 'import';
|
||||||
|
export const BATCH_EXPORT_SCRIPT_NAME = 'export';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@dataService(SCRIPT)
|
@dataService(SCRIPT)
|
||||||
|
@@ -259,9 +259,15 @@ describe('MenuResolver', () => {
|
|||||||
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||||
id: 'import', visible: true,
|
id: 'import', visible: true,
|
||||||
}));
|
}));
|
||||||
|
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||||
|
id: 'import_batch', parentID: 'import', visible: true,
|
||||||
|
}));
|
||||||
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||||
id: 'export', visible: true,
|
id: 'export', visible: true,
|
||||||
}));
|
}));
|
||||||
|
expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({
|
||||||
|
id: 'export_batch', parentID: 'export', visible: true,
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -44,6 +44,9 @@ import {
|
|||||||
METADATA_IMPORT_SCRIPT_NAME,
|
METADATA_IMPORT_SCRIPT_NAME,
|
||||||
ScriptDataService
|
ScriptDataService
|
||||||
} from './core/data/processes/script-data.service';
|
} from './core/data/processes/script-data.service';
|
||||||
|
import {
|
||||||
|
ExportBatchSelectorComponent
|
||||||
|
} from './shared/dso-selector/modal-wrappers/export-batch-selector/export-batch-selector.component';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates all of the app's menus
|
* Creates all of the app's menus
|
||||||
@@ -440,6 +443,20 @@ export class MenuResolver implements Resolve<boolean> {
|
|||||||
} as OnClickMenuItemModel,
|
} as OnClickMenuItemModel,
|
||||||
shouldPersistOnRouteChange: true
|
shouldPersistOnRouteChange: true
|
||||||
});
|
});
|
||||||
|
this.menuService.addSection(MenuID.ADMIN, {
|
||||||
|
id: 'export_batch',
|
||||||
|
parentID: 'export',
|
||||||
|
active: false,
|
||||||
|
visible: true,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.ONCLICK,
|
||||||
|
text: 'menu.section.export_batch',
|
||||||
|
function: () => {
|
||||||
|
this.modalService.open(ExportBatchSelectorComponent);
|
||||||
|
}
|
||||||
|
} as OnClickMenuItemModel,
|
||||||
|
shouldPersistOnRouteChange: true
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -448,20 +465,7 @@ export class MenuResolver implements Resolve<boolean> {
|
|||||||
* the import scripts exist and the current user is allowed to execute them
|
* the import scripts exist and the current user is allowed to execute them
|
||||||
*/
|
*/
|
||||||
createImportMenuSections() {
|
createImportMenuSections() {
|
||||||
const menuList = [
|
const menuList = [];
|
||||||
// TODO: enable this menu item once the feature has been implemented
|
|
||||||
// {
|
|
||||||
// id: 'import_batch',
|
|
||||||
// parentID: 'import',
|
|
||||||
// active: false,
|
|
||||||
// visible: true,
|
|
||||||
// model: {
|
|
||||||
// type: MenuItemType.LINK,
|
|
||||||
// text: 'menu.section.import_batch',
|
|
||||||
// link: ''
|
|
||||||
// } as LinkMenuItemModel,
|
|
||||||
// }
|
|
||||||
];
|
|
||||||
menuList.forEach((menuSection) => this.menuService.addSection(MenuID.ADMIN, menuSection));
|
menuList.forEach((menuSection) => this.menuService.addSection(MenuID.ADMIN, menuSection));
|
||||||
|
|
||||||
observableCombineLatest([
|
observableCombineLatest([
|
||||||
@@ -498,6 +502,18 @@ export class MenuResolver implements Resolve<boolean> {
|
|||||||
} as LinkMenuItemModel,
|
} as LinkMenuItemModel,
|
||||||
shouldPersistOnRouteChange: true
|
shouldPersistOnRouteChange: true
|
||||||
});
|
});
|
||||||
|
this.menuService.addSection(MenuID.ADMIN, {
|
||||||
|
id: 'import_batch',
|
||||||
|
parentID: 'import',
|
||||||
|
active: false,
|
||||||
|
visible: true,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.LINK,
|
||||||
|
text: 'menu.section.import_batch',
|
||||||
|
link: '/admin/batch-import'
|
||||||
|
} as LinkMenuItemModel,
|
||||||
|
shouldPersistOnRouteChange: true
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -10,7 +10,9 @@ export enum SelectorActionType {
|
|||||||
CREATE = 'create',
|
CREATE = 'create',
|
||||||
EDIT = 'edit',
|
EDIT = 'edit',
|
||||||
EXPORT_METADATA = 'export-metadata',
|
EXPORT_METADATA = 'export-metadata',
|
||||||
SET_SCOPE = 'set-scope'
|
IMPORT_BATCH = 'import-batch',
|
||||||
|
SET_SCOPE = 'set-scope',
|
||||||
|
EXPORT_BATCH = 'export-batch'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -0,0 +1,210 @@
|
|||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { DebugElement, NgModule, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { NgbActiveModal, NgbModal, NgbModalModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { BATCH_EXPORT_SCRIPT_NAME, ScriptDataService } from '../../../../core/data/processes/script-data.service';
|
||||||
|
import { Collection } from '../../../../core/shared/collection.model';
|
||||||
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
|
import { ProcessParameter } from '../../../../process-page/processes/process-parameter.model';
|
||||||
|
import { ConfirmationModalComponent } from '../../../confirmation-modal/confirmation-modal.component';
|
||||||
|
import { TranslateLoaderMock } from '../../../mocks/translate-loader.mock';
|
||||||
|
import { NotificationsService } from '../../../notifications/notifications.service';
|
||||||
|
import { NotificationsServiceStub } from '../../../testing/notifications-service.stub';
|
||||||
|
import {
|
||||||
|
createFailedRemoteDataObject$,
|
||||||
|
createSuccessfulRemoteDataObject,
|
||||||
|
createSuccessfulRemoteDataObject$
|
||||||
|
} from '../../../remote-data.utils';
|
||||||
|
import { ExportBatchSelectorComponent } from './export-batch-selector.component';
|
||||||
|
import { AuthorizationDataService } from '../../../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
|
||||||
|
// No way to add entryComponents yet to testbed; alternative implemented; source: https://stackoverflow.com/questions/41689468/how-to-shallow-test-a-component-with-an-entrycomponents
|
||||||
|
@NgModule({
|
||||||
|
imports: [NgbModalModule,
|
||||||
|
TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: TranslateLoaderMock
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
exports: [],
|
||||||
|
declarations: [ConfirmationModalComponent],
|
||||||
|
providers: []
|
||||||
|
})
|
||||||
|
class ModelTestModule {
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('ExportBatchSelectorComponent', () => {
|
||||||
|
let component: ExportBatchSelectorComponent;
|
||||||
|
let fixture: ComponentFixture<ExportBatchSelectorComponent>;
|
||||||
|
let debugElement: DebugElement;
|
||||||
|
let modalRef;
|
||||||
|
|
||||||
|
let router;
|
||||||
|
let notificationService: NotificationsServiceStub;
|
||||||
|
let scriptService;
|
||||||
|
let authorizationDataService;
|
||||||
|
|
||||||
|
const mockItem = Object.assign(new Item(), {
|
||||||
|
id: 'fake-id',
|
||||||
|
uuid: 'fake-id',
|
||||||
|
handle: 'fake/handle',
|
||||||
|
lastModified: '2018'
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockCollection: Collection = Object.assign(new Collection(), {
|
||||||
|
id: 'test-collection-1-1',
|
||||||
|
uuid: 'test-collection-1-1',
|
||||||
|
name: 'test-collection-1',
|
||||||
|
metadata: {
|
||||||
|
'dc.identifier.uri': [
|
||||||
|
{
|
||||||
|
language: null,
|
||||||
|
value: 'fake/test-collection-1'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const itemRD = createSuccessfulRemoteDataObject(mockItem);
|
||||||
|
const modalStub = jasmine.createSpyObj('modalStub', ['close']);
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
notificationService = new NotificationsServiceStub();
|
||||||
|
router = jasmine.createSpyObj('router', {
|
||||||
|
navigateByUrl: jasmine.createSpy('navigateByUrl')
|
||||||
|
});
|
||||||
|
scriptService = jasmine.createSpyObj('scriptService',
|
||||||
|
{
|
||||||
|
invoke: createSuccessfulRemoteDataObject$({ processId: '45' })
|
||||||
|
}
|
||||||
|
);
|
||||||
|
authorizationDataService = jasmine.createSpyObj('authorizationDataService', {
|
||||||
|
isAuthorized: observableOf(true)
|
||||||
|
});
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), ModelTestModule],
|
||||||
|
declarations: [ExportBatchSelectorComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: NgbActiveModal, useValue: modalStub },
|
||||||
|
{ provide: NotificationsService, useValue: notificationService },
|
||||||
|
{ provide: ScriptDataService, useValue: scriptService },
|
||||||
|
{ provide: AuthorizationDataService, useValue: authorizationDataService },
|
||||||
|
{
|
||||||
|
provide: ActivatedRoute,
|
||||||
|
useValue: {
|
||||||
|
root: {
|
||||||
|
snapshot: {
|
||||||
|
data: {
|
||||||
|
dso: itemRD,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: Router, useValue: router
|
||||||
|
}
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ExportBatchSelectorComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
debugElement = fixture.debugElement;
|
||||||
|
const modalService = TestBed.inject(NgbModal);
|
||||||
|
modalRef = modalService.open(ConfirmationModalComponent);
|
||||||
|
modalRef.componentInstance.response = observableOf(true);
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if item is selected', () => {
|
||||||
|
let scriptRequestSucceeded;
|
||||||
|
beforeEach((done) => {
|
||||||
|
component.navigate(mockItem).subscribe((succeeded: boolean) => {
|
||||||
|
scriptRequestSucceeded = succeeded;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should not invoke batch-export script', () => {
|
||||||
|
expect(scriptService.invoke).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if collection is selected and is admin', () => {
|
||||||
|
let scriptRequestSucceeded;
|
||||||
|
beforeEach((done) => {
|
||||||
|
spyOn((component as any).modalService, 'open').and.returnValue(modalRef);
|
||||||
|
component.navigate(mockCollection).subscribe((succeeded: boolean) => {
|
||||||
|
scriptRequestSucceeded = succeeded;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should invoke the batch-export script with option --id uuid option', () => {
|
||||||
|
const parameterValues: ProcessParameter[] = [
|
||||||
|
Object.assign(new ProcessParameter(), { name: '--id', value: mockCollection.uuid }),
|
||||||
|
Object.assign(new ProcessParameter(), { name: '--type', value: 'COLLECTION' }),
|
||||||
|
];
|
||||||
|
expect(scriptService.invoke).toHaveBeenCalledWith(BATCH_EXPORT_SCRIPT_NAME, parameterValues, []);
|
||||||
|
});
|
||||||
|
it('success notification is shown', () => {
|
||||||
|
expect(scriptRequestSucceeded).toBeTrue();
|
||||||
|
expect(notificationService.success).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
it('redirected to process page', () => {
|
||||||
|
expect(router.navigateByUrl).toHaveBeenCalledWith('/processes/45');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('if collection is selected and is not admin', () => {
|
||||||
|
let scriptRequestSucceeded;
|
||||||
|
beforeEach((done) => {
|
||||||
|
(authorizationDataService.isAuthorized as jasmine.Spy).and.returnValue(observableOf(false));
|
||||||
|
spyOn((component as any).modalService, 'open').and.returnValue(modalRef);
|
||||||
|
component.navigate(mockCollection).subscribe((succeeded: boolean) => {
|
||||||
|
scriptRequestSucceeded = succeeded;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should invoke the Batch-export script with option --id uuid without option', () => {
|
||||||
|
const parameterValues: ProcessParameter[] = [
|
||||||
|
Object.assign(new ProcessParameter(), { name: '--id', value: mockCollection.uuid }),
|
||||||
|
Object.assign(new ProcessParameter(), { name: '--type', value: 'COLLECTION' })
|
||||||
|
];
|
||||||
|
expect(scriptService.invoke).toHaveBeenCalledWith(BATCH_EXPORT_SCRIPT_NAME, parameterValues, []);
|
||||||
|
});
|
||||||
|
it('success notification is shown', () => {
|
||||||
|
expect(scriptRequestSucceeded).toBeTrue();
|
||||||
|
expect(notificationService.success).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
it('redirected to process page', () => {
|
||||||
|
expect(router.navigateByUrl).toHaveBeenCalledWith('/processes/45');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if collection is selected; but script invoke fails', () => {
|
||||||
|
let scriptRequestSucceeded;
|
||||||
|
beforeEach((done) => {
|
||||||
|
spyOn((component as any).modalService, 'open').and.returnValue(modalRef);
|
||||||
|
jasmine.getEnv().allowRespy(true);
|
||||||
|
spyOn(scriptService, 'invoke').and.returnValue(createFailedRemoteDataObject$('Error', 500));
|
||||||
|
component.navigate(mockCollection).subscribe((succeeded: boolean) => {
|
||||||
|
scriptRequestSucceeded = succeeded;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('error notification is shown', () => {
|
||||||
|
expect(scriptRequestSucceeded).toBeFalse();
|
||||||
|
expect(notificationService.error).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,111 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
|
import { map, switchMap } from 'rxjs/operators';
|
||||||
|
import { BATCH_EXPORT_SCRIPT_NAME, ScriptDataService } from '../../../../core/data/processes/script-data.service';
|
||||||
|
import { Collection } from '../../../../core/shared/collection.model';
|
||||||
|
import { DSpaceObjectType } from '../../../../core/shared/dspace-object-type.model';
|
||||||
|
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
|
||||||
|
import { NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { ProcessParameter } from '../../../../process-page/processes/process-parameter.model';
|
||||||
|
import { ConfirmationModalComponent } from '../../../confirmation-modal/confirmation-modal.component';
|
||||||
|
import { isNotEmpty } from '../../../empty.util';
|
||||||
|
import { NotificationsService } from '../../../notifications/notifications.service';
|
||||||
|
import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils';
|
||||||
|
import { DSOSelectorModalWrapperComponent, SelectorActionType } from '../dso-selector-modal-wrapper.component';
|
||||||
|
import { getFirstCompletedRemoteData } from '../../../../core/shared/operators';
|
||||||
|
import { Process } from '../../../../process-page/processes/process.model';
|
||||||
|
import { RemoteData } from '../../../../core/data/remote-data';
|
||||||
|
import { getProcessDetailRoute } from '../../../../process-page/process-page-routing.paths';
|
||||||
|
import { AuthorizationDataService } from '../../../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { FeatureID } from '../../../../core/data/feature-authorization/feature-id';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component to wrap a list of existing dso's inside a modal
|
||||||
|
* Used to choose a dso from to export metadata of
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-export-metadata-selector',
|
||||||
|
templateUrl: '../dso-selector-modal-wrapper.component.html',
|
||||||
|
})
|
||||||
|
export class ExportBatchSelectorComponent extends DSOSelectorModalWrapperComponent implements OnInit {
|
||||||
|
objectType = DSpaceObjectType.DSPACEOBJECT;
|
||||||
|
selectorTypes = [DSpaceObjectType.COLLECTION];
|
||||||
|
action = SelectorActionType.EXPORT_BATCH;
|
||||||
|
|
||||||
|
constructor(protected activeModal: NgbActiveModal, protected route: ActivatedRoute, private router: Router,
|
||||||
|
protected notificationsService: NotificationsService, protected translationService: TranslateService,
|
||||||
|
protected scriptDataService: ScriptDataService,
|
||||||
|
protected authorizationDataService: AuthorizationDataService,
|
||||||
|
private modalService: NgbModal) {
|
||||||
|
super(activeModal, route);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the dso is a collection or community: start export-metadata script & navigate to process if successful
|
||||||
|
* Otherwise show error message
|
||||||
|
*/
|
||||||
|
navigate(dso: DSpaceObject): Observable<boolean> {
|
||||||
|
if (dso instanceof Collection) {
|
||||||
|
const modalRef = this.modalService.open(ConfirmationModalComponent);
|
||||||
|
modalRef.componentInstance.dso = dso;
|
||||||
|
modalRef.componentInstance.headerLabel = 'confirmation-modal.export-batch.header';
|
||||||
|
modalRef.componentInstance.infoLabel = 'confirmation-modal.export-batch.info';
|
||||||
|
modalRef.componentInstance.cancelLabel = 'confirmation-modal.export-batch.cancel';
|
||||||
|
modalRef.componentInstance.confirmLabel = 'confirmation-modal.export-batch.confirm';
|
||||||
|
modalRef.componentInstance.confirmIcon = 'fas fa-file-export';
|
||||||
|
const resp$ = modalRef.componentInstance.response.pipe(switchMap((confirm: boolean) => {
|
||||||
|
if (confirm) {
|
||||||
|
const startScriptSucceeded$ = this.startScriptNotifyAndRedirect(dso);
|
||||||
|
return startScriptSucceeded$.pipe(
|
||||||
|
switchMap((r: boolean) => {
|
||||||
|
return observableOf(r);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const modalRefExport = this.modalService.open(ExportBatchSelectorComponent);
|
||||||
|
modalRefExport.componentInstance.dsoRD = createSuccessfulRemoteDataObject(dso);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
resp$.subscribe();
|
||||||
|
return resp$;
|
||||||
|
} else {
|
||||||
|
return observableOf(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start export-metadata script of dso & navigate to process if successful
|
||||||
|
* Otherwise show error message
|
||||||
|
* @param dso Dso to export
|
||||||
|
*/
|
||||||
|
private startScriptNotifyAndRedirect(dso: DSpaceObject): Observable<boolean> {
|
||||||
|
const parameterValues: ProcessParameter[] = [
|
||||||
|
Object.assign(new ProcessParameter(), { name: '--id', value: dso.uuid }),
|
||||||
|
Object.assign(new ProcessParameter(), { name: '--type', value: 'COLLECTION' })
|
||||||
|
];
|
||||||
|
return this.authorizationDataService.isAuthorized(FeatureID.AdministratorOf).pipe(
|
||||||
|
switchMap(() => {
|
||||||
|
return this.scriptDataService.invoke(BATCH_EXPORT_SCRIPT_NAME, parameterValues, []);
|
||||||
|
}),
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
map((rd: RemoteData<Process>) => {
|
||||||
|
if (rd.hasSucceeded) {
|
||||||
|
const title = this.translationService.get('process.new.notification.success.title');
|
||||||
|
const content = this.translationService.get('process.new.notification.success.content');
|
||||||
|
this.notificationsService.success(title, content);
|
||||||
|
if (isNotEmpty(rd.payload)) {
|
||||||
|
this.router.navigateByUrl(getProcessDetailRoute(rd.payload.processId));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
const title = this.translationService.get('process.new.notification.error.title');
|
||||||
|
const content = this.translationService.get('process.new.notification.error.content');
|
||||||
|
this.notificationsService.error(title, content);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,77 @@
|
|||||||
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { Collection } from '../../../../core/shared/collection.model';
|
||||||
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
|
import { ImportBatchSelectorComponent } from './import-batch-selector.component';
|
||||||
|
|
||||||
|
describe('ImportBatchSelectorComponent', () => {
|
||||||
|
let component: ImportBatchSelectorComponent;
|
||||||
|
let fixture: ComponentFixture<ImportBatchSelectorComponent>;
|
||||||
|
const mockItem = Object.assign(new Item(), {
|
||||||
|
id: 'fake-id',
|
||||||
|
uuid: 'fake-id',
|
||||||
|
handle: 'fake/handle',
|
||||||
|
lastModified: '2018'
|
||||||
|
});
|
||||||
|
const mockCollection: Collection = Object.assign(new Collection(), {
|
||||||
|
id: 'test-collection-1-1',
|
||||||
|
uuid: 'test-collection-1-1',
|
||||||
|
name: 'test-collection-1',
|
||||||
|
metadata: {
|
||||||
|
'dc.identifier.uri': [
|
||||||
|
{
|
||||||
|
language: null,
|
||||||
|
value: 'fake/test-collection-1'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const modalStub = jasmine.createSpyObj('modalStub', ['close']);
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
|
||||||
|
declarations: [ImportBatchSelectorComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: NgbActiveModal, useValue: modalStub },
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ImportBatchSelectorComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
spyOn(component.response, 'emit');
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if item is selected', () => {
|
||||||
|
beforeEach((done) => {
|
||||||
|
component.navigate(mockItem).subscribe(() => {
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should emit null value', () => {
|
||||||
|
expect(component.response.emit).toHaveBeenCalledWith(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if collection is selected', () => {
|
||||||
|
beforeEach((done) => {
|
||||||
|
component.navigate(mockCollection).subscribe(() => {
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should emit collection value', () => {
|
||||||
|
expect(component.response.emit).toHaveBeenCalledWith(mockCollection);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@@ -0,0 +1,44 @@
|
|||||||
|
import { Component, EventEmitter, OnInit, Output } from '@angular/core';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { Collection } from '../../../../core/shared/collection.model';
|
||||||
|
import { DSpaceObjectType } from '../../../../core/shared/dspace-object-type.model';
|
||||||
|
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
|
||||||
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { DSOSelectorModalWrapperComponent, SelectorActionType } from '../dso-selector-modal-wrapper.component';
|
||||||
|
import { Observable, of } from 'rxjs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component to wrap a list of existing dso's inside a modal
|
||||||
|
* Used to choose a dso from to import metadata of
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-import-batch-selector',
|
||||||
|
templateUrl: '../dso-selector-modal-wrapper.component.html',
|
||||||
|
})
|
||||||
|
export class ImportBatchSelectorComponent extends DSOSelectorModalWrapperComponent implements OnInit {
|
||||||
|
objectType = DSpaceObjectType.DSPACEOBJECT;
|
||||||
|
selectorTypes = [DSpaceObjectType.COLLECTION];
|
||||||
|
action = SelectorActionType.IMPORT_BATCH;
|
||||||
|
/**
|
||||||
|
* An event fired when the modal is closed
|
||||||
|
*/
|
||||||
|
@Output()
|
||||||
|
response = new EventEmitter<DSpaceObject>();
|
||||||
|
|
||||||
|
constructor(protected activeModal: NgbActiveModal,
|
||||||
|
protected route: ActivatedRoute) {
|
||||||
|
super(activeModal, route);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the dso is a collection:
|
||||||
|
*/
|
||||||
|
navigate(dso: DSpaceObject): Observable<null> {
|
||||||
|
if (dso instanceof Collection) {
|
||||||
|
this.response.emit(dso);
|
||||||
|
return of(null);
|
||||||
|
}
|
||||||
|
this.response.emit(null);
|
||||||
|
return of(null);
|
||||||
|
}
|
||||||
|
}
|
@@ -24,6 +24,12 @@ import { ConfirmationModalComponent } from './confirmation-modal/confirmation-mo
|
|||||||
import {
|
import {
|
||||||
ExportMetadataSelectorComponent
|
ExportMetadataSelectorComponent
|
||||||
} from './dso-selector/modal-wrappers/export-metadata-selector/export-metadata-selector.component';
|
} from './dso-selector/modal-wrappers/export-metadata-selector/export-metadata-selector.component';
|
||||||
|
import {
|
||||||
|
ExportBatchSelectorComponent
|
||||||
|
} from './dso-selector/modal-wrappers/export-batch-selector/export-batch-selector.component';
|
||||||
|
import {
|
||||||
|
ImportBatchSelectorComponent
|
||||||
|
} from './dso-selector/modal-wrappers/import-batch-selector/import-batch-selector.component';
|
||||||
import { FileDropzoneNoUploaderComponent } from './file-dropzone-no-uploader/file-dropzone-no-uploader.component';
|
import { FileDropzoneNoUploaderComponent } from './file-dropzone-no-uploader/file-dropzone-no-uploader.component';
|
||||||
import { ItemListElementComponent } from './object-list/item-list-element/item-types/item/item-list-element.component';
|
import { ItemListElementComponent } from './object-list/item-list-element/item-types/item/item-list-element.component';
|
||||||
import { EnumKeysPipe } from './utils/enum-keys-pipe';
|
import { EnumKeysPipe } from './utils/enum-keys-pipe';
|
||||||
@@ -476,6 +482,8 @@ const COMPONENTS = [
|
|||||||
CollectionDropdownComponent,
|
CollectionDropdownComponent,
|
||||||
EntityDropdownComponent,
|
EntityDropdownComponent,
|
||||||
ExportMetadataSelectorComponent,
|
ExportMetadataSelectorComponent,
|
||||||
|
ImportBatchSelectorComponent,
|
||||||
|
ExportBatchSelectorComponent,
|
||||||
ConfirmationModalComponent,
|
ConfirmationModalComponent,
|
||||||
VocabularyTreeviewComponent,
|
VocabularyTreeviewComponent,
|
||||||
AuthorizedCollectionSelectorComponent,
|
AuthorizedCollectionSelectorComponent,
|
||||||
@@ -555,6 +563,8 @@ const ENTRY_COMPONENTS = [
|
|||||||
BitstreamRequestACopyPageComponent,
|
BitstreamRequestACopyPageComponent,
|
||||||
CurationFormComponent,
|
CurationFormComponent,
|
||||||
ExportMetadataSelectorComponent,
|
ExportMetadataSelectorComponent,
|
||||||
|
ImportBatchSelectorComponent,
|
||||||
|
ExportBatchSelectorComponent,
|
||||||
ConfirmationModalComponent,
|
ConfirmationModalComponent,
|
||||||
VocabularyTreeviewComponent,
|
VocabularyTreeviewComponent,
|
||||||
SidebarSearchListElementComponent,
|
SidebarSearchListElementComponent,
|
||||||
|
@@ -542,28 +542,45 @@
|
|||||||
|
|
||||||
"admin.metadata-import.breadcrumbs": "Import Metadata",
|
"admin.metadata-import.breadcrumbs": "Import Metadata",
|
||||||
|
|
||||||
|
"admin.batch-import.breadcrumbs": "Import Batch",
|
||||||
|
|
||||||
"admin.metadata-import.title": "Import Metadata",
|
"admin.metadata-import.title": "Import Metadata",
|
||||||
|
|
||||||
|
"admin.batch-import.title": "Import Batch",
|
||||||
|
|
||||||
"admin.metadata-import.page.header": "Import Metadata",
|
"admin.metadata-import.page.header": "Import Metadata",
|
||||||
|
|
||||||
|
"admin.batch-import.page.header": "Import Batch",
|
||||||
|
|
||||||
"admin.metadata-import.page.help": "You can drop or browse CSV files that contain batch metadata operations on files here",
|
"admin.metadata-import.page.help": "You can drop or browse CSV files that contain batch metadata operations on files here",
|
||||||
|
|
||||||
|
"admin.batch-import.page.help": "Select the Collection to import into. Then, drop or browse to a Simple Archive Format (SAF) zip file that includes the Items to import",
|
||||||
|
|
||||||
"admin.metadata-import.page.dropMsg": "Drop a metadata CSV to import",
|
"admin.metadata-import.page.dropMsg": "Drop a metadata CSV to import",
|
||||||
|
|
||||||
|
"admin.batch-import.page.dropMsg": "Drop a batch ZIP to import",
|
||||||
|
|
||||||
"admin.metadata-import.page.dropMsgReplace": "Drop to replace the metadata CSV to import",
|
"admin.metadata-import.page.dropMsgReplace": "Drop to replace the metadata CSV to import",
|
||||||
|
|
||||||
|
"admin.batch-import.page.dropMsgReplace": "Drop to replace the batch ZIP to import",
|
||||||
|
|
||||||
"admin.metadata-import.page.button.return": "Back",
|
"admin.metadata-import.page.button.return": "Back",
|
||||||
|
|
||||||
"admin.metadata-import.page.button.proceed": "Proceed",
|
"admin.metadata-import.page.button.proceed": "Proceed",
|
||||||
|
|
||||||
|
"admin.metadata-import.page.button.select-collection": "Select Collection",
|
||||||
|
|
||||||
"admin.metadata-import.page.error.addFile": "Select file first!",
|
"admin.metadata-import.page.error.addFile": "Select file first!",
|
||||||
|
|
||||||
|
"admin.batch-import.page.error.addFile": "Select Zip file first!",
|
||||||
|
|
||||||
"admin.metadata-import.page.validateOnly": "Validate Only",
|
"admin.metadata-import.page.validateOnly": "Validate Only",
|
||||||
|
|
||||||
"admin.metadata-import.page.validateOnly.hint": "When selected, the uploaded CSV will be validated. You will receive a report of detected changes, but no changes will be saved.",
|
"admin.metadata-import.page.validateOnly.hint": "When selected, the uploaded CSV will be validated. You will receive a report of detected changes, but no changes will be saved.",
|
||||||
|
|
||||||
|
"admin.batch-import.page.validateOnly.hint": "When selected, the uploaded ZIP will be validated. You will receive a report of detected changes, but no changes will be saved.",
|
||||||
|
|
||||||
|
"admin.batch-import.page.remove": "remove",
|
||||||
|
|
||||||
"auth.errors.invalid-user": "Invalid email address or password.",
|
"auth.errors.invalid-user": "Invalid email address or password.",
|
||||||
|
|
||||||
@@ -1346,6 +1363,10 @@
|
|||||||
|
|
||||||
"dso-selector.export-metadata.dspaceobject.head": "Export metadata from",
|
"dso-selector.export-metadata.dspaceobject.head": "Export metadata from",
|
||||||
|
|
||||||
|
"dso-selector.export-batch.dspaceobject.head": "Export Batch (ZIP) from",
|
||||||
|
|
||||||
|
"dso-selector.import-batch.dspaceobject.head": "Import batch from",
|
||||||
|
|
||||||
"dso-selector.no-results": "No {{ type }} found",
|
"dso-selector.no-results": "No {{ type }} found",
|
||||||
|
|
||||||
"dso-selector.placeholder": "Search for a {{ type }}",
|
"dso-selector.placeholder": "Search for a {{ type }}",
|
||||||
@@ -1374,6 +1395,14 @@
|
|||||||
|
|
||||||
"confirmation-modal.export-metadata.confirm": "Export",
|
"confirmation-modal.export-metadata.confirm": "Export",
|
||||||
|
|
||||||
|
"confirmation-modal.export-batch.header": "Export batch (ZIP) for {{ dsoName }}",
|
||||||
|
|
||||||
|
"confirmation-modal.export-batch.info": "Are you sure you want to export batch (ZIP) for {{ dsoName }}",
|
||||||
|
|
||||||
|
"confirmation-modal.export-batch.cancel": "Cancel",
|
||||||
|
|
||||||
|
"confirmation-modal.export-batch.confirm": "Export",
|
||||||
|
|
||||||
"confirmation-modal.delete-eperson.header": "Delete EPerson \"{{ dsoName }}\"",
|
"confirmation-modal.delete-eperson.header": "Delete EPerson \"{{ dsoName }}\"",
|
||||||
|
|
||||||
"confirmation-modal.delete-eperson.info": "Are you sure you want to delete EPerson \"{{ dsoName }}\"",
|
"confirmation-modal.delete-eperson.info": "Are you sure you want to delete EPerson \"{{ dsoName }}\"",
|
||||||
@@ -2617,6 +2646,7 @@
|
|||||||
|
|
||||||
"menu.section.export_metadata": "Metadata",
|
"menu.section.export_metadata": "Metadata",
|
||||||
|
|
||||||
|
"menu.section.export_batch": "Batch Export (ZIP)",
|
||||||
|
|
||||||
|
|
||||||
"menu.section.icon.access_control": "Access Control menu section",
|
"menu.section.icon.access_control": "Access Control menu section",
|
||||||
|
Reference in New Issue
Block a user