diff --git a/package-lock.json b/package-lock.json index 5dc582df6..7d6cd64b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "osf", - "version": "26.4.0", + "version": "26.6.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "osf", - "version": "26.4.0", + "version": "26.6.1", "dependencies": { "@angular/animations": "^19.2.0", "@angular/cdk": "^19.2.1", diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index a9ab876ec..34090d83c 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -6,7 +6,14 @@ import { Subject } from 'rxjs'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; -import { NavigationEnd, Router } from '@angular/router'; +import { + NavigationCancel, + NavigationEnd, + NavigationError, + NavigationStart, + ResolveStart, + Router, +} from '@angular/router'; import { CookieConsentBannerComponent } from '@core/components/osf-banners/cookie-consent-banner/cookie-consent-banner.component'; import { ENVIRONMENT } from '@core/provider/environment.provider'; @@ -18,10 +25,12 @@ import { TranslateServiceMock } from '../testing/mocks/translate.service.mock'; import { FullScreenLoaderComponent } from './shared/components/full-screen-loader/full-screen-loader.component'; import { ToastComponent } from './shared/components/toast/toast.component'; import { CustomDialogService } from './shared/services/custom-dialog.service'; +import { LoaderService } from './shared/services/loader.service'; import { AppComponent } from './app.component'; import { OSFTestingModule } from '@testing/osf.testing.module'; import { CustomDialogServiceMockBuilder } from '@testing/providers/custom-dialog-provider.mock'; +import { LoaderServiceMock } from '@testing/providers/loader-service.mock'; import { GoogleTagManagerService } from 'angular-google-tag-manager'; describe('Component: App', () => { @@ -29,6 +38,7 @@ describe('Component: App', () => { let gtmServiceMock: jest.Mocked; let fixture: ComponentFixture; let mockCustomDialogService: ReturnType; + let loaderServiceMock: LoaderServiceMock; beforeEach(async () => { mockCustomDialogService = CustomDialogServiceMockBuilder.create().build(); @@ -37,6 +47,7 @@ describe('Component: App', () => { gtmServiceMock = { pushTag: jest.fn(), } as any; + loaderServiceMock = new LoaderServiceMock(); await TestBed.configureTestingModule({ imports: [ @@ -47,6 +58,7 @@ describe('Component: App', () => { providers: [ provideStore([UserState, UserEmailsState]), MockProvider(CustomDialogService, mockCustomDialogService), + MockProvider(LoaderService, loaderServiceMock), TranslateServiceMock, { provide: GoogleTagManagerService, useValue: gtmServiceMock }, { @@ -103,4 +115,55 @@ describe('Component: App', () => { expect(gtmServiceMock.pushTag).not.toHaveBeenCalled(); }); }); + + describe('Loader routing behavior', () => { + it('should not show loader on NavigationStart', () => { + fixture.detectChanges(); + + routerEvents$.next(new NavigationStart(1, '/next')); + + expect(loaderServiceMock.show).not.toHaveBeenCalled(); + }); + + it('should show loader on ResolveStart', () => { + fixture.detectChanges(); + + routerEvents$.next(new ResolveStart(1, '/next', '/next', {} as any)); + + expect(loaderServiceMock.show).toHaveBeenCalled(); + }); + + it('should hide loader on NavigationEnd after delay', () => { + jest.useFakeTimers(); + fixture.detectChanges(); + + routerEvents$.next(new NavigationEnd(1, '/previous', '/current')); + jest.advanceTimersByTime(500); + + expect(loaderServiceMock.hide).toHaveBeenCalled(); + jest.useRealTimers(); + }); + + it('should hide loader on NavigationCancel after delay', () => { + jest.useFakeTimers(); + fixture.detectChanges(); + + routerEvents$.next(new NavigationCancel(1, '/current', 'cancelled')); + jest.advanceTimersByTime(500); + + expect(loaderServiceMock.hide).toHaveBeenCalled(); + jest.useRealTimers(); + }); + + it('should hide loader on NavigationError after delay', () => { + jest.useFakeTimers(); + fixture.detectChanges(); + + routerEvents$.next(new NavigationError(1, '/current', new Error('test'))); + jest.advanceTimersByTime(500); + + expect(loaderServiceMock.hide).toHaveBeenCalled(); + jest.useRealTimers(); + }); + }); }); diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 03eee262a..b71780b08 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -5,14 +5,7 @@ import { switchMap, timer } from 'rxjs'; import { isPlatformBrowser } from '@angular/common'; import { ChangeDetectionStrategy, Component, DestroyRef, effect, inject, OnInit, PLATFORM_ID } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { - NavigationCancel, - NavigationEnd, - NavigationError, - NavigationStart, - Router, - RouterOutlet, -} from '@angular/router'; +import { NavigationCancel, NavigationEnd, NavigationError, ResolveStart, Router, RouterOutlet } from '@angular/router'; import { ENVIRONMENT } from '@core/provider/environment.provider'; import { GetCurrentUser } from '@core/store/user'; @@ -65,7 +58,7 @@ export class AppComponent implements OnInit { if (this.isBrowser) { this.router.events.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((event) => { - if (event instanceof NavigationStart) { + if (event instanceof ResolveStart) { this.loaderService.show(); } else if ( event instanceof NavigationEnd || diff --git a/src/app/features/preprints/components/stepper/author-assertion-step/author-assertions-step.component.html b/src/app/features/preprints/components/stepper/author-assertion-step/author-assertions-step.component.html index 6840ebace..6aed5bb85 100644 --- a/src/app/features/preprints/components/stepper/author-assertion-step/author-assertions-step.component.html +++ b/src/app/features/preprints/components/stepper/author-assertion-step/author-assertions-step.component.html @@ -226,6 +226,13 @@

{{ 'preprints.preprintStepper.authorAssertions.publicPreregistration.title' severity="info" (onClick)="backButtonClicked()" /> + { expect(emitSpy).toHaveBeenCalled(); }); + it('should emit deleteClicked when deletePreprint is called', () => { + setup({ detectChanges: false }); + const emitSpy = jest.spyOn(component.deleteClicked, 'emit'); + + component.deletePreprint(); + + expect(emitSpy).toHaveBeenCalled(); + }); + it('should handle discard confirmation callbacks when there are unsaved changes', () => { setup(); const emitSpy = jest.spyOn(component.backClicked, 'emit'); diff --git a/src/app/features/preprints/components/stepper/author-assertion-step/author-assertions-step.component.ts b/src/app/features/preprints/components/stepper/author-assertion-step/author-assertions-step.component.ts index e8838344a..e56172b5f 100644 --- a/src/app/features/preprints/components/stepper/author-assertion-step/author-assertions-step.component.ts +++ b/src/app/features/preprints/components/stepper/author-assertion-step/author-assertions-step.component.ts @@ -126,6 +126,7 @@ export class AuthorAssertionsStepComponent { nextClicked = output(); backClicked = output(); + deleteClicked = output(); constructor() { effect(() => { @@ -259,6 +260,10 @@ export class AuthorAssertionsStepComponent { }); } + deletePreprint() { + this.deleteClicked.emit(); + } + private disableAndClearValidators(control: AbstractControl): void { if (control instanceof FormArray) { while (control.length !== 0) { diff --git a/src/app/features/preprints/components/stepper/file-step/file-step.component.html b/src/app/features/preprints/components/stepper/file-step/file-step.component.html index ee8b37860..a83cf434a 100644 --- a/src/app/features/preprints/components/stepper/file-step/file-step.component.html +++ b/src/app/features/preprints/components/stepper/file-step/file-step.component.html @@ -130,15 +130,6 @@

{{ 'preprints.preprintStepper.file.title' | translate }}

{{ preprintFile()!.name }}

- - } @@ -152,6 +143,13 @@

{{ 'preprints.preprintStepper.file.title' | translate }}

severity="info" (onClick)="backButtonClicked()" /> + { expect(emitSpy).toHaveBeenCalled(); }); + it('should emit deleteClicked when deletePreprint is called', () => { + setup({ detectChanges: false }); + const emitSpy = jest.spyOn(component.deleteClicked, 'emit'); + + component.deletePreprint(); + + expect(emitSpy).toHaveBeenCalled(); + }); + it('should handle nextButtonClicked for allowed and blocked states', () => { setup({ detectChanges: false }); const emitSpy = jest.spyOn(component.nextClicked, 'emit'); @@ -303,29 +312,6 @@ describe('FileStepComponent', () => { expect(store.dispatch).toHaveBeenCalledWith(new FetchPreprintPrimaryFile()); }); - it('should set version mode and reset selected source on version file confirmation', () => { - setup({ detectChanges: false }); - - component.versionFile(); - const options = confirmationServiceMock.confirmContinue.mock.calls[0][0]; - options.onConfirm(); - - expect(component.versionFileMode()).toBe(true); - expect(store.dispatch).toHaveBeenCalledWith(new SetSelectedPreprintFileSource(PreprintFileSource.None)); - }); - - it('should not change mode or selected source on version file reject', () => { - setup({ detectChanges: false }); - - component.versionFile(); - const options = confirmationServiceMock.confirmContinue.mock.calls[0][0]; - (store.dispatch as jest.Mock).mockClear(); - options.onReject(); - - expect(component.versionFileMode()).toBe(false); - expect(store.dispatch).not.toHaveBeenCalledWith(new SetSelectedPreprintFileSource(PreprintFileSource.None)); - }); - it('should handle cancelButtonClicked for file present and file missing states', () => { setup({ detectChanges: false }); diff --git a/src/app/features/preprints/components/stepper/file-step/file-step.component.ts b/src/app/features/preprints/components/stepper/file-step/file-step.component.ts index b8f771b61..28d85a2c7 100644 --- a/src/app/features/preprints/components/stepper/file-step/file-step.component.ts +++ b/src/app/features/preprints/components/stepper/file-step/file-step.component.ts @@ -120,6 +120,7 @@ export class FileStepComponent implements OnInit { nextClicked = output(); backClicked = output(); + deleteClicked = output(); isFileSourceSelected = computed(() => this.selectedFileSource() !== PreprintFileSource.None); canProceedToNext = computed(() => !!this.preprintFile() && !this.versionFileMode()); @@ -163,6 +164,10 @@ export class FileStepComponent implements OnInit { this.nextClicked.emit(); } + deletePreprint() { + this.deleteClicked.emit(); + } + onFileSelected(event: Event): void { const input = event.target as HTMLInputElement; const file = input.files?.[0]; @@ -204,18 +209,6 @@ export class FileStepComponent implements OnInit { this.actions.copyFileFromProject(file).subscribe(() => this.actions.fetchPreprintFile()); } - versionFile() { - this.customConfirmationService.confirmContinue({ - headerKey: 'preprints.preprintStepper.file.versionFile.header', - messageKey: 'preprints.preprintStepper.file.versionFile.message', - onConfirm: () => { - this.versionFileMode.set(true); - this.actions.setSelectedFileSource(PreprintFileSource.None); - }, - onReject: () => null, - }); - } - cancelButtonClicked() { if (this.preprintFile()) { return; diff --git a/src/app/features/preprints/components/stepper/preprints-metadata-step/preprints-metadata-step.component.html b/src/app/features/preprints/components/stepper/preprints-metadata-step/preprints-metadata-step.component.html index 624123cc0..21173d764 100644 --- a/src/app/features/preprints/components/stepper/preprints-metadata-step/preprints-metadata-step.component.html +++ b/src/app/features/preprints/components/stepper/preprints-metadata-step/preprints-metadata-step.component.html @@ -106,6 +106,13 @@

{{ 'preprints.preprintStepper.metadata.publicationCitationTitle severity="info" (onClick)="backButtonClicked()" /> + { expect(customConfirmationServiceMock.confirmContinue).not.toHaveBeenCalled(); }); + it('should emit deleteClicked when deletePreprint is called', () => { + setup({ detectChanges: false }); + const emitSpy = jest.spyOn(component.deleteClicked, 'emit'); + + component.deletePreprint(); + + expect(emitSpy).toHaveBeenCalled(); + }); + it('should request confirmation and emit on confirm when there are changes in backButtonClicked', () => { setup(); const backClickedSpy = jest.spyOn(component.backClicked, 'emit'); diff --git a/src/app/features/preprints/components/stepper/preprints-metadata-step/preprints-metadata-step.component.ts b/src/app/features/preprints/components/stepper/preprints-metadata-step/preprints-metadata-step.component.ts index 66be42590..2fde5a640 100644 --- a/src/app/features/preprints/components/stepper/preprints-metadata-step/preprints-metadata-step.component.ts +++ b/src/app/features/preprints/components/stepper/preprints-metadata-step/preprints-metadata-step.component.ts @@ -65,6 +65,7 @@ export class PreprintsMetadataStepComponent implements OnInit { provider = input.required(); nextClicked = output(); backClicked = output(); + deleteClicked = output(); private actions = createDispatchMap({ updatePreprint: UpdatePreprint, @@ -154,6 +155,10 @@ export class PreprintsMetadataStepComponent implements OnInit { }); } + deletePreprint() { + this.deleteClicked.emit(); + } + createLicense(licenseDetails: { id: string; licenseOptions: LicenseOptions }) { this.actions.saveLicense(licenseDetails.id, licenseDetails.licenseOptions); } diff --git a/src/app/features/preprints/components/stepper/review-step/review-step.component.html b/src/app/features/preprints/components/stepper/review-step/review-step.component.html index 236dc3d20..1d4c5be30 100644 --- a/src/app/features/preprints/components/stepper/review-step/review-step.component.html +++ b/src/app/features/preprints/components/stepper/review-step/review-step.component.html @@ -240,6 +240,14 @@

{{ 'preprints.preprintStepper.review.sections.supplements.title' | translate [disabled]="isPreprintSubmitting()" (onClick)="cancelSubmission()" /> + { new LoadMoreBibliographicContributors(undefined, ResourceType.Preprint) ); }); + + it('should emit deleteClicked when deletePreprint is called', () => { + setup({ detectChanges: false }); + const emitSpy = jest.spyOn(component.deleteClicked, 'emit'); + + component.deletePreprint(); + + expect(emitSpy).toHaveBeenCalled(); + }); }); diff --git a/src/app/features/preprints/components/stepper/review-step/review-step.component.ts b/src/app/features/preprints/components/stepper/review-step/review-step.component.ts index 5a37192fb..37c6086fc 100644 --- a/src/app/features/preprints/components/stepper/review-step/review-step.component.ts +++ b/src/app/features/preprints/components/stepper/review-step/review-step.component.ts @@ -9,7 +9,7 @@ import { Tag } from 'primeng/tag'; import { of, switchMap, tap } from 'rxjs'; import { DatePipe, TitleCasePipe } from '@angular/common'; -import { ChangeDetectionStrategy, Component, computed, inject, input, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, inject, input, OnInit, output } from '@angular/core'; import { Router } from '@angular/router'; import { ApplicabilityStatus, PreregLinkInfo, ReviewsState } from '@osf/features/preprints/enums'; @@ -59,6 +59,7 @@ export class ReviewStepComponent implements OnInit { private readonly toastService = inject(ToastService); readonly provider = input.required(); + readonly deleteClicked = output(); private readonly actions = createDispatchMap({ getBibliographicContributors: GetBibliographicContributors, @@ -144,4 +145,8 @@ export class ReviewStepComponent implements OnInit { loadMoreContributors(): void { this.actions.loadMoreBibliographicContributors(this.preprint()?.id, ResourceType.Preprint); } + + deletePreprint(): void { + this.deleteClicked.emit(); + } } diff --git a/src/app/features/preprints/components/stepper/supplements-step/supplements-step.component.html b/src/app/features/preprints/components/stepper/supplements-step/supplements-step.component.html index f90168727..1c6e97f4b 100644 --- a/src/app/features/preprints/components/stepper/supplements-step/supplements-step.component.html +++ b/src/app/features/preprints/components/stepper/supplements-step/supplements-step.component.html @@ -90,6 +90,14 @@

{{ 'preprints.preprintStepper.supplements.title' | translate }}

severity="info" (onClick)="backButtonClicked()" /> + { expect(confirmationMock.confirmContinue).not.toHaveBeenCalled(); }); + it('should emit deleteClicked when deletePreprint is called', () => { + setup({ detectChanges: false }); + const emitSpy = jest.spyOn(component.deleteClicked, 'emit'); + + component.deletePreprint(); + + expect(emitSpy).toHaveBeenCalled(); + }); + it('should compute next button disabled state for create and connect options', () => { setup({ detectChanges: false }); component.selectedSupplementOption.set(SupplementOptions.CreateNewProject); diff --git a/src/app/features/preprints/components/stepper/supplements-step/supplements-step.component.ts b/src/app/features/preprints/components/stepper/supplements-step/supplements-step.component.ts index 04fbb95c3..86ff19826 100644 --- a/src/app/features/preprints/components/stepper/supplements-step/supplements-step.component.ts +++ b/src/app/features/preprints/components/stepper/supplements-step/supplements-step.component.ts @@ -85,6 +85,7 @@ export class SupplementsStepComponent implements OnInit { nextClicked = output(); backClicked = output(); + deleteClicked = output(); readonly SupplementOptions = SupplementOptions; @@ -244,4 +245,8 @@ export class SupplementsStepComponent implements OnInit { this.backClicked.emit(); } + + deletePreprint() { + this.deleteClicked.emit(); + } } diff --git a/src/app/features/preprints/models/preprint-draft-deletion.model.ts b/src/app/features/preprints/models/preprint-draft-deletion.model.ts new file mode 100644 index 000000000..48e7fc501 --- /dev/null +++ b/src/app/features/preprints/models/preprint-draft-deletion.model.ts @@ -0,0 +1,5 @@ +export interface ConfirmDeleteDraftOptions { + onDelete: () => void; + onReset: () => void; + redirectUrl: string; +} diff --git a/src/app/features/preprints/pages/create-new-version/create-new-version.component.html b/src/app/features/preprints/pages/create-new-version/create-new-version.component.html index 42bc5e0e6..38ce8afb7 100644 --- a/src/app/features/preprints/pages/create-new-version/create-new-version.component.html +++ b/src/app/features/preprints/pages/create-new-version/create-new-version.component.html @@ -36,10 +36,15 @@

@switch (currentStep().value) { @case (PreprintSteps.File) { - + } @case (PreprintSteps.Review) { - + } }
diff --git a/src/app/features/preprints/pages/create-new-version/create-new-version.component.spec.ts b/src/app/features/preprints/pages/create-new-version/create-new-version.component.spec.ts index eaeaa8b2d..9ed96a69d 100644 --- a/src/app/features/preprints/pages/create-new-version/create-new-version.component.spec.ts +++ b/src/app/features/preprints/pages/create-new-version/create-new-version.component.spec.ts @@ -17,8 +17,14 @@ import { FileStepComponent, ReviewStepComponent } from '../../components'; import { createNewVersionStepsConst } from '../../constants'; import { PreprintSteps } from '../../enums'; import { PreprintProviderDetails } from '../../models'; +import { PreprintDraftDeletionService } from '../../services/preprint-draft-deletion.service'; import { GetPreprintProviderById, PreprintProvidersSelectors } from '../../store/preprint-providers'; -import { FetchPreprintById, PreprintStepperSelectors, ResetPreprintStepperState } from '../../store/preprint-stepper'; +import { + DeletePreprint, + FetchPreprintById, + PreprintStepperSelectors, + ResetPreprintStepperState, +} from '../../store/preprint-stepper'; import { CreateNewVersionComponent } from './create-new-version.component'; @@ -27,6 +33,10 @@ import { provideOSFCore } from '@testing/osf.testing.provider'; import { BrandServiceMock, BrandServiceMockType } from '@testing/providers/brand-service.mock'; import { BrowserTabServiceMock, BrowserTabServiceMockType } from '@testing/providers/browser-tab-service.mock'; import { HeaderStyleServiceMock, HeaderStyleServiceMockType } from '@testing/providers/header-style-service.mock'; +import { + PreprintDraftDeletionServiceMock, + PreprintDraftDeletionServiceMockType, +} from '@testing/providers/preprint-draft-deletion-provider.mock'; import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; import { RouterMockBuilder, RouterMockType } from '@testing/providers/router-provider.mock'; import { mergeSignalOverrides, provideMockStore, SignalOverride } from '@testing/providers/store-provider.mock'; @@ -39,6 +49,7 @@ describe('CreateNewVersionComponent', () => { let brandServiceMock: BrandServiceMockType; let headerStyleMock: HeaderStyleServiceMockType; let browserTabMock: BrowserTabServiceMockType; + let draftDeletionMock: PreprintDraftDeletionServiceMockType; const mockProvider: PreprintProviderDetails = PREPRINT_PROVIDER_DETAILS_MOCK; const mockProviderId = 'osf'; @@ -62,6 +73,7 @@ describe('CreateNewVersionComponent', () => { brandServiceMock = BrandServiceMock.simple(); headerStyleMock = HeaderStyleServiceMock.simple(); browserTabMock = BrowserTabServiceMock.simple(); + draftDeletionMock = PreprintDraftDeletionServiceMock.simple(); TestBed.configureTestingModule({ imports: [CreateNewVersionComponent, ...MockComponents(StepperComponent, FileStepComponent, ReviewStepComponent)], @@ -77,6 +89,12 @@ describe('CreateNewVersionComponent', () => { ], }); + TestBed.overrideComponent(CreateNewVersionComponent, { + set: { + providers: [{ provide: PreprintDraftDeletionService, useValue: draftDeletionMock }], + }, + }); + store = TestBed.inject(Store); fixture = TestBed.createComponent(CreateNewVersionComponent); component = fixture.componentInstance; @@ -111,7 +129,7 @@ describe('CreateNewVersionComponent', () => { expect(browserTabMock.updateTabStyles).toHaveBeenCalledWith(mockProvider.faviconUrl, mockProvider.name); }); - it('should reset services on destroy', () => { + it('should reset services, delegate destroy delete, and reset stepper state', () => { setup(); component.ngOnDestroy(); @@ -119,6 +137,8 @@ describe('CreateNewVersionComponent', () => { expect(headerStyleMock.resetToDefaults).toHaveBeenCalled(); expect(brandServiceMock.resetBranding).toHaveBeenCalled(); expect(browserTabMock.resetToDefaults).toHaveBeenCalled(); + expect(draftDeletionMock.deleteOnDestroyIfNeeded).toHaveBeenCalledWith(expect.any(Function)); + expect(store.dispatch).toHaveBeenCalledWith(new DeletePreprint()); expect(store.dispatch).toHaveBeenCalledWith(new ResetPreprintStepperState()); }); @@ -140,16 +160,20 @@ describe('CreateNewVersionComponent', () => { expect(event.preventDefault).not.toHaveBeenCalled(); }); - it('should prevent deactivation when not submitted', () => { + it('should delegate canDeactivate to PreprintDraftDeletionService', () => { setup(); - expect(component.canDeactivate()).toBe(false); + component.canDeactivate(); + + expect(draftDeletionMock.canDeactivate).toHaveBeenCalledWith(false); }); - it('should allow deactivation when submitted', () => { + it('should pass submitted state to canDeactivate on PreprintDraftDeletionService', () => { setup({ selectorOverrides: [{ selector: PreprintStepperSelectors.hasBeenSubmitted, value: true }] }); - expect(component.canDeactivate()).toBe(true); + component.canDeactivate(); + + expect(draftDeletionMock.canDeactivate).toHaveBeenCalledWith(true); }); it('should ignore stepping forward via stepper', () => { @@ -193,4 +217,37 @@ describe('CreateNewVersionComponent', () => { expect(routerMock.navigate).toHaveBeenCalledWith([mockPreprintId.split('_')[0]]); }); + + it('should call confirmDeleteDraft on PreprintDraftDeletionService with my-preprints redirect', () => { + setup(); + + component.requestDeletePreprint(); + + expect(draftDeletionMock.confirmDeleteDraft).toHaveBeenCalledWith( + expect.objectContaining({ + redirectUrl: '/my-preprints', + onDelete: expect.any(Function), + onReset: expect.any(Function), + }) + ); + }); + + it('should allow deactivation when draft deletion service reports deleted', () => { + setup(); + draftDeletionMock.deleted = true; + + expect(component.canDeactivate()).toBe(true); + }); + + it('should skip destroy delete when draft already deleted', () => { + setup(); + draftDeletionMock.deleted = true; + (store.dispatch as jest.Mock).mockClear(); + + component.ngOnDestroy(); + + expect(draftDeletionMock.deleteOnDestroyIfNeeded).toHaveBeenCalledWith(expect.any(Function)); + expect((store.dispatch as jest.Mock).mock.calls.some(([action]) => action instanceof DeletePreprint)).toBe(false); + expect(store.dispatch).toHaveBeenCalledWith(new ResetPreprintStepperState()); + }); }); diff --git a/src/app/features/preprints/pages/create-new-version/create-new-version.component.ts b/src/app/features/preprints/pages/create-new-version/create-new-version.component.ts index 70176a373..2dffaf6e3 100644 --- a/src/app/features/preprints/pages/create-new-version/create-new-version.component.ts +++ b/src/app/features/preprints/pages/create-new-version/create-new-version.component.ts @@ -30,8 +30,14 @@ import { HeaderStyleService } from '@osf/shared/services/header-style.service'; import { FileStepComponent, ReviewStepComponent } from '../../components'; import { createNewVersionStepsConst } from '../../constants'; import { PreprintSteps } from '../../enums'; +import { PreprintDraftDeletionService } from '../../services/preprint-draft-deletion.service'; import { GetPreprintProviderById, PreprintProvidersSelectors } from '../../store/preprint-providers'; -import { FetchPreprintById, PreprintStepperSelectors, ResetPreprintStepperState } from '../../store/preprint-stepper'; +import { + DeletePreprint, + FetchPreprintById, + PreprintStepperSelectors, + ResetPreprintStepperState, +} from '../../store/preprint-stepper'; @Component({ selector: 'osf-create-new-version', @@ -39,6 +45,7 @@ import { FetchPreprintById, PreprintStepperSelectors, ResetPreprintStepperState templateUrl: './create-new-version.component.html', styleUrl: './create-new-version.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, + providers: [PreprintDraftDeletionService], }) export class CreateNewVersionComponent implements OnDestroy, CanDeactivateComponent { @HostBinding('class') classes = 'flex-1 flex flex-column w-full'; @@ -48,6 +55,7 @@ export class CreateNewVersionComponent implements OnDestroy, CanDeactivateCompon private readonly brandService = inject(BrandService); private readonly headerStyleHelper = inject(HeaderStyleService); private readonly browserTabHelper = inject(BrowserTabService); + private readonly draftDeletionService = inject(PreprintDraftDeletionService); private readonly providerId = toSignal(this.route.params.pipe(map((params) => params['providerId']))); private readonly preprintId = toSignal(this.route.params.pipe(map((params) => params['preprintId']))); @@ -56,6 +64,7 @@ export class CreateNewVersionComponent implements OnDestroy, CanDeactivateCompon getPreprintProviderById: GetPreprintProviderById, fetchPreprint: FetchPreprintById, resetState: ResetPreprintStepperState, + deletePreprint: DeletePreprint, }); readonly preprintProvider = select(PreprintProvidersSelectors.getPreprintProviderDetails(this.providerId())); @@ -98,11 +107,14 @@ export class CreateNewVersionComponent implements OnDestroy, CanDeactivateCompon this.headerStyleHelper.resetToDefaults(); this.brandService.resetBranding(); this.browserTabHelper.resetToDefaults(); + + this.draftDeletionService.deleteOnDestroyIfNeeded(() => this.actions.deletePreprint()); + this.actions.resetState(); } canDeactivate(): boolean { - return this.hasBeenSubmitted(); + return this.draftDeletionService.canDeactivate(this.hasBeenSubmitted()); } stepChange(step: StepOption): void { @@ -128,4 +140,12 @@ export class CreateNewVersionComponent implements OnDestroy, CanDeactivateCompon this.router.navigate([id]); } } + + requestDeletePreprint(): void { + this.draftDeletionService.confirmDeleteDraft({ + onDelete: () => this.actions.deletePreprint(), + onReset: () => this.actions.resetState(), + redirectUrl: '/my-preprints', + }); + } } diff --git a/src/app/features/preprints/pages/preprint-details/preprint-details.component.ts b/src/app/features/preprints/pages/preprint-details/preprint-details.component.ts index 279e56a90..6f4ef2c67 100644 --- a/src/app/features/preprints/pages/preprint-details/preprint-details.component.ts +++ b/src/app/features/preprints/pages/preprint-details/preprint-details.component.ts @@ -32,6 +32,7 @@ import { UserSelectors } from '@core/store/user'; import { ReviewPermissions } from '@osf/shared/enums/review-permissions.enum'; import { CustomDialogService } from '@osf/shared/services/custom-dialog.service'; import { DataciteService } from '@osf/shared/services/datacite/datacite.service'; +import { LoaderService } from '@osf/shared/services/loader.service'; import { MetaTagsService } from '@osf/shared/services/meta-tags.service'; import { MetaTagsBuilderService } from '@osf/shared/services/meta-tags-builder.service'; import { SignpostingService } from '@osf/shared/services/signposting.service'; @@ -101,6 +102,7 @@ export class PreprintDetailsComponent implements OnInit, OnDestroy { private readonly dataciteService = inject(DataciteService); private readonly prerenderReady = inject(PrerenderReadyService); private readonly signpostingService = inject(SignpostingService); + private readonly loaderService = inject(LoaderService); private readonly environment = inject(ENVIRONMENT); private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID)); @@ -353,10 +355,14 @@ export class PreprintDetailsComponent implements OnInit, OnDestroy { return; } + this.loaderService.show(); + this.actions .createNewVersion(preprintId) .pipe( catchError((e) => { + this.loaderService.hide(); + if (e instanceof HttpErrorResponse && e.status === 409) { this.toastService.showError(e.error.errors[0].detail); } @@ -366,6 +372,8 @@ export class PreprintDetailsComponent implements OnInit, OnDestroy { ) .subscribe({ complete: () => { + this.loaderService.hide(); + const newVersionPreprint = this.store.selectSnapshot(PreprintStepperSelectors.getPreprint); if (newVersionPreprint?.id) { this.router.navigate(['preprints', this.providerId(), 'new-version', newVersionPreprint.id]); diff --git a/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.html b/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.html index 38a89d790..b69b74be7 100644 --- a/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.html +++ b/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.html @@ -39,23 +39,37 @@

} @case (PreprintSteps.File) { - + } @case (PreprintSteps.Metadata) { } @case (PreprintSteps.AuthorAssertions) { - + } @case (PreprintSteps.Supplements) { - + } @case (PreprintSteps.Review) { - + } } diff --git a/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.spec.ts b/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.spec.ts index 9803536df..5a2be1117 100644 --- a/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.spec.ts +++ b/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.spec.ts @@ -24,6 +24,7 @@ import { import { submitPreprintSteps } from '../../constants'; import { PreprintSteps } from '../../enums'; import { PreprintProviderDetails } from '../../models'; +import { PreprintDraftDeletionService } from '../../services/preprint-draft-deletion.service'; import { GetPreprintProviderById, PreprintProvidersSelectors } from '../../store/preprint-providers'; import { DeletePreprint, PreprintStepperSelectors, ResetPreprintStepperState } from '../../store/preprint-stepper'; @@ -34,6 +35,10 @@ import { provideOSFCore } from '@testing/osf.testing.provider'; import { BrandServiceMock, BrandServiceMockType } from '@testing/providers/brand-service.mock'; import { BrowserTabServiceMock, BrowserTabServiceMockType } from '@testing/providers/browser-tab-service.mock'; import { HeaderStyleServiceMock, HeaderStyleServiceMockType } from '@testing/providers/header-style-service.mock'; +import { + PreprintDraftDeletionServiceMock, + PreprintDraftDeletionServiceMockType, +} from '@testing/providers/preprint-draft-deletion-provider.mock'; import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; import { mergeSignalOverrides, provideMockStore, SignalOverride } from '@testing/providers/store-provider.mock'; @@ -44,6 +49,7 @@ describe('SubmitPreprintStepperComponent', () => { let brandServiceMock: BrandServiceMockType; let headerStyleMock: HeaderStyleServiceMockType; let browserTabMock: BrowserTabServiceMockType; + let draftDeletionMock: PreprintDraftDeletionServiceMockType; const mockProvider: PreprintProviderDetails = PREPRINT_PROVIDER_DETAILS_MOCK; const mockProviderId = 'osf'; @@ -62,6 +68,7 @@ describe('SubmitPreprintStepperComponent', () => { brandServiceMock = BrandServiceMock.simple(); headerStyleMock = HeaderStyleServiceMock.simple(); browserTabMock = BrowserTabServiceMock.simple(); + draftDeletionMock = PreprintDraftDeletionServiceMock.simple(); TestBed.configureTestingModule({ imports: [ @@ -87,6 +94,12 @@ describe('SubmitPreprintStepperComponent', () => { ], }); + TestBed.overrideComponent(SubmitPreprintStepperComponent, { + set: { + providers: [{ provide: PreprintDraftDeletionService, useValue: draftDeletionMock }], + }, + }); + store = TestBed.inject(Store); fixture = TestBed.createComponent(SubmitPreprintStepperComponent); component = fixture.componentInstance; @@ -119,7 +132,7 @@ describe('SubmitPreprintStepperComponent', () => { expect(browserTabMock.updateTabStyles).toHaveBeenCalledWith(mockProvider.faviconUrl, mockProvider.name); }); - it('should reset services and delete preprint on destroy', () => { + it('should reset services, delegate destroy delete, and reset stepper state', () => { setup(); component.ngOnDestroy(); @@ -127,6 +140,7 @@ describe('SubmitPreprintStepperComponent', () => { expect(headerStyleMock.resetToDefaults).toHaveBeenCalled(); expect(brandServiceMock.resetBranding).toHaveBeenCalled(); expect(browserTabMock.resetToDefaults).toHaveBeenCalled(); + expect(draftDeletionMock.deleteOnDestroyIfNeeded).toHaveBeenCalledWith(expect.any(Function)); expect(store.dispatch).toHaveBeenCalledWith(new DeletePreprint()); expect(store.dispatch).toHaveBeenCalledWith(new ResetPreprintStepperState()); }); @@ -194,16 +208,20 @@ describe('SubmitPreprintStepperComponent', () => { expect(event.preventDefault).not.toHaveBeenCalled(); }); - it('should prevent deactivation when not submitted', () => { + it('should delegate canDeactivate to PreprintDraftDeletionService', () => { setup(); - expect(component.canDeactivate()).toBe(false); + component.canDeactivate(); + + expect(draftDeletionMock.canDeactivate).toHaveBeenCalledWith(false); }); - it('should allow deactivation when submitted', () => { + it('should allow deactivation when submitted via PreprintDraftDeletionService', () => { setup({ selectorOverrides: [{ selector: PreprintStepperSelectors.hasBeenSubmitted, value: true }] }); - expect(component.canDeactivate()).toBe(true); + component.canDeactivate(); + + expect(draftDeletionMock.canDeactivate).toHaveBeenCalledWith(true); }); it('should ignore stepping forward via stepper', () => { @@ -262,4 +280,37 @@ describe('SubmitPreprintStepperComponent', () => { expect(component.currentStep()).toEqual(firstStep); }); + + it('should call confirmDeleteDraft on PreprintDraftDeletionService with preprints redirect', () => { + setup(); + + component.requestDeletePreprint(); + + expect(draftDeletionMock.confirmDeleteDraft).toHaveBeenCalledWith( + expect.objectContaining({ + redirectUrl: '/preprints', + onDelete: expect.any(Function), + onReset: expect.any(Function), + }) + ); + }); + + it('should allow deactivation when draft deletion service reports deleted', () => { + setup(); + draftDeletionMock.deleted = true; + + expect(component.canDeactivate()).toBe(true); + }); + + it('should skip destroy delete when draft already deleted', () => { + setup(); + draftDeletionMock.deleted = true; + (store.dispatch as jest.Mock).mockClear(); + + component.ngOnDestroy(); + + expect(draftDeletionMock.deleteOnDestroyIfNeeded).toHaveBeenCalledWith(expect.any(Function)); + expect((store.dispatch as jest.Mock).mock.calls.some(([action]) => action instanceof DeletePreprint)).toBe(false); + expect(store.dispatch).toHaveBeenCalledWith(new ResetPreprintStepperState()); + }); }); diff --git a/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.ts b/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.ts index bb0e6f7a2..c7ce9f79e 100644 --- a/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.ts +++ b/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.ts @@ -39,6 +39,7 @@ import { } from '../../components'; import { submitPreprintSteps } from '../../constants'; import { PreprintSteps } from '../../enums'; +import { PreprintDraftDeletionService } from '../../services/preprint-draft-deletion.service'; import { GetPreprintProviderById, PreprintProvidersSelectors } from '../../store/preprint-providers'; import { DeletePreprint, PreprintStepperSelectors, ResetPreprintStepperState } from '../../store/preprint-stepper'; @@ -58,15 +59,17 @@ import { DeletePreprint, PreprintStepperSelectors, ResetPreprintStepperState } f templateUrl: './submit-preprint-stepper.component.html', styleUrl: './submit-preprint-stepper.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, + providers: [PreprintDraftDeletionService], }) export class SubmitPreprintStepperComponent implements OnDestroy, CanDeactivateComponent { @HostBinding('class') classes = 'flex-1 flex flex-column w-full'; - private readonly route = inject(ActivatedRoute); private readonly document = inject(DOCUMENT); + private readonly route = inject(ActivatedRoute); private readonly brandService = inject(BrandService); private readonly headerStyleHelper = inject(HeaderStyleService); private readonly browserTabHelper = inject(BrowserTabService); + private readonly draftDeletionService = inject(PreprintDraftDeletionService); private providerId = toSignal(this.route.params.pipe(map((params) => params['providerId']))); @@ -76,14 +79,16 @@ export class SubmitPreprintStepperComponent implements OnDestroy, CanDeactivateC deletePreprint: DeletePreprint, }); - readonly PreprintSteps = PreprintSteps; - preprintProvider = select(PreprintProvidersSelectors.getPreprintProviderDetails(this.providerId())); isPreprintProviderLoading = select(PreprintProvidersSelectors.isPreprintProviderDetailsLoading); hasBeenSubmitted = select(PreprintStepperSelectors.hasBeenSubmitted); + currentStep = signal(submitPreprintSteps[0]); + isWeb = toSignal(inject(IS_WEB)); + readonly PreprintSteps = PreprintSteps; + readonly steps = computed(() => { const provider = this.preprintProvider(); @@ -122,17 +127,27 @@ export class SubmitPreprintStepperComponent implements OnDestroy, CanDeactivateC } canDeactivate(): boolean { - return this.hasBeenSubmitted(); + return this.draftDeletionService.canDeactivate(this.hasBeenSubmitted()); } ngOnDestroy() { this.headerStyleHelper.resetToDefaults(); this.brandService.resetBranding(); this.browserTabHelper.resetToDefaults(); - this.actions.deletePreprint(); + + this.draftDeletionService.deleteOnDestroyIfNeeded(() => this.actions.deletePreprint()); + this.actions.resetState(); } + requestDeletePreprint(): void { + this.draftDeletionService.confirmDeleteDraft({ + onDelete: () => this.actions.deletePreprint(), + onReset: () => this.actions.resetState(), + redirectUrl: '/preprints', + }); + } + stepChange(step: StepOption): void { if (step.index >= this.currentStep().index) { return; diff --git a/src/app/features/preprints/services/preprint-draft-deletion.service.spec.ts b/src/app/features/preprints/services/preprint-draft-deletion.service.spec.ts new file mode 100644 index 000000000..b523afea9 --- /dev/null +++ b/src/app/features/preprints/services/preprint-draft-deletion.service.spec.ts @@ -0,0 +1,96 @@ +import { MockProvider } from 'ng-mocks'; + +import { TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; + +import { CustomConfirmationService } from '@osf/shared/services/custom-confirmation.service'; +import { ToastService } from '@osf/shared/services/toast.service'; + +import { PreprintDraftDeletionService } from './preprint-draft-deletion.service'; + +import { provideOSFCore } from '@testing/osf.testing.provider'; +import { + CustomConfirmationServiceMock, + CustomConfirmationServiceMockType, +} from '@testing/providers/custom-confirmation-provider.mock'; +import { RouterMockBuilder, RouterMockType } from '@testing/providers/router-provider.mock'; +import { ToastServiceMock, ToastServiceMockType } from '@testing/providers/toast-provider.mock'; + +describe('PreprintDraftDeletionService', () => { + let service: PreprintDraftDeletionService; + let confirmationMock: CustomConfirmationServiceMockType; + let toastMock: ToastServiceMockType; + let routerMock: RouterMockType; + + beforeEach(() => { + confirmationMock = CustomConfirmationServiceMock.simple(); + toastMock = ToastServiceMock.simple(); + routerMock = RouterMockBuilder.create().build(); + + TestBed.configureTestingModule({ + providers: [ + provideOSFCore(), + PreprintDraftDeletionService, + MockProvider(CustomConfirmationService, confirmationMock), + MockProvider(ToastService, toastMock), + MockProvider(Router, routerMock), + ], + }); + + service = TestBed.inject(PreprintDraftDeletionService); + }); + + it('should create', () => { + expect(service).toBeTruthy(); + }); + + it('should open confirm delete and run delete, reset, toast, navigate on confirm', () => { + const onDelete = jest.fn(); + const onReset = jest.fn(); + + service.confirmDeleteDraft({ + onDelete, + onReset, + redirectUrl: '/preprints', + }); + + expect(confirmationMock.confirmDelete).toHaveBeenCalledWith({ + headerKey: 'preprints.preprintStepper.deleteDraft.header', + messageKey: 'preprints.preprintStepper.deleteDraft.message', + onConfirm: expect.any(Function), + }); + + const { onConfirm } = confirmationMock.confirmDelete.mock.calls[0][0]; + onConfirm(); + + expect(onDelete).toHaveBeenCalled(); + expect(onReset).toHaveBeenCalled(); + expect(toastMock.showSuccess).toHaveBeenCalledWith('preprints.preprintStepper.deleteDraft.success'); + expect(routerMock.navigateByUrl).toHaveBeenCalledWith('/preprints'); + }); + + it('should allow canDeactivate and skip deleteOnDestroy after confirmed delete', () => { + const onDelete = jest.fn(); + const onReset = jest.fn(); + + service.confirmDeleteDraft({ onDelete, onReset, redirectUrl: '/x' }); + const { onConfirm } = confirmationMock.confirmDelete.mock.calls[0][0]; + onConfirm(); + + expect(service.canDeactivate(false)).toBe(true); + + const destroyDelete = jest.fn(); + service.deleteOnDestroyIfNeeded(destroyDelete); + expect(destroyDelete).not.toHaveBeenCalled(); + }); + + it('should return canDeactivate true when submitted', () => { + expect(service.canDeactivate(true)).toBe(true); + }); + + it('should call deleteOnDestroy when not yet deleted', () => { + const destroyDelete = jest.fn(); + service.deleteOnDestroyIfNeeded(destroyDelete); + expect(destroyDelete).toHaveBeenCalled(); + }); +}); diff --git a/src/app/features/preprints/services/preprint-draft-deletion.service.ts b/src/app/features/preprints/services/preprint-draft-deletion.service.ts new file mode 100644 index 000000000..1843e742b --- /dev/null +++ b/src/app/features/preprints/services/preprint-draft-deletion.service.ts @@ -0,0 +1,40 @@ +import { inject, Injectable } from '@angular/core'; +import { Router } from '@angular/router'; + +import { CustomConfirmationService } from '@osf/shared/services/custom-confirmation.service'; +import { ToastService } from '@osf/shared/services/toast.service'; + +import { ConfirmDeleteDraftOptions } from '../models/preprint-draft-deletion.model'; + +@Injectable() +export class PreprintDraftDeletionService { + private readonly customConfirmationService = inject(CustomConfirmationService); + private readonly toastService = inject(ToastService); + private readonly router = inject(Router); + + private preprintDeleted = false; + + canDeactivate(hasBeenSubmitted: boolean): boolean { + return hasBeenSubmitted || this.preprintDeleted; + } + + deleteOnDestroyIfNeeded(onDelete: () => void): void { + if (!this.preprintDeleted) { + onDelete(); + } + } + + confirmDeleteDraft(options: ConfirmDeleteDraftOptions): void { + this.customConfirmationService.confirmDelete({ + headerKey: 'preprints.preprintStepper.deleteDraft.header', + messageKey: 'preprints.preprintStepper.deleteDraft.message', + onConfirm: () => { + this.preprintDeleted = true; + options.onDelete(); + options.onReset(); + this.toastService.showSuccess('preprints.preprintStepper.deleteDraft.success'); + this.router.navigateByUrl(options.redirectUrl); + }, + }); + } +} diff --git a/src/app/features/preprints/services/preprints.service.ts b/src/app/features/preprints/services/preprints.service.ts index d9edd0e91..fad44bbdd 100644 --- a/src/app/features/preprints/services/preprints.service.ts +++ b/src/app/features/preprints/services/preprints.service.ts @@ -156,7 +156,7 @@ export class PreprintsService { ApiData, null > - >(`${this.apiUrl}/preprints/${prevVersionPreprintId}/versions/?version=2.20`) + >(`${this.apiUrl}/preprints/${prevVersionPreprintId}/versions/`) .pipe(map((response) => PreprintsMapper.fromPreprintJsonApi(response.data))); } diff --git a/src/app/features/preprints/store/preprint-stepper/preprint-stepper.state.ts b/src/app/features/preprints/store/preprint-stepper/preprint-stepper.state.ts index cc817558d..f71e0b265 100644 --- a/src/app/features/preprints/store/preprint-stepper/preprint-stepper.state.ts +++ b/src/app/features/preprints/store/preprint-stepper/preprint-stepper.state.ts @@ -166,12 +166,12 @@ export class PreprintStepperState { ctx.setState(patch({ preprintFile: patch({ isLoading: true }) })); return this.fileService.updateFileContent(action.file, uploadedFile.links.upload).pipe( - switchMap(() => { - if (uploadedFile.name !== action.file.name) { - return this.fileService.renameEntry(uploadedFile.links.upload, action.file.name, 'replace'); - } - return EMPTY; - }) + switchMap(() => + uploadedFile.name !== action.file.name + ? this.fileService.renameEntry(uploadedFile.links.upload, action.file.name, 'replace') + : of(null) + ), + catchError((error) => handleSectionError(ctx, 'preprintFile', error)) ); } diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index e31380f1c..1ac2ac1d2 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -2279,6 +2279,11 @@ "description": "This will make your project public, if it is not already", "subDescription": "The projects and components for which you have admin access are listed below." }, + "deleteDraft": { + "header": "Delete Preprint Draft", + "message": "Are you sure you want to delete this preprint draft?", + "success": "Preprint draft successfully deleted." + }, "common": { "validation": { "fillRequiredFields": "Fill in “Required” fields to continue" diff --git a/src/testing/providers/preprint-draft-deletion-provider.mock.ts b/src/testing/providers/preprint-draft-deletion-provider.mock.ts new file mode 100644 index 000000000..1bfec646d --- /dev/null +++ b/src/testing/providers/preprint-draft-deletion-provider.mock.ts @@ -0,0 +1,24 @@ +import { PreprintDraftDeletionService } from '@osf/features/preprints/services/preprint-draft-deletion.service'; + +export type PreprintDraftDeletionServiceMockType = Partial & { + deleted: boolean; + confirmDeleteDraft: jest.Mock; + canDeactivate: jest.Mock; + deleteOnDestroyIfNeeded: jest.Mock; +}; + +export const PreprintDraftDeletionServiceMock = { + simple(): PreprintDraftDeletionServiceMockType { + const service: PreprintDraftDeletionServiceMockType = { + deleted: false, + confirmDeleteDraft: jest.fn(), + canDeactivate: jest.fn((submitted: boolean) => submitted || service.deleted), + deleteOnDestroyIfNeeded: jest.fn((onDelete: () => void) => { + if (!service.deleted) { + onDelete(); + } + }), + }; + return service; + }, +};