Skip to content

Commit

Permalink
Merged in DSC-818 (pull request DSpace#376)
Browse files Browse the repository at this point in the history
[DSC-818] Add bulk export page and link it to side menu

Approved-by: Giuseppe Digilio
  • Loading branch information
Sufiyan Shaikh authored and atarix83 committed Oct 12, 2023
2 parents b9d9fed + a298284 commit 6d85dba
Show file tree
Hide file tree
Showing 12 changed files with 356 additions and 33 deletions.
1 change: 1 addition & 0 deletions src/app/core/data/processes/script-data.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +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 COLLECTION_EXPORT_SCRIPT_NAME = 'collection-export';
export const BATCH_IMPORT_SCRIPT_NAME = 'import';
export const BATCH_EXPORT_SCRIPT_NAME = 'export';
export const ITEM_EXPORT_SCRIPT_NAME = 'item-export';
Expand Down
27 changes: 22 additions & 5 deletions src/app/menu.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,11 @@ import {
ThemedEditItemSelectorComponent
} from './shared/dso-selector/modal-wrappers/edit-item-selector/themed-edit-item-selector.component';
import {
ExportMetadataSelectorComponent
} from './shared/dso-selector/modal-wrappers/export-metadata-selector/export-metadata-selector.component';
ExportMetadataCsvSelectorComponent
} from './shared/dso-selector/modal-wrappers/export-metadata-csv-selector/export-metadata-csv-selector.component';
import {
ExportMetadataXlsSelectorComponent
} from './shared/dso-selector/modal-wrappers/export-metadata-xls-selector/export-metadata-xls-selector.component';
import { AuthorizationDataService } from './core/data/feature-authorization/authorization-data.service';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import {
Expand Down Expand Up @@ -535,15 +538,29 @@ export class MenuResolver implements Resolve<boolean> {
shouldPersistOnRouteChange: true
});
this.menuService.addSection(MenuID.ADMIN, {
id: 'export_metadata',
id: 'export_metadata_csv',
parentID: 'export',
active: true,
visible: true,
model: {
type: MenuItemType.ONCLICK,
text: 'menu.section.export_metadata_csv',
function: () => {
this.modalService.open(ExportMetadataCsvSelectorComponent);
}
} as OnClickMenuItemModel,
shouldPersistOnRouteChange: true
});
this.menuService.addSection(MenuID.ADMIN, {
id: 'export_metadata_xls',
parentID: 'export',
active: true,
visible: true,
model: {
type: MenuItemType.ONCLICK,
text: 'menu.section.export_metadata',
text: 'menu.section.export_metadata_xls',
function: () => {
this.modalService.open(ExportMetadataSelectorComponent);
this.modalService.open(ExportMetadataXlsSelectorComponent);
}
} as OnClickMenuItemModel,
shouldPersistOnRouteChange: true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model'
import { ContextMenuEntryComponent } from '../context-menu-entry.component';
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
import { ProcessParameter } from '../../../process-page/processes/process-parameter.model';
import { ScriptDataService } from '../../../core/data/processes/script-data.service';
import { COLLECTION_EXPORT_SCRIPT_NAME, ScriptDataService } from '../../../core/data/processes/script-data.service';
import { NotificationsService } from '../../notifications/notifications.service';
import { RequestService } from '../../../core/data/request.service';
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
Expand Down Expand Up @@ -62,7 +62,7 @@ export class ExportCollectionMenuComponent extends ContextMenuEntryComponent {
{ name: '-c', value: this.contextMenuObject.id }
];

this.scriptService.invoke('collection-export', stringParameters, [])
this.scriptService.invoke(COLLECTION_EXPORT_SCRIPT_NAME, stringParameters, [])
.pipe(getFirstCompletedRemoteData())
.subscribe((rd: RemoteData<Process>) => {
if (rd.isSuccess) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import { hasValue, isNotEmpty } from '../../empty.util';
export enum SelectorActionType {
CREATE = 'create',
EDIT = 'edit',
EXPORT_METADATA = 'export-metadata',
EXPORT_METADATA_CSV = 'export-metadata-csv',
EXPORT_METADATA_XLS = 'export-metadata-xls',
IMPORT_BATCH = 'import-batch',
SET_SCOPE = 'set-scope',
EXPORT_BATCH = 'export-batch',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { FeatureID } from '../../../../core/data/feature-authorization/feature-i
* Used to choose a dso from to export metadata of
*/
@Component({
selector: 'ds-export-metadata-selector',
selector: 'ds-export-metadata-csv-selector',
templateUrl: '../dso-selector-modal-wrapper.component.html',
})
export class ExportBatchSelectorComponent extends DSOSelectorModalWrapperComponent implements OnInit {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { DSOSelectorModalWrapperComponent, SelectorActionType } from '../dso-selector-modal-wrapper.component';
import { TranslateService } from '@ngx-translate/core';
import { getFirstCompletedRemoteData } from '../../../../core/shared/operators';
import { ScriptDataService } from '../../../../core/data/processes/script-data.service';
import { COLLECTION_EXPORT_SCRIPT_NAME, ScriptDataService } from '../../../../core/data/processes/script-data.service';
import { RemoteData } from '../../../../core/data/remote-data';
import { ProcessParameter } from '../../../../process-page/processes/process-parameter.model';
import { Process } from '../../../../process-page/processes/process.model';
Expand Down Expand Up @@ -45,7 +45,7 @@ export class ExportExcelSelectorComponent extends DSOSelectorModalWrapperCompone
{ name: '-c', value: dso.id }
];

this.scriptService.invoke('collection-export', stringParameters, [])
this.scriptService.invoke(COLLECTION_EXPORT_SCRIPT_NAME, stringParameters, [])
.pipe(getFirstCompletedRemoteData())
.subscribe((rd: RemoteData<Process>) => {
if (rd.isSuccess) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
createSuccessfulRemoteDataObject,
createSuccessfulRemoteDataObject$
} from '../../../remote-data.utils';
import { ExportMetadataSelectorComponent } from './export-metadata-selector.component';
import { ExportMetadataCsvSelectorComponent } from './export-metadata-csv-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
Expand All @@ -40,9 +40,9 @@ import { AuthorizationDataService } from '../../../../core/data/feature-authoriz
class ModelTestModule {
}

describe('ExportMetadataSelectorComponent', () => {
let component: ExportMetadataSelectorComponent;
let fixture: ComponentFixture<ExportMetadataSelectorComponent>;
describe('ExportMetadataCsvSelectorComponent', () => {
let component: ExportMetadataCsvSelectorComponent;
let fixture: ComponentFixture<ExportMetadataCsvSelectorComponent>;
let debugElement: DebugElement;
let modalRef;

Expand Down Expand Up @@ -103,7 +103,7 @@ describe('ExportMetadataSelectorComponent', () => {
});
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), ModelTestModule],
declarations: [ExportMetadataSelectorComponent],
declarations: [ExportMetadataCsvSelectorComponent],
providers: [
{ provide: NgbActiveModal, useValue: modalStub },
{ provide: NotificationsService, useValue: notificationService },
Expand Down Expand Up @@ -131,7 +131,7 @@ describe('ExportMetadataSelectorComponent', () => {
}));

beforeEach(() => {
fixture = TestBed.createComponent(ExportMetadataSelectorComponent);
fixture = TestBed.createComponent(ExportMetadataCsvSelectorComponent);
component = fixture.componentInstance;
debugElement = fixture.debugElement;
const modalService = TestBed.inject(NgbModal);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,14 @@ import { FeatureID } from '../../../../core/data/feature-authorization/feature-i
* Used to choose a dso from to export metadata of
*/
@Component({
selector: 'ds-export-metadata-selector',
selector: 'ds-export-metadata-csv-selector',
templateUrl: '../dso-selector-modal-wrapper.component.html',
})
export class ExportMetadataSelectorComponent extends DSOSelectorModalWrapperComponent implements OnInit {
export class ExportMetadataCsvSelectorComponent extends DSOSelectorModalWrapperComponent implements OnInit {
configuration = 'backend';
objectType = DSpaceObjectType.DSPACEOBJECT;
selectorTypes = [DSpaceObjectType.COLLECTION, DSpaceObjectType.COMMUNITY];
action = SelectorActionType.EXPORT_METADATA;
action = SelectorActionType.EXPORT_METADATA_CSV;

constructor(protected activeModal: NgbActiveModal, protected route: ActivatedRoute, private router: Router,
protected notificationsService: NotificationsService, protected translationService: TranslateService,
Expand All @@ -52,10 +52,10 @@ export class ExportMetadataSelectorComponent extends DSOSelectorModalWrapperComp
if (dso instanceof Collection || dso instanceof Community) {
const modalRef = this.modalService.open(ConfirmationModalComponent);
modalRef.componentInstance.dso = dso;
modalRef.componentInstance.headerLabel = 'confirmation-modal.export-metadata.header';
modalRef.componentInstance.infoLabel = 'confirmation-modal.export-metadata.info';
modalRef.componentInstance.cancelLabel = 'confirmation-modal.export-metadata.cancel';
modalRef.componentInstance.confirmLabel = 'confirmation-modal.export-metadata.confirm';
modalRef.componentInstance.headerLabel = 'confirmation-modal.export-metadata-csv.header';
modalRef.componentInstance.infoLabel = 'confirmation-modal.export-metadata-csv.info';
modalRef.componentInstance.cancelLabel = 'confirmation-modal.export-metadata-csv.cancel';
modalRef.componentInstance.confirmLabel = 'confirmation-modal.export-metadata-csv.confirm';
modalRef.componentInstance.confirmIcon = 'fas fa-file-export';
const resp$ = modalRef.componentInstance.response.pipe(switchMap((confirm: boolean) => {
if (confirm) {
Expand All @@ -66,7 +66,7 @@ export class ExportMetadataSelectorComponent extends DSOSelectorModalWrapperComp
})
);
} else {
const modalRefExport = this.modalService.open(ExportMetadataSelectorComponent);
const modalRefExport = this.modalService.open(ExportMetadataCsvSelectorComponent);
modalRefExport.componentInstance.dsoRD = createSuccessfulRemoteDataObject(dso);
}
}));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
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 { COLLECTION_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 { ExportMetadataXlsSelectorComponent } from './export-metadata-xls-selector.component';

// 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('ExportMetadataXlsSelectorComponent', () => {
let component: ExportMetadataXlsSelectorComponent;
let fixture: ComponentFixture<ExportMetadataXlsSelectorComponent>;
let debugElement: DebugElement;
let modalRef;

let router;
let notificationService: NotificationsServiceStub;
let scriptService;

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' })
}
);
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), ModelTestModule],
declarations: [ExportMetadataXlsSelectorComponent],
providers: [
{ provide: NgbActiveModal, useValue: modalStub },
{ provide: NotificationsService, useValue: notificationService },
{ provide: ScriptDataService, useValue: scriptService },
{
provide: ActivatedRoute,
useValue: {
root: {
snapshot: {
data: {
dso: itemRD,
},
},
}
},
},
{
provide: Router, useValue: router
}
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();

}));

beforeEach(() => {
fixture = TestBed.createComponent(ExportMetadataXlsSelectorComponent);
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 collection-export script', () => {
expect(scriptService.invoke).not.toHaveBeenCalled();
});
});

describe('if collection is selected', () => {
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 collection-export script with option -c uuid', () => {
const parameterValues: ProcessParameter[] = [
Object.assign(new ProcessParameter(), { name: '-c', value: mockCollection.uuid }),
];
expect(scriptService.invoke).toHaveBeenCalledWith(COLLECTION_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();
});
});

});
Loading

0 comments on commit 6d85dba

Please sign in to comment.