From c1498424f3605eb61cb90cc7d853ebf86305ee8f Mon Sep 17 00:00:00 2001 From: Nikunj Sharma Date: Tue, 13 Sep 2022 16:18:32 +0530 Subject: [PATCH 1/6] CST-6685 changes for import batch --- .../batch-import-page.component.html | 35 ++++ .../batch-import-page.component.spec.ts | 149 ++++++++++++++++++ .../batch-import-page.component.ts | 123 +++++++++++++++ src/app/admin/admin-routing.module.ts | 7 + src/app/admin/admin.module.ts | 4 +- .../data/processes/script-data.service.ts | 1 + src/app/menu.resolver.spec.ts | 3 + src/app/menu.resolver.ts | 27 ++-- .../dso-selector-modal-wrapper.component.ts | 1 + .../import-batch-selector.component.spec.ts | 100 ++++++++++++ .../import-batch-selector.component.ts | 45 ++++++ src/app/shared/shared.module.ts | 5 + src/assets/i18n/en.json5 | 27 +++- 13 files changed, 508 insertions(+), 19 deletions(-) create mode 100644 src/app/admin/admin-import-batch-page/batch-import-page.component.html create mode 100644 src/app/admin/admin-import-batch-page/batch-import-page.component.spec.ts create mode 100644 src/app/admin/admin-import-batch-page/batch-import-page.component.ts create mode 100644 src/app/shared/dso-selector/modal-wrappers/import-batch-selector/import-batch-selector.component.spec.ts create mode 100644 src/app/shared/dso-selector/modal-wrappers/import-batch-selector/import-batch-selector.component.ts diff --git a/src/app/admin/admin-import-batch-page/batch-import-page.component.html b/src/app/admin/admin-import-batch-page/batch-import-page.component.html new file mode 100644 index 0000000000..dbc8c74437 --- /dev/null +++ b/src/app/admin/admin-import-batch-page/batch-import-page.component.html @@ -0,0 +1,35 @@ +
+ +

{{'admin.batch-import.page.help' | translate}}

+

+ selected collection: {{getDspaceObjectName()}}  + {{'admin.batch-import.page.remove' | translate}} +

+

+ +

+
+
+ + +
+ + {{'admin.batch-import.page.validateOnly.hint' | translate}} + +
+ + + + +
+ + +
+
diff --git a/src/app/admin/admin-import-batch-page/batch-import-page.component.spec.ts b/src/app/admin/admin-import-batch-page/batch-import-page.component.spec.ts new file mode 100644 index 0000000000..eecf05f543 --- /dev/null +++ b/src/app/admin/admin-import-batch-page/batch-import-page.component.spec.ts @@ -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; + + 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(); + }); + }); + }); +}); diff --git a/src/app/admin/admin-import-batch-page/batch-import-page.component.ts b/src/app/admin/admin-import-batch-page/batch-import-page.component.ts new file mode 100644 index 0000000000..2985aff3aa --- /dev/null +++ b/src/app/admin/admin-import-batch-page/batch-import-page.component.ts @@ -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) => { + 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; + } +} diff --git a/src/app/admin/admin-routing.module.ts b/src/app/admin/admin-routing.module.ts index ee5cb8737b..1ea20bc9a0 100644 --- a/src/app/admin/admin-routing.module.ts +++ b/src/app/admin/admin-routing.module.ts @@ -7,6 +7,7 @@ import { AdminWorkflowPageComponent } from './admin-workflow-page/admin-workflow import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service'; import { AdminCurationTasksComponent } from './admin-curation-tasks/admin-curation-tasks.component'; import { REGISTRIES_MODULE_PATH } from './admin-routing-paths'; +import { BatchImportPageComponent } from './admin-import-batch-page/batch-import-page.component'; @NgModule({ imports: [ @@ -40,6 +41,12 @@ import { REGISTRIES_MODULE_PATH } from './admin-routing-paths'; component: MetadataImportPageComponent, 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: [ diff --git a/src/app/admin/admin.module.ts b/src/app/admin/admin.module.ts index b28a0cf89e..0ddbefd253 100644 --- a/src/app/admin/admin.module.ts +++ b/src/app/admin/admin.module.ts @@ -9,6 +9,7 @@ import { AdminWorkflowModuleModule } from './admin-workflow-page/admin-workflow. import { AdminSearchModule } from './admin-search-page/admin-search.module'; 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 { BatchImportPageComponent } from './admin-import-batch-page/batch-import-page.component'; const ENTRY_COMPONENTS = [ // put only entry components that use custom decorator @@ -28,7 +29,8 @@ const ENTRY_COMPONENTS = [ ], declarations: [ AdminCurationTasksComponent, - MetadataImportPageComponent + MetadataImportPageComponent, + BatchImportPageComponent ] }) export class AdminModule { diff --git a/src/app/core/data/processes/script-data.service.ts b/src/app/core/data/processes/script-data.service.ts index 75a66c822a..91f8d591af 100644 --- a/src/app/core/data/processes/script-data.service.ts +++ b/src/app/core/data/processes/script-data.service.ts @@ -25,6 +25,7 @@ import { CoreState } from '../../core-state.model'; export const METADATA_IMPORT_SCRIPT_NAME = 'metadata-import'; export const METADATA_EXPORT_SCRIPT_NAME = 'metadata-export'; +export const BATCH_IMPORT_SCRIPT_NAME = 'batch-import'; @Injectable() @dataService(SCRIPT) diff --git a/src/app/menu.resolver.spec.ts b/src/app/menu.resolver.spec.ts index db90b7ea00..f39075ad27 100644 --- a/src/app/menu.resolver.spec.ts +++ b/src/app/menu.resolver.spec.ts @@ -259,6 +259,9 @@ describe('MenuResolver', () => { expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({ 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({ id: 'export', visible: true, })); diff --git a/src/app/menu.resolver.ts b/src/app/menu.resolver.ts index f12079f737..a7c1fe415e 100644 --- a/src/app/menu.resolver.ts +++ b/src/app/menu.resolver.ts @@ -448,20 +448,7 @@ export class MenuResolver implements Resolve { * the import scripts exist and the current user is allowed to execute them */ createImportMenuSections() { - 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, - // } - ]; + const menuList = []; menuList.forEach((menuSection) => this.menuService.addSection(MenuID.ADMIN, menuSection)); observableCombineLatest([ @@ -498,6 +485,18 @@ export class MenuResolver implements Resolve { } as LinkMenuItemModel, 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 + }); }); } diff --git a/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.ts b/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.ts index ca8343cfad..937608a2c6 100644 --- a/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.ts @@ -10,6 +10,7 @@ export enum SelectorActionType { CREATE = 'create', EDIT = 'edit', EXPORT_METADATA = 'export-metadata', + IMPORT_BATCH = 'import-batch', SET_SCOPE = 'set-scope' } diff --git a/src/app/shared/dso-selector/modal-wrappers/import-batch-selector/import-batch-selector.component.spec.ts b/src/app/shared/dso-selector/modal-wrappers/import-batch-selector/import-batch-selector.component.spec.ts new file mode 100644 index 0000000000..576c1ea140 --- /dev/null +++ b/src/app/shared/dso-selector/modal-wrappers/import-batch-selector/import-batch-selector.component.spec.ts @@ -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; + 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); + }); + }); +}); diff --git a/src/app/shared/dso-selector/modal-wrappers/import-batch-selector/import-batch-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/import-batch-selector/import-batch-selector.component.ts new file mode 100644 index 0000000000..2fdae23388 --- /dev/null +++ b/src/app/shared/dso-selector/modal-wrappers/import-batch-selector/import-batch-selector.component.ts @@ -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(); + + constructor(protected activeModal: NgbActiveModal, + protected route: ActivatedRoute) { + super(activeModal, route); + } + + /** + * If the dso is a collection or community: + */ + navigate(dso: DSpaceObject): Observable { + if (dso instanceof Collection || dso instanceof Community) { + this.response.emit(dso); + return of(null); + } + this.response.emit(null); + return of(null); + } +} diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 9036ff98c5..5e78796e24 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -24,6 +24,9 @@ import { ConfirmationModalComponent } from './confirmation-modal/confirmation-mo import { ExportMetadataSelectorComponent } 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 { ItemListElementComponent } from './object-list/item-list-element/item-types/item/item-list-element.component'; import { EnumKeysPipe } from './utils/enum-keys-pipe'; @@ -468,6 +471,7 @@ const COMPONENTS = [ CollectionDropdownComponent, EntityDropdownComponent, ExportMetadataSelectorComponent, + ImportBatchSelectorComponent, ConfirmationModalComponent, VocabularyTreeviewComponent, AuthorizedCollectionSelectorComponent, @@ -545,6 +549,7 @@ const ENTRY_COMPONENTS = [ BitstreamRequestACopyPageComponent, CurationFormComponent, ExportMetadataSelectorComponent, + ImportBatchSelectorComponent, ConfirmationModalComponent, VocabularyTreeviewComponent, SidebarSearchListElementComponent, diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index bd40bc642c..171dbaa332 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -542,28 +542,45 @@ "admin.metadata-import.breadcrumbs": "Import Metadata", + "admin.batch-import.breadcrumbs": "Import Batch", + "admin.metadata-import.title": "Import Metadata", + "admin.batch-import.title": "Import Batch", + "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.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.batch-import.page.dropMsg": "Drop a batch ZIP 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.proceed": "Proceed", + "admin.metadata-import.page.button.select-collection": "Select Collection", + "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.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.", @@ -640,7 +657,7 @@ "bitstream-request-a-copy.alert.canDownload2": "here", "bitstream-request-a-copy.header": "Request a copy of the file", - + "bitstream-request-a-copy.intro": "Enter the following information to request a copy for the following item: ", "bitstream-request-a-copy.intro.bitstream.one": "Requesting the following file: ", @@ -1345,6 +1362,8 @@ "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.placeholder": "Search for a {{ type }}", @@ -2984,7 +3003,7 @@ "process.detail.create" : "Create similar process", "process.detail.actions": "Actions", - + "process.detail.delete.button": "Delete process", "process.detail.delete.header": "Delete process", @@ -3037,7 +3056,7 @@ "process.bulk.delete.success": "{{count}} process(es) have been succesfully deleted", - + "profile.breadcrumbs": "Update Profile", From 5b1e0d3e0d3967b24d8cf236c6ee137ffc95a62b Mon Sep 17 00:00:00 2001 From: Nikunj Sharma Date: Fri, 23 Sep 2022 16:16:22 +0530 Subject: [PATCH 2/6] CST-6685 updated changes --- .../admin-import-batch-page/batch-import-page.component.spec.ts | 2 ++ .../admin-import-batch-page/batch-import-page.component.ts | 1 + src/app/core/data/processes/script-data.service.ts | 2 +- .../import-batch-selector/import-batch-selector.component.ts | 2 +- src/assets/i18n/en.json5 | 2 +- 5 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/app/admin/admin-import-batch-page/batch-import-page.component.spec.ts b/src/app/admin/admin-import-batch-page/batch-import-page.component.spec.ts index eecf05f543..6c9d84123b 100644 --- a/src/app/admin/admin-import-batch-page/batch-import-page.component.spec.ts +++ b/src/app/admin/admin-import-batch-page/batch-import-page.component.spec.ts @@ -101,6 +101,7 @@ describe('BatchImportPageComponent', () => { const parameterValues: ProcessParameter[] = [ Object.assign(new ProcessParameter(), { name: '--zip', value: 'filename.zip' }), ]; + parameterValues.push(Object.assign(new ProcessParameter(), { name: '--add', value: 'filename.zip' })); expect(scriptService.invoke).toHaveBeenCalledWith(BATCH_IMPORT_SCRIPT_NAME, parameterValues, [fileMock]); }); it('success notification is shown', () => { @@ -121,6 +122,7 @@ describe('BatchImportPageComponent', () => { 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', value: 'filename.zip' }), Object.assign(new ProcessParameter(), { name: '-v', value: true }), ]; expect(scriptService.invoke).toHaveBeenCalledWith(BATCH_IMPORT_SCRIPT_NAME, parameterValues, [fileMock]); diff --git a/src/app/admin/admin-import-batch-page/batch-import-page.component.ts b/src/app/admin/admin-import-batch-page/batch-import-page.component.ts index 2985aff3aa..8f49ed64b6 100644 --- a/src/app/admin/admin-import-batch-page/batch-import-page.component.ts +++ b/src/app/admin/admin-import-batch-page/batch-import-page.component.ts @@ -78,6 +78,7 @@ export class BatchImportPageComponent { const parameterValues: ProcessParameter[] = [ Object.assign(new ProcessParameter(), { name: '--zip', value: this.fileObject.name }), ]; + parameterValues.push(Object.assign(new ProcessParameter(), { name: '--add', value: this.fileObject.name })); if (this.dso) { parameterValues.push(Object.assign(new ProcessParameter(), { name: '--collection', value: this.dso.uuid })); } diff --git a/src/app/core/data/processes/script-data.service.ts b/src/app/core/data/processes/script-data.service.ts index f362a9c676..58674d4005 100644 --- a/src/app/core/data/processes/script-data.service.ts +++ b/src/app/core/data/processes/script-data.service.ts @@ -24,7 +24,7 @@ import { dataService } from '../base/data-service.decorator'; export const METADATA_IMPORT_SCRIPT_NAME = 'metadata-import'; export const METADATA_EXPORT_SCRIPT_NAME = 'metadata-export'; -export const BATCH_IMPORT_SCRIPT_NAME = 'batch-import'; +export const BATCH_IMPORT_SCRIPT_NAME = 'import'; @Injectable() @dataService(SCRIPT) diff --git a/src/app/shared/dso-selector/modal-wrappers/import-batch-selector/import-batch-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/import-batch-selector/import-batch-selector.component.ts index 2fdae23388..a88aeaff3c 100644 --- a/src/app/shared/dso-selector/modal-wrappers/import-batch-selector/import-batch-selector.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/import-batch-selector/import-batch-selector.component.ts @@ -18,7 +18,7 @@ import { Observable, of } from 'rxjs'; }) export class ImportBatchSelectorComponent extends DSOSelectorModalWrapperComponent implements OnInit { objectType = DSpaceObjectType.DSPACEOBJECT; - selectorTypes = [DSpaceObjectType.COLLECTION, DSpaceObjectType.COMMUNITY]; + selectorTypes = [DSpaceObjectType.COLLECTION]; action = SelectorActionType.IMPORT_BATCH; /** * An event fired when the modal is closed diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 4dd963a5fe..3e48254a6e 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -554,7 +554,7 @@ "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.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", From b89640e4d239b9a3b9d96f1353205222e48e6d2b Mon Sep 17 00:00:00 2001 From: Nikunj Sharma Date: Mon, 26 Sep 2022 23:58:03 +0530 Subject: [PATCH 3/6] [CST-6685] removed value param with --add --- .../batch-import-page.component.spec.ts | 4 ++-- .../admin-import-batch-page/batch-import-page.component.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/admin/admin-import-batch-page/batch-import-page.component.spec.ts b/src/app/admin/admin-import-batch-page/batch-import-page.component.spec.ts index 6c9d84123b..36ba1137c9 100644 --- a/src/app/admin/admin-import-batch-page/batch-import-page.component.spec.ts +++ b/src/app/admin/admin-import-batch-page/batch-import-page.component.spec.ts @@ -101,7 +101,7 @@ describe('BatchImportPageComponent', () => { const parameterValues: ProcessParameter[] = [ Object.assign(new ProcessParameter(), { name: '--zip', value: 'filename.zip' }), ]; - parameterValues.push(Object.assign(new ProcessParameter(), { name: '--add', 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', () => { @@ -122,7 +122,7 @@ describe('BatchImportPageComponent', () => { 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', 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]); diff --git a/src/app/admin/admin-import-batch-page/batch-import-page.component.ts b/src/app/admin/admin-import-batch-page/batch-import-page.component.ts index 8f49ed64b6..03bbbe8af9 100644 --- a/src/app/admin/admin-import-batch-page/batch-import-page.component.ts +++ b/src/app/admin/admin-import-batch-page/batch-import-page.component.ts @@ -78,7 +78,7 @@ export class BatchImportPageComponent { const parameterValues: ProcessParameter[] = [ Object.assign(new ProcessParameter(), { name: '--zip', value: this.fileObject.name }), ]; - parameterValues.push(Object.assign(new ProcessParameter(), { name: '--add', value: this.fileObject.name })); + parameterValues.push(Object.assign(new ProcessParameter(), { name: '--add' })); if (this.dso) { parameterValues.push(Object.assign(new ProcessParameter(), { name: '--collection', value: this.dso.uuid })); } From 98ee0751ece8146e7ee17ed0f685f92f3171bdc4 Mon Sep 17 00:00:00 2001 From: Nikunj Sharma Date: Tue, 27 Sep 2022 19:35:37 +0530 Subject: [PATCH 4/6] [CST-6685] added new batch export functionality --- .../batch-import-page.component.ts | 2 +- .../data/processes/script-data.service.ts | 1 + src/app/menu.resolver.spec.ts | 3 + src/app/menu.resolver.ts | 17 ++ .../dso-selector-modal-wrapper.component.ts | 3 +- .../export-batch-selector.component.spec.ts | 211 ++++++++++++++++++ .../export-batch-selector.component.ts | 114 ++++++++++ .../import-batch-selector.component.spec.ts | 23 -- .../import-batch-selector.component.ts | 5 +- src/app/shared/shared.module.ts | 5 + src/assets/i18n/en.json5 | 11 + 11 files changed, 367 insertions(+), 28 deletions(-) create mode 100644 src/app/shared/dso-selector/modal-wrappers/export-batch-selector/export-batch-selector.component.spec.ts create mode 100644 src/app/shared/dso-selector/modal-wrappers/export-batch-selector/export-batch-selector.component.ts diff --git a/src/app/admin/admin-import-batch-page/batch-import-page.component.ts b/src/app/admin/admin-import-batch-page/batch-import-page.component.ts index 03bbbe8af9..7171c67585 100644 --- a/src/app/admin/admin-import-batch-page/batch-import-page.component.ts +++ b/src/app/admin/admin-import-batch-page/batch-import-page.component.ts @@ -77,8 +77,8 @@ export class BatchImportPageComponent { } else { const parameterValues: ProcessParameter[] = [ Object.assign(new ProcessParameter(), { name: '--zip', value: this.fileObject.name }), + Object.assign(new ProcessParameter(), { name: '--add' }) ]; - parameterValues.push(Object.assign(new ProcessParameter(), { name: '--add' })); if (this.dso) { parameterValues.push(Object.assign(new ProcessParameter(), { name: '--collection', value: this.dso.uuid })); } diff --git a/src/app/core/data/processes/script-data.service.ts b/src/app/core/data/processes/script-data.service.ts index 58674d4005..d9c92cb1d2 100644 --- a/src/app/core/data/processes/script-data.service.ts +++ b/src/app/core/data/processes/script-data.service.ts @@ -25,6 +25,7 @@ import { dataService } from '../base/data-service.decorator'; export const METADATA_IMPORT_SCRIPT_NAME = 'metadata-import'; export const METADATA_EXPORT_SCRIPT_NAME = 'metadata-export'; export const BATCH_IMPORT_SCRIPT_NAME = 'import'; +export const BATCH_EXPORT_SCRIPT_NAME = 'export'; @Injectable() @dataService(SCRIPT) diff --git a/src/app/menu.resolver.spec.ts b/src/app/menu.resolver.spec.ts index f39075ad27..4fd44efe66 100644 --- a/src/app/menu.resolver.spec.ts +++ b/src/app/menu.resolver.spec.ts @@ -265,6 +265,9 @@ describe('MenuResolver', () => { expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({ id: 'export', visible: true, })); + expect(menuService.addSection).toHaveBeenCalledWith(MenuID.ADMIN, jasmine.objectContaining({ + id: 'export_batch', parentID: 'export', visible: true, + })); }); }); diff --git a/src/app/menu.resolver.ts b/src/app/menu.resolver.ts index a7c1fe415e..8630150c58 100644 --- a/src/app/menu.resolver.ts +++ b/src/app/menu.resolver.ts @@ -44,6 +44,9 @@ import { METADATA_IMPORT_SCRIPT_NAME, ScriptDataService } 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 @@ -440,6 +443,20 @@ export class MenuResolver implements Resolve { } as OnClickMenuItemModel, 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 + }); }); } diff --git a/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.ts b/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.ts index 937608a2c6..113ca518fd 100644 --- a/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.ts @@ -11,7 +11,8 @@ export enum SelectorActionType { EDIT = 'edit', EXPORT_METADATA = 'export-metadata', IMPORT_BATCH = 'import-batch', - SET_SCOPE = 'set-scope' + SET_SCOPE = 'set-scope', + EXPORT_BATCH = 'export-batch' } /** diff --git a/src/app/shared/dso-selector/modal-wrappers/export-batch-selector/export-batch-selector.component.spec.ts b/src/app/shared/dso-selector/modal-wrappers/export-batch-selector/export-batch-selector.component.spec.ts new file mode 100644 index 0000000000..bfd10346b3 --- /dev/null +++ b/src/app/shared/dso-selector/modal-wrappers/export-batch-selector/export-batch-selector.component.spec.ts @@ -0,0 +1,211 @@ +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; + 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 and -a option', () => { + const parameterValues: ProcessParameter[] = [ + Object.assign(new ProcessParameter(), { name: '--id', value: mockCollection.uuid }), + Object.assign(new ProcessParameter(), { name: '--type', value: 'COLLECTION' }), + Object.assign(new ProcessParameter(), { name: '-a' }), + ]; + 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 the -a 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(); + }); + }); +}); diff --git a/src/app/shared/dso-selector/modal-wrappers/export-batch-selector/export-batch-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/export-batch-selector/export-batch-selector.component.ts new file mode 100644 index 0000000000..cb09b34c58 --- /dev/null +++ b/src/app/shared/dso-selector/modal-wrappers/export-batch-selector/export-batch-selector.component.ts @@ -0,0 +1,114 @@ +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 { + 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 { + 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((isAdmin) => { + if (isAdmin) { + parameterValues.push(Object.assign(new ProcessParameter(), {name: '-a'})); + } + return this.scriptDataService.invoke(BATCH_EXPORT_SCRIPT_NAME, parameterValues, []); + }), + getFirstCompletedRemoteData(), + map((rd: RemoteData) => { + 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; + } + }) + ); + } +} diff --git a/src/app/shared/dso-selector/modal-wrappers/import-batch-selector/import-batch-selector.component.spec.ts b/src/app/shared/dso-selector/modal-wrappers/import-batch-selector/import-batch-selector.component.spec.ts index 576c1ea140..6ed3bf28be 100644 --- a/src/app/shared/dso-selector/modal-wrappers/import-batch-selector/import-batch-selector.component.spec.ts +++ b/src/app/shared/dso-selector/modal-wrappers/import-batch-selector/import-batch-selector.component.spec.ts @@ -4,7 +4,6 @@ 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'; @@ -30,18 +29,6 @@ describe('ImportBatchSelectorComponent', () => { ] } }); - 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({ @@ -87,14 +74,4 @@ describe('ImportBatchSelectorComponent', () => { }); }); - describe('if community is selected', () => { - beforeEach((done) => { - component.navigate(mockCommunity).subscribe(() => { - done(); - }); - }); - it('should emit community value', () => { - expect(component.response.emit).toHaveBeenCalledWith(mockCommunity); - }); - }); }); diff --git a/src/app/shared/dso-selector/modal-wrappers/import-batch-selector/import-batch-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/import-batch-selector/import-batch-selector.component.ts index a88aeaff3c..4696e42e2d 100644 --- a/src/app/shared/dso-selector/modal-wrappers/import-batch-selector/import-batch-selector.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/import-batch-selector/import-batch-selector.component.ts @@ -1,7 +1,6 @@ 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'; @@ -32,10 +31,10 @@ export class ImportBatchSelectorComponent extends DSOSelectorModalWrapperCompone } /** - * If the dso is a collection or community: + * If the dso is a collection: */ navigate(dso: DSpaceObject): Observable { - if (dso instanceof Collection || dso instanceof Community) { + if (dso instanceof Collection) { this.response.emit(dso); return of(null); } diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index fc65fb6882..9d4ca55e51 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -24,6 +24,9 @@ import { ConfirmationModalComponent } from './confirmation-modal/confirmation-mo import { ExportMetadataSelectorComponent } 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'; @@ -475,6 +478,7 @@ const COMPONENTS = [ EntityDropdownComponent, ExportMetadataSelectorComponent, ImportBatchSelectorComponent, + ExportBatchSelectorComponent, ConfirmationModalComponent, VocabularyTreeviewComponent, AuthorizedCollectionSelectorComponent, @@ -554,6 +558,7 @@ const ENTRY_COMPONENTS = [ CurationFormComponent, ExportMetadataSelectorComponent, ImportBatchSelectorComponent, + ExportBatchSelectorComponent, ConfirmationModalComponent, VocabularyTreeviewComponent, SidebarSearchListElementComponent, diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 93ab5302fb..10565b4d39 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1363,6 +1363,8 @@ "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", @@ -1393,6 +1395,14 @@ "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.info": "Are you sure you want to delete EPerson \"{{ dsoName }}\"", @@ -2636,6 +2646,7 @@ "menu.section.export_metadata": "Metadata", + "menu.section.export_batch": "Batch Export(ZIP)", "menu.section.icon.access_control": "Access Control menu section", From aa53e2fff684e102d74a4f5c580d1a7e97be70ee Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Wed, 28 Sep 2022 09:54:25 +0200 Subject: [PATCH 5/6] [CST-6685] fix label --- src/assets/i18n/en.json5 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 10565b4d39..8451c3b45e 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1363,7 +1363,7 @@ "dso-selector.export-metadata.dspaceobject.head": "Export metadata from", - "dso-selector.export-batch.dspaceobject.head": "Export Batch(ZIP) from", + "dso-selector.export-batch.dspaceobject.head": "Export Batch (ZIP) from", "dso-selector.import-batch.dspaceobject.head": "Import batch from", @@ -1395,9 +1395,9 @@ "confirmation-modal.export-metadata.confirm": "Export", - "confirmation-modal.export-batch.header": "Export batch(ZIP) for {{ dsoName }}", + "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.info": "Are you sure you want to export batch (ZIP) for {{ dsoName }}", "confirmation-modal.export-batch.cancel": "Cancel", @@ -2646,7 +2646,7 @@ "menu.section.export_metadata": "Metadata", - "menu.section.export_batch": "Batch Export(ZIP)", + "menu.section.export_batch": "Batch Export (ZIP)", "menu.section.icon.access_control": "Access Control menu section", From fadcd2e0c6c30a1ebdf0385ac1a07d3ee9d261f6 Mon Sep 17 00:00:00 2001 From: Nikunj Sharma Date: Thu, 29 Sep 2022 19:13:39 +0530 Subject: [PATCH 6/6] [CST-6685] removed -a option --- .../export-batch-selector.component.spec.ts | 5 ++--- .../export-batch-selector/export-batch-selector.component.ts | 5 +---- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/app/shared/dso-selector/modal-wrappers/export-batch-selector/export-batch-selector.component.spec.ts b/src/app/shared/dso-selector/modal-wrappers/export-batch-selector/export-batch-selector.component.spec.ts index bfd10346b3..18ec6007ea 100644 --- a/src/app/shared/dso-selector/modal-wrappers/export-batch-selector/export-batch-selector.component.spec.ts +++ b/src/app/shared/dso-selector/modal-wrappers/export-batch-selector/export-batch-selector.component.spec.ts @@ -150,11 +150,10 @@ describe('ExportBatchSelectorComponent', () => { done(); }); }); - it('should invoke the batch-export script with option --id uuid and -a option', () => { + 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' }), - Object.assign(new ProcessParameter(), { name: '-a' }), ]; expect(scriptService.invoke).toHaveBeenCalledWith(BATCH_EXPORT_SCRIPT_NAME, parameterValues, []); }); @@ -176,7 +175,7 @@ describe('ExportBatchSelectorComponent', () => { done(); }); }); - it('should invoke the Batch-export script with option --id uuid without the -a option', () => { + 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' }) diff --git a/src/app/shared/dso-selector/modal-wrappers/export-batch-selector/export-batch-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/export-batch-selector/export-batch-selector.component.ts index cb09b34c58..0645e09029 100644 --- a/src/app/shared/dso-selector/modal-wrappers/export-batch-selector/export-batch-selector.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/export-batch-selector/export-batch-selector.component.ts @@ -86,10 +86,7 @@ export class ExportBatchSelectorComponent extends DSOSelectorModalWrapperCompone Object.assign(new ProcessParameter(), { name: '--type', value: 'COLLECTION' }) ]; return this.authorizationDataService.isAuthorized(FeatureID.AdministratorOf).pipe( - switchMap((isAdmin) => { - if (isAdmin) { - parameterValues.push(Object.assign(new ProcessParameter(), {name: '-a'})); - } + switchMap(() => { return this.scriptDataService.invoke(BATCH_EXPORT_SCRIPT_NAME, parameterValues, []); }), getFirstCompletedRemoteData(),