71712: confirmation modal for export + tests &

- request causing error because of issue #756, commented out for now &
- drop event prevention in a HostListener like dragover event
This commit is contained in:
Marie Verdonck
2020-07-27 18:17:37 +02:00
parent 66cdf9dd18
commit 3e0f4a54e6
9 changed files with 228 additions and 14 deletions

View File

@@ -357,7 +357,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
observableCombineLatest( observableCombineLatest(
this.authorizationService.isAuthorized(FeatureID.AdministratorOf), this.authorizationService.isAuthorized(FeatureID.AdministratorOf),
this.scriptDataService.scriptWithNameExistsAndCanExecute(METADATA_EXPORT_SCRIPT_NAME) // this.scriptDataService.scriptWithNameExistsAndCanExecute(METADATA_EXPORT_SCRIPT_NAME)
).pipe( ).pipe(
// TODO uncomment when #635 (https://github.com/DSpace/dspace-angular/issues/635) is fixed; otherwise even in production mode, the metadata export button is only available after a refresh (and not in dev mode) // TODO uncomment when #635 (https://github.com/DSpace/dspace-angular/issues/635) is fixed; otherwise even in production mode, the metadata export button is only available after a refresh (and not in dev mode)
// filter(([authorized, metadataExportScriptExists]: boolean[]) => authorized && metadataExportScriptExists), // filter(([authorized, metadataExportScriptExists]: boolean[]) => authorized && metadataExportScriptExists),
@@ -416,7 +416,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
observableCombineLatest( observableCombineLatest(
this.authorizationService.isAuthorized(FeatureID.AdministratorOf), this.authorizationService.isAuthorized(FeatureID.AdministratorOf),
this.scriptDataService.scriptWithNameExistsAndCanExecute(METADATA_IMPORT_SCRIPT_NAME) // this.scriptDataService.scriptWithNameExistsAndCanExecute(METADATA_IMPORT_SCRIPT_NAME)
).pipe( ).pipe(
// TODO uncomment when #635 (https://github.com/DSpace/dspace-angular/issues/635) is fixed // TODO uncomment when #635 (https://github.com/DSpace/dspace-angular/issues/635) is fixed
// filter(([authorized, metadataImportScriptExists]: boolean[]) => authorized && metadataImportScriptExists), // filter(([authorized, metadataImportScriptExists]: boolean[]) => authorized && metadataImportScriptExists),

View File

@@ -0,0 +1,16 @@
<div>
<div class="modal-header">{{ headerLabel | translate:{dsoName: dso?.name} }}
<button type="button" class="close" (click)="close()" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<p>{{ infoLabel | translate:{dsoName: dso?.name} }}</p>
<button type="button" class="cancel btn btn-secondary" (click)="cancelPressed()" aria-label="Cancel">
{{ cancelLabel | translate:{dsoName: dso?.name} }}
</button>
<button type="button" class="confirm btn btn-primary" (click)="confirmPressed()" aria-label="Confirm" ngbAutofocus>
{{ confirmLabel | translate:{dsoName: dso?.name} }}
</button>
</div>
</div>

View File

@@ -0,0 +1,117 @@
import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { TranslateModule } from '@ngx-translate/core';
import { ConfirmationModalComponent } from './confirmation-modal.component';
describe('ConfirmationModalComponent', () => {
let component: ConfirmationModalComponent;
let fixture: ComponentFixture<ConfirmationModalComponent>;
let debugElement: DebugElement;
const modalStub = jasmine.createSpyObj('modalStub', ['close']);
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot()],
declarations: [ConfirmationModalComponent],
providers: [
{ provide: NgbActiveModal, useValue: modalStub }
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ConfirmationModalComponent);
component = fixture.componentInstance;
debugElement = fixture.debugElement;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
describe('close', () => {
beforeEach(() => {
component.close();
});
it('should call the close method on the active modal', () => {
expect(modalStub.close).toHaveBeenCalled();
});
});
describe('confirmPressed', () => {
beforeEach(() => {
spyOn(component.response, 'emit');
component.confirmPressed();
});
it('should call the close method on the active modal', () => {
expect(modalStub.close).toHaveBeenCalled();
});
it('event emitter should emit true', () => {
expect(component.response.emit).toHaveBeenCalledWith(true);
});
});
describe('cancelPressed', () => {
beforeEach(() => {
spyOn(component.response, 'emit');
component.cancelPressed();
});
it('should call the close method on the active modal', () => {
expect(modalStub.close).toHaveBeenCalled();
});
it('event emitter should emit false', () => {
expect(component.response.emit).toHaveBeenCalledWith(false);
});
});
describe('when the click method emits on close button', () => {
beforeEach(fakeAsync(() => {
spyOn(component, 'close');
debugElement.query(By.css('button.close')).triggerEventHandler('click', {});
tick();
fixture.detectChanges();
}));
it('should call the close method on the component', () => {
expect(component.close).toHaveBeenCalled();
});
});
describe('when the click method emits on cancel button', () => {
beforeEach(fakeAsync(() => {
spyOn(component, 'close');
spyOn(component.response, 'emit');
debugElement.query(By.css('button.cancel')).triggerEventHandler('click', {});
tick();
fixture.detectChanges();
}));
it('should call the close method on the component', () => {
expect(component.close).toHaveBeenCalled();
});
it('event emitter should emit false', () => {
expect(component.response.emit).toHaveBeenCalledWith(false);
});
});
describe('when the click method emits on confirm button', () => {
beforeEach(fakeAsync(() => {
spyOn(component, 'close');
spyOn(component.response, 'emit');
debugElement.query(By.css('button.confirm')).triggerEventHandler('click', {});
tick();
fixture.detectChanges();
}));
it('should call the close method on the component', () => {
expect(component.close).toHaveBeenCalled();
});
it('event emitter should emit true', () => {
expect(component.response.emit).toHaveBeenCalledWith(true);
});
});
});

View File

@@ -0,0 +1,48 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { DSpaceObject } from '../../core/shared/dspace-object.model';
@Component({
selector: 'ds-confirmation-modal',
templateUrl: 'confirmation-modal.component.html',
})
export class ConfirmationModalComponent {
@Input() headerLabel: string;
@Input() infoLabel: string;
@Input() cancelLabel: string;
@Input() confirmLabel: string;
@Input() dso: DSpaceObject;
/**
* An event fired when the cancel or confirm button is clicked, with respectively false or true
*/
@Output()
response = new EventEmitter<boolean>();
constructor(protected activeModal: NgbActiveModal) {
}
/**
* Confirm the action that led to the modal
*/
confirmPressed() {
this.response.emit(true);
this.close();
}
/**
* Cancel the action that led to the modal and close modal
*/
cancelPressed() {
this.response.emit(false);
this.close();
}
/**
* Close the modal
*/
close() {
this.activeModal.close();
}
}

View File

@@ -4,16 +4,19 @@ import { TranslateService } from '@ngx-translate/core';
import { Observable } from 'rxjs/internal/Observable'; import { Observable } from 'rxjs/internal/Observable';
import { take, map } from 'rxjs/operators'; import { take, map } from 'rxjs/operators';
import { of as observableOf } from 'rxjs'; import { of as observableOf } from 'rxjs';
import { AuthService } from '../../../../core/auth/auth.service';
import { METADATA_EXPORT_SCRIPT_NAME, ScriptDataService } from '../../../../core/data/processes/script-data.service'; import { METADATA_EXPORT_SCRIPT_NAME, ScriptDataService } from '../../../../core/data/processes/script-data.service';
import { RequestEntry } from '../../../../core/data/request.reducer'; import { RequestEntry } from '../../../../core/data/request.reducer';
import { Collection } from '../../../../core/shared/collection.model'; import { Collection } from '../../../../core/shared/collection.model';
import { Community } from '../../../../core/shared/community.model'; import { Community } from '../../../../core/shared/community.model';
import { DSpaceObjectType } from '../../../../core/shared/dspace-object-type.model'; import { DSpaceObjectType } from '../../../../core/shared/dspace-object-type.model';
import { DSpaceObject } from '../../../../core/shared/dspace-object.model'; import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { ProcessParameter } from '../../../../process-page/processes/process-parameter.model'; import { ProcessParameter } from '../../../../process-page/processes/process-parameter.model';
import { ConfirmationModalComponent } from '../../../confirmation-modal/confirmation-modal.component';
import { isNotEmpty } from '../../../empty.util'; import { isNotEmpty } from '../../../empty.util';
import { NotificationsService } from '../../../notifications/notifications.service'; import { NotificationsService } from '../../../notifications/notifications.service';
import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils';
import { DSOSelectorModalWrapperComponent, SelectorActionType } from '../dso-selector-modal-wrapper.component'; import { DSOSelectorModalWrapperComponent, SelectorActionType } from '../dso-selector-modal-wrapper.component';
/** /**
@@ -21,7 +24,7 @@ import { DSOSelectorModalWrapperComponent, SelectorActionType } from '../dso-sel
* Used to choose a dso from to export metadata of * Used to choose a dso from to export metadata of
*/ */
@Component({ @Component({
selector: 'ds-edit-item-selector', selector: 'ds-export-metadata-selector',
templateUrl: '../dso-selector-modal-wrapper.component.html', templateUrl: '../dso-selector-modal-wrapper.component.html',
}) })
export class ExportMetadataSelectorComponent extends DSOSelectorModalWrapperComponent implements OnInit { export class ExportMetadataSelectorComponent extends DSOSelectorModalWrapperComponent implements OnInit {
@@ -31,7 +34,8 @@ export class ExportMetadataSelectorComponent extends DSOSelectorModalWrapperComp
constructor(protected activeModal: NgbActiveModal, protected route: ActivatedRoute, private router: Router, constructor(protected activeModal: NgbActiveModal, protected route: ActivatedRoute, private router: Router,
protected notificationsService: NotificationsService, protected translationService: TranslateService, protected notificationsService: NotificationsService, protected translationService: TranslateService,
protected scriptDataService: ScriptDataService) { protected scriptDataService: ScriptDataService,
private modalService: NgbModal) {
super(activeModal, route); super(activeModal, route);
} }
@@ -41,9 +45,23 @@ export class ExportMetadataSelectorComponent extends DSOSelectorModalWrapperComp
*/ */
navigate(dso: DSpaceObject): Observable<boolean> { navigate(dso: DSpaceObject): Observable<boolean> {
if (dso instanceof Collection || dso instanceof Community) { 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.response.subscribe((confirm: boolean) => {
if (confirm) {
const startScriptSucceeded = this.startScriptNotifyAndRedirect(dso, dso.handle); const startScriptSucceeded = this.startScriptNotifyAndRedirect(dso, dso.handle);
startScriptSucceeded.pipe(take(1)).subscribe(); startScriptSucceeded.pipe(take(1)).subscribe();
return startScriptSucceeded; return startScriptSucceeded;
} else {
const modalRef = this.modalService.open(ExportMetadataSelectorComponent);
modalRef.componentInstance.dsoRD = createSuccessfulRemoteDataObject(dso);
}
});
} else { } else {
this.notificationsService.error(this.translationService.get('dso-selector.export-metadata.notValidDSO')); this.notificationsService.error(this.translationService.get('dso-selector.export-metadata.notValidDSO'));
return observableOf(false); return observableOf(false);

View File

@@ -51,15 +51,17 @@ export class FileDropzoneNoUploaderComponent implements OnInit {
ngOnInit() { ngOnInit() {
this.uploaderId = 'ds-drag-and-drop-uploader' + uniqueId(); this.uploaderId = 'ds-drag-and-drop-uploader' + uniqueId();
this.isOverDocumentDropZone = observableOf(false); this.isOverDocumentDropZone = observableOf(false);
window.addEventListener('drop', (e: DragEvent) => {
return e && e.preventDefault();
}, false);
this.uploader = new FileUploader({ this.uploader = new FileUploader({
// required, but using onFileDrop, not uploader // required, but using onFileDrop, not uploader
url: 'placeholder', url: 'placeholder',
}); });
} }
@HostListener('window:drop', ['$event'])
onDrop(event: any) {
event.preventDefault();
}
@HostListener('window:dragover', ['$event']) @HostListener('window:dragover', ['$event'])
onDragOver(event: any) { onDragOver(event: any) {
// Show drop area on the page // Show drop area on the page

View File

@@ -10,6 +10,7 @@ import { MissingTranslationHandler, TranslateModule } from '@ngx-translate/core'
import { NgxPaginationModule } from 'ngx-pagination'; import { NgxPaginationModule } from 'ngx-pagination';
import { ComcolRoleComponent } from './comcol-forms/edit-comcol-page/comcol-role/comcol-role.component'; import { ComcolRoleComponent } from './comcol-forms/edit-comcol-page/comcol-role/comcol-role.component';
import { ConfirmationModalComponent } from './confirmation-modal/confirmation-modal.component';
import { ExportMetadataSelectorComponent } from './dso-selector/modal-wrappers/export-metadata-selector/export-metadata-selector.component'; import { ExportMetadataSelectorComponent } from './dso-selector/modal-wrappers/export-metadata-selector/export-metadata-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 { PublicationListElementComponent } from './object-list/item-list-element/item-types/publication/publication-list-element.component'; import { PublicationListElementComponent } from './object-list/item-list-element/item-types/publication/publication-list-element.component';
@@ -401,7 +402,8 @@ const COMPONENTS = [
GroupSearchBoxComponent, GroupSearchBoxComponent,
FileDownloadLinkComponent, FileDownloadLinkComponent,
CollectionDropdownComponent, CollectionDropdownComponent,
ExportMetadataSelectorComponent ExportMetadataSelectorComponent,
ConfirmationModalComponent
]; ];
const ENTRY_COMPONENTS = [ const ENTRY_COMPONENTS = [
@@ -478,7 +480,8 @@ const ENTRY_COMPONENTS = [
ClaimedTaskActionsEditMetadataComponent, ClaimedTaskActionsEditMetadataComponent,
FileDownloadLinkComponent, FileDownloadLinkComponent,
CurationFormComponent, CurationFormComponent,
ExportMetadataSelectorComponent ExportMetadataSelectorComponent,
ConfirmationModalComponent
]; ];
const SHARED_ITEM_PAGE_COMPONENTS = [ const SHARED_ITEM_PAGE_COMPONENTS = [

View File

@@ -995,6 +995,16 @@
"confirmation-modal.export-metadata.header": "Export metadata for {{ dsoName }}",
"confirmation-modal.export-metadata.info": "Are you sure you want to export metadata for {{ dsoName }}",
"confirmation-modal.export-metadata.cancel": "Cancel",
"confirmation-modal.export-metadata.confirm": "Export",
"error.bitstream": "Error fetching bitstream", "error.bitstream": "Error fetching bitstream",
"error.browse-by": "Error fetching items", "error.browse-by": "Error fetching items",

View File

@@ -105,7 +105,7 @@ export const environment: Partial<GlobalConfig> = {
}, },
// Angular Universal settings // Angular Universal settings
universal: { universal: {
preboot: true, preboot: false,
async: true, async: true,
time: false time: false
}, },