diff --git a/src/app/shared/comcol/comcol-forms/comcol-form/comcol-form.component.html b/src/app/shared/comcol/comcol-forms/comcol-form/comcol-form.component.html index 3d2e990fe4..815b1b9748 100644 --- a/src/app/shared/comcol/comcol-forms/comcol-form/comcol-form.component.html +++ b/src/app/shared/comcol/comcol-forms/comcol-form/comcol-form.component.html @@ -11,11 +11,7 @@
- +
@@ -42,17 +38,4 @@ {{ type.value + '.edit.return' | translate }} - - - - - diff --git a/src/app/shared/comcol/comcol-forms/comcol-form/comcol-form.component.spec.ts b/src/app/shared/comcol/comcol-forms/comcol-form/comcol-form.component.spec.ts index 3f74f05118..868134b34a 100644 --- a/src/app/shared/comcol/comcol-forms/comcol-form/comcol-form.component.spec.ts +++ b/src/app/shared/comcol/comcol-forms/comcol-form/comcol-form.component.spec.ts @@ -19,7 +19,7 @@ import { NotificationsServiceStub } from '../../../testing/notifications-service import { VarDirective } from '../../../utils/var.directive'; import { ComColFormComponent } from './comcol-form.component'; import { Operation } from 'fast-json-patch'; -import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../../remote-data.utils'; +import { createSuccessfulRemoteDataObject$ } from '../../../remote-data.utils'; describe('ComColFormComponent', () => { let comp: ComColFormComponent; @@ -61,7 +61,8 @@ describe('ComColFormComponent', () => { const logoEndpoint = 'rest/api/logo/endpoint'; const dsoService = Object.assign({ getLogoEndpoint: () => observableOf(logoEndpoint), - deleteLogo: () => createSuccessfulRemoteDataObject$({}) + deleteLogo: () => createSuccessfulRemoteDataObject$({}), + findById: () => createSuccessfulRemoteDataObject$({}) }); const notificationsService = new NotificationsServiceStub(); @@ -70,7 +71,8 @@ describe('ComColFormComponent', () => { /* eslint-enable no-empty, @typescript-eslint/no-empty-function */ const requestServiceStub = jasmine.createSpyObj('requestService', { - removeByHrefSubstring: {} + removeByHrefSubstring: {}, + setStaleByHrefSubstring: {} }); const objectCacheStub = jasmine.createSpyObj('objectCache', { remove: {} @@ -148,8 +150,6 @@ describe('ComColFormComponent', () => { type: Community.type, } ), - uploader: undefined, - deleteLogo: false, operations: operations, } ); @@ -158,32 +158,22 @@ describe('ComColFormComponent', () => { describe('onCompleteItem', () => { beforeEach(() => { - spyOn(comp.finish, 'emit'); comp.onCompleteItem(); }); it('should show a success notification', () => { expect(notificationsService.success).toHaveBeenCalled(); }); - - it('should emit finish', () => { - expect(comp.finish.emit).toHaveBeenCalled(); - }); }); describe('onUploadError', () => { beforeEach(() => { - spyOn(comp.finish, 'emit'); comp.onUploadError(); }); it('should show an error notification', () => { expect(notificationsService.error).toHaveBeenCalled(); }); - - it('should emit finish', () => { - expect(comp.finish.emit).toHaveBeenCalled(); - }); }); }); @@ -204,6 +194,11 @@ describe('ComColFormComponent', () => { it('should initialize the uploadFilesOptions with a POST method', () => { expect(comp.uploadFilesOptions.method).toEqual(RestRequestMethod.POST); }); + + it('should not show the delete logo button', () => { + const button = fixture.debugElement.query(By.css('#logo-section .btn-danger')); + expect(button).toBeFalsy(); + }); }); describe('and the dso contains a logo', () => { @@ -222,96 +217,71 @@ describe('ComColFormComponent', () => { expect(comp.uploadFilesOptions.url).toEqual(logoEndpoint); }); - it('should initialize the uploadFilesOptions with a PUT method', () => { - expect(comp.uploadFilesOptions.method).toEqual(RestRequestMethod.PUT); + it('should show the delete logo button', () => { + const button = fixture.debugElement.query(By.css('#logo-section .btn-danger')); + expect(button).toBeTruthy(); }); - describe('submit with logo marked for deletion', () => { + describe('when the delete logo button is clicked', () => { beforeEach(() => { - spyOn(dsoService, 'deleteLogo').and.callThrough(); - comp.markLogoForDeletion = true; - }); - - it('should call dsoService.deleteLogo on the DSO', () => { - comp.onSubmit(); + spyOn(dsoService, 'deleteLogo').and.returnValue(createSuccessfulRemoteDataObject$({})); + spyOn(comp, 'handleLogoDeletion').and.callThrough(); + spyOn(comp, 'createConfirmationModal').and.callThrough(); + spyOn(comp, 'subscribeToConfirmationResponse').and.callThrough(); + const deleteButton = fixture.debugElement.query(By.css('#logo-section .btn-danger')); + deleteButton.nativeElement.click(); fixture.detectChanges(); - - expect(dsoService.deleteLogo).toHaveBeenCalledWith(comp.dso); }); - describe('when dsoService.deleteLogo returns a successful response', () => { + it('should create a confirmation modal with the correct labels and properties', () => { + const modalServiceSpy = spyOn((comp as any).modalService, 'open').and.callThrough(); + + const modalRef = comp.createConfirmationModal(); + + expect(modalServiceSpy).toHaveBeenCalled(); + + expect(modalRef).toBeDefined(); + expect(modalRef.componentInstance).toBeDefined(); + + expect(modalRef.componentInstance.headerLabel).toBe('community-collection.edit.logo.delete.title'); + expect(modalRef.componentInstance.infoLabel).toBe('confirmation-modal.delete-community-collection-logo.info'); + expect(modalRef.componentInstance.cancelLabel).toBe('form.cancel'); + expect(modalRef.componentInstance.confirmLabel).toBe('community-collection.edit.logo.delete.title'); + expect(modalRef.componentInstance.confirmIcon).toBe('fas fa-trash'); + }); + + it('should call createConfirmationModal method', () => { + expect(comp.createConfirmationModal).toHaveBeenCalled(); + }); + + it('should call subscribeToConfirmationResponse method', () => { + expect(comp.subscribeToConfirmationResponse).toHaveBeenCalled(); + }); + + describe('when the modal is closed', () => { + + let modalRef; + beforeEach(() => { - dsoService.deleteLogo.and.returnValue(createSuccessfulRemoteDataObject$({})); - comp.onSubmit(); + modalRef = comp.createConfirmationModal(); + comp.subscribeToConfirmationResponse(modalRef); }); - it('should display a success notification', () => { - expect(notificationsService.success).toHaveBeenCalled(); - }); - }); + it('should call handleLogoDeletion and dsoService.deleteLogo methods when deletion is confirmed', waitForAsync(() => { + modalRef.componentInstance.confirmPressed(); - describe('when dsoService.deleteLogo returns an error response', () => { - beforeEach(() => { - dsoService.deleteLogo.and.returnValue(createFailedRemoteDataObject$('Error', 500)); - comp.onSubmit(); - }); + expect(comp.handleLogoDeletion).toHaveBeenCalled(); + expect(dsoService.deleteLogo).toHaveBeenCalled(); - it('should display an error notification', () => { - expect(notificationsService.error).toHaveBeenCalled(); - }); - }); - }); + })); - describe('deleteLogo', () => { - beforeEach(() => { - comp.deleteLogo(); - fixture.detectChanges(); - }); + it('should not call handleLogoDeletion and dsoService.deleteLogo methods when deletion is refused', waitForAsync(() => { + modalRef.componentInstance.cancelPressed(); - it('should set markLogoForDeletion to true', () => { - expect(comp.markLogoForDeletion).toEqual(true); - }); + expect(comp.handleLogoDeletion).not.toHaveBeenCalled(); + expect(dsoService.deleteLogo).not.toHaveBeenCalled(); + })); - it('should mark the logo section with a danger alert', () => { - const logoSection = fixture.debugElement.query(By.css('#logo-section.alert-danger')); - expect(logoSection).toBeTruthy(); - }); - - it('should hide the delete button', () => { - const button = fixture.debugElement.query(By.css('#logo-section .btn-danger')); - expect(button).not.toBeTruthy(); - }); - - it('should show the undo button', () => { - const button = fixture.debugElement.query(By.css('#logo-section .btn-warning')); - expect(button).toBeTruthy(); - }); - }); - - describe('undoDeleteLogo', () => { - beforeEach(() => { - comp.markLogoForDeletion = true; - comp.undoDeleteLogo(); - fixture.detectChanges(); - }); - - it('should set markLogoForDeletion to false', () => { - expect(comp.markLogoForDeletion).toEqual(false); - }); - - it('should disable the danger alert on the logo section', () => { - const logoSection = fixture.debugElement.query(By.css('#logo-section.alert-danger')); - expect(logoSection).not.toBeTruthy(); - }); - - it('should show the delete button', () => { - const button = fixture.debugElement.query(By.css('#logo-section .btn-danger')); - expect(button).toBeTruthy(); - }); - - it('should hide the undo button', () => { - const button = fixture.debugElement.query(By.css('#logo-section .btn-warning')); - expect(button).not.toBeTruthy(); }); }); }); diff --git a/src/app/shared/comcol/comcol-forms/comcol-form/comcol-form.component.ts b/src/app/shared/comcol/comcol-forms/comcol-form/comcol-form.component.ts index d060cb4ad1..cc8539e79b 100644 --- a/src/app/shared/comcol/comcol-forms/comcol-form/comcol-form.component.ts +++ b/src/app/shared/comcol/comcol-forms/comcol-form/comcol-form.component.ts @@ -3,13 +3,12 @@ import { UntypedFormGroup } from '@angular/forms'; import { DynamicFormControlModel, DynamicFormService, DynamicInputModel } from '@ng-dynamic-forms/core'; import { TranslateService } from '@ngx-translate/core'; import { FileUploader } from 'ng2-file-upload'; -import { BehaviorSubject, combineLatest as observableCombineLatest, Subscription } from 'rxjs'; +import { BehaviorSubject, combineLatest as observableCombineLatest, Observable, Subscription } from 'rxjs'; import { AuthService } from '../../../../core/auth/auth.service'; import { ObjectCacheService } from '../../../../core/cache/object-cache.service'; import { ComColDataService } from '../../../../core/data/comcol-data.service'; import { RemoteData } from '../../../../core/data/remote-data'; import { RequestService } from '../../../../core/data/request.service'; -import { RestRequestMethod } from '../../../../core/data/rest-request-method'; import { Bitstream } from '../../../../core/shared/bitstream.model'; import { Collection } from '../../../../core/shared/collection.model'; import { Community } from '../../../../core/shared/community.model'; @@ -22,8 +21,11 @@ import { UploaderComponent } from '../../../upload/uploader/uploader.component'; import { Operation } from 'fast-json-patch'; import { NoContent } from '../../../../core/shared/NoContent.model'; import { getFirstCompletedRemoteData } from '../../../../core/shared/operators'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; import { followLink } from '../../../utils/follow-link-config.model'; +import { ConfirmationModalComponent } from '../../../confirmation-modal/confirmation-modal.component'; +import { map, take, tap } from 'rxjs/operators'; +import { DSpaceObject } from '../../../../core/shared/dspace-object.model'; /** * A form for creating and editing Communities or Collections @@ -92,12 +94,7 @@ export class ComColFormComponent implements On @Output() back: EventEmitter = new EventEmitter(); /** - * Fires an event when the logo has finished uploading (with or without errors) or was removed - */ - @Output() finish: EventEmitter = new EventEmitter(); - - /** - * Observable keeping track whether or not the uploader has finished initializing + * Observable keeping track whether the uploader has finished initializing * Used to start rendering the uploader component */ initializedUploaderOptions = new BehaviorSubject(false); @@ -121,7 +118,7 @@ export class ComColFormComponent implements On protected authService: AuthService, protected requestService: RequestService, protected objectCache: ObjectCacheService, - protected modalService: NgbModal) { + protected modalService: NgbModal){ } ngOnInit(): void { @@ -147,10 +144,6 @@ export class ComColFormComponent implements On ]).subscribe(([href, logoRD]: [string, RemoteData]) => { this.uploadFilesOptions.url = href; this.uploadFilesOptions.authToken = this.authService.buildAuthHeader(); - // If the object already contains a logo, send out a PUT request instead of POST for setting a new logo - // if (hasValue(logoRD.payload)) { - // this.uploadFilesOptions.method = RestRequestMethod.PUT; - // } this.initializedUploaderOptions.next(true); }) ); @@ -224,73 +217,129 @@ export class ComColFormComponent implements On } ); } - /** - * Helper method that provides a modal - */ - openModal(content: any) { - this.modalService.open(content); - } - /** - * Helper method that confirms the deletion of the logo and handles possible errors - */ - confirmLogoDelete(removeLogo: any) { - //this.refreshCache() - this.modalService.open(removeLogo).result.then((result) => { - if (result === 'delete') { - if (hasValue(this.dso.id) && hasValue(this.dso._links.logo)) { - this.dsoService.deleteLogo(this.dso).pipe( - getFirstCompletedRemoteData() - ).subscribe((response: RemoteData) => { - if (response.hasSucceeded) { - this.refreshCache(); - this.notificationsService.success( - this.translate.get(this.type.value + '.edit.logo.notifications.delete.success.title'), - this.translate.get(this.type.value + '.edit.logo.notifications.delete.success.content') - ); - } else { - this.notificationsService.error( - this.translate.get(this.type.value + '.edit.logo.notifications.delete.error.title'), - response.errorMessage - ); - } - this.dso.logo = undefined; - this.uploadFilesOptions.method = RestRequestMethod.POST; - // this.finish.emit(); - }); - } else if (result === 'cancel') { - return; - } - } - - } - ); + /** + * Helper method that confirms the deletion of the logo opening a confirmation modal + */ + confirmLogoDeleteWithModal(): void { + const modalRef = this.createConfirmationModal(); + this.subscribeToConfirmationResponse(modalRef); } /** - * Refresh the object's cache to ensure the latest version + * Creates and opens the confirmation modal + * @returns Reference to the opened modal */ - private refreshCache() { - this.requestService.setStaleByHrefSubstring(this.dso.id); - this.objectCache.remove(this.dso._links.self.href); - this.dsoService.findById(this.dso.id, false, true, followLink('logo')).pipe( - getFirstCompletedRemoteData() - ).subscribe((rd: RemoteData) => { - if (rd.hasSucceeded) { - this.dso = rd.payload; + createConfirmationModal(): NgbModalRef { + const modalRef = this.modalService.open(ConfirmationModalComponent); + modalRef.componentInstance.headerLabel = 'community-collection.edit.logo.delete.title'; + modalRef.componentInstance.infoLabel = 'confirmation-modal.delete-community-collection-logo.info'; + modalRef.componentInstance.cancelLabel = 'form.cancel'; + modalRef.componentInstance.confirmLabel = 'community-collection.edit.logo.delete.title'; + modalRef.componentInstance.confirmIcon = 'fas fa-trash'; + return modalRef; + } + + /** + * Subscribes to the confirmation modal's response and calls the logo deletion handler if confirmed + * @param modalRef References to the opened confirmation modal + */ + subscribeToConfirmationResponse(modalRef: NgbModalRef): void { + modalRef.componentInstance.response.pipe( + take(1) + ).subscribe((confirmed: boolean) => { + if (confirmed) { + this.handleLogoDeletion(); } }); } /** + * Method that confirms the deletion of the logo, handling both possible outcomes + */ + handleLogoDeletion(): void { + if (hasValue(this.dso.id) && hasValue(this.dso._links.logo)) { + this.dsoService.deleteLogo(this.dso).pipe( + getFirstCompletedRemoteData() + ).subscribe((response: RemoteData) => { + const successMessageKey = `${this.type.value}.edit.logo.notifications.delete.success`; + const errorMessageKey = `${this.type.value}.edit.logo.notifications.delete.error`; + + if (response.hasSucceeded) { + this.handleSuccessfulDeletion(successMessageKey); + } else { + this.handleFailedDeletion(errorMessageKey, response.errorMessage); + } + + }); + } + } + + + /** + * Handles successful logo deletion + * @param successMessageKey Translation key for success message + */ + private handleSuccessfulDeletion(successMessageKey: string): void { + this.refreshDsoCache(); + this.notificationsService.success( + this.translate.get(`${successMessageKey}.title`), + this.translate.get(`${successMessageKey}.content`) + ); + } + + /** + * Handles failed logo deletion + * @param errorMessageKey Translation key for error message + * @param errorMessage Error message from the response + */ + private handleFailedDeletion(errorMessageKey: string, errorMessage: string): void { + this.notificationsService.error( + this.translate.get(`${errorMessageKey}.title`), + errorMessage + ); + } + + /** + * Refresh the object's cache to obtain the latest version + */ + private refreshDsoCache() { + this.clearDsoCache(); + return this.fetchUpdatedDso(); + } + + /** + * Clears the cache related to the current dso + */ + private clearDsoCache() { + this.requestService.setStaleByHrefSubstring(this.dso.id); + this.objectCache.remove(this.dso._links.self.href); + } + + /** + * Fetches the latest data for the dso + */ +private fetchUpdatedDso(): Observable { + return this.dsoService.findById(this.dso.id, false, true, followLink('logo')).pipe( + tap((rd: RemoteData) => { + if (rd.hasSucceeded) { + this.dso = rd.payload; + } + }), + map((rd: RemoteData) => rd.hasSucceeded ? rd.payload : null) + ); + } + + + +/** * The request was successful, display a success notification */ public onCompleteItem() { if (hasValue(this.dso.id)) { - this.refreshCache(); + this.refreshDsoCache(); } this.notificationsService.success(null, this.translate.get(this.type.value + '.edit.logo.notifications.add.success')); - // this.finish.emit(); } /** @@ -298,7 +347,6 @@ export class ComColFormComponent implements On */ public onUploadError() { this.notificationsService.error(null, this.translate.get(this.type.value + '.edit.logo.notifications.add.error')); - this.finish.emit(); } /** diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 315fcb6672..7b7609a22e 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1228,8 +1228,12 @@ "community.edit.logo.delete.title": "Delete logo", + "community-collection.edit.logo.delete.title": "Confirm deletion", + "community.edit.logo.delete-undo.title": "Undo delete", + "community-collection.edit.logo.delete-undo.title": "Undo delete", + "community.edit.logo.label": "Community logo", "community.edit.logo.notifications.add.error": "Uploading community logo failed. Please verify the content before retrying.", @@ -1586,6 +1590,8 @@ "confirmation-modal.delete-eperson.confirm": "Delete", + "confirmation-modal.delete-community-collection-logo.info": "Are you sure you want to delete the logo?", + "confirmation-modal.delete-profile.header": "Delete Profile", "confirmation-modal.delete-profile.info": "Are you sure you want to delete your profile",