CST-6685 changes for import batch

This commit is contained in:
Nikunj Sharma
2022-09-13 16:18:32 +05:30
parent e7dc5f8d14
commit c1498424f3
13 changed files with 508 additions and 19 deletions

View File

@@ -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>&nbsp;
<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>

View File

@@ -0,0 +1,149 @@
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' }),
];
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: '-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();
});
});
});
});

View File

@@ -0,0 +1,123 @@
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 }),
];
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;
}
}

View File

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

View File

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

View File

@@ -25,6 +25,7 @@ import { CoreState } from '../../core-state.model';
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 = 'batch-import';
@Injectable() @Injectable()
@dataService(SCRIPT) @dataService(SCRIPT)

View File

@@ -259,6 +259,9 @@ 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,
})); }));

View File

@@ -448,20 +448,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 +485,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
});
}); });
} }

View File

@@ -10,6 +10,7 @@ export enum SelectorActionType {
CREATE = 'create', CREATE = 'create',
EDIT = 'edit', EDIT = 'edit',
EXPORT_METADATA = 'export-metadata', EXPORT_METADATA = 'export-metadata',
IMPORT_BATCH = 'import-batch',
SET_SCOPE = 'set-scope' SET_SCOPE = 'set-scope'
} }

View File

@@ -0,0 +1,100 @@
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 { Community } from '../../../../core/shared/community.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 mockCommunity = Object.assign(new Community(), {
id: 'test-uuid',
uuid: 'test-uuid',
metadata: {
'dc.identifier.uri': [
{
language: null,
value: 'fake/test-community-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);
});
});
describe('if community is selected', () => {
beforeEach((done) => {
component.navigate(mockCommunity).subscribe(() => {
done();
});
});
it('should emit community value', () => {
expect(component.response.emit).toHaveBeenCalledWith(mockCommunity);
});
});
});

View File

@@ -0,0 +1,45 @@
import { Component, EventEmitter, OnInit, Output } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Collection } from '../../../../core/shared/collection.model';
import { Community } from '../../../../core/shared/community.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, DSpaceObjectType.COMMUNITY];
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 or community:
*/
navigate(dso: DSpaceObject): Observable<null> {
if (dso instanceof Collection || dso instanceof Community) {
this.response.emit(dso);
return of(null);
}
this.response.emit(null);
return of(null);
}
}

View File

@@ -24,6 +24,9 @@ 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 {
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';
@@ -468,6 +471,7 @@ const COMPONENTS = [
CollectionDropdownComponent, CollectionDropdownComponent,
EntityDropdownComponent, EntityDropdownComponent,
ExportMetadataSelectorComponent, ExportMetadataSelectorComponent,
ImportBatchSelectorComponent,
ConfirmationModalComponent, ConfirmationModalComponent,
VocabularyTreeviewComponent, VocabularyTreeviewComponent,
AuthorizedCollectionSelectorComponent, AuthorizedCollectionSelectorComponent,
@@ -545,6 +549,7 @@ const ENTRY_COMPONENTS = [
BitstreamRequestACopyPageComponent, BitstreamRequestACopyPageComponent,
CurationFormComponent, CurationFormComponent,
ExportMetadataSelectorComponent, ExportMetadataSelectorComponent,
ImportBatchSelectorComponent,
ConfirmationModalComponent, ConfirmationModalComponent,
VocabularyTreeviewComponent, VocabularyTreeviewComponent,
SidebarSearchListElementComponent, SidebarSearchListElementComponent,

View File

@@ -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": "You can drop or browse ZIP files that contain batch operations on files here",
"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.",
@@ -1345,6 +1362,8 @@
"dso-selector.export-metadata.dspaceobject.head": "Export metadata from", "dso-selector.export-metadata.dspaceobject.head": "Export metadata 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 }}",