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 6aed5bb85..4558f4e58 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,13 +226,15 @@

{{ 'preprints.preprintStepper.authorAssertions.publicPreregistration.title' severity="info" (onClick)="backButtonClicked()" /> - + @if (showDeleteButton()) { + + } { expect(controls.preregLinkInfo.value).toBe(PreregLinkInfo.Both); }); + it('should default showDeleteButton to false', () => { + setup(); + + expect(component.showDeleteButton()).toBe(false); + }); + + it('should update showDeleteButton when input changes', () => { + setup(); + + fixture.componentRef.setInput('showDeleteButton', true); + fixture.detectChanges(); + + expect(component.showDeleteButton()).toBe(true); + }); + it('should enable coiStatement control when hasCoi becomes true', () => { setup({ detectChanges: true }); component.authorAssertionsForm.controls.hasCoi.setValue(true); 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 e56172b5f..c89b207b5 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 @@ -10,7 +10,7 @@ import { Textarea } from 'primeng/textarea'; import { Tooltip } from 'primeng/tooltip'; import { NgClass } from '@angular/common'; -import { ChangeDetectionStrategy, Component, effect, inject, output } from '@angular/core'; +import { ChangeDetectionStrategy, Component, effect, inject, input, output } from '@angular/core'; import { toSignal } from '@angular/core/rxjs-interop'; import { AbstractControl, @@ -124,6 +124,7 @@ export class AuthorAssertionsStepComponent { initialValue: this.createdPreprint()?.hasPreregLinks ?? ApplicabilityStatus.NotApplicable, }); + showDeleteButton = input(false); nextClicked = output(); backClicked = output(); deleteClicked = output(); 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 a83cf434a..e6ee28e98 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 @@ -143,13 +143,15 @@

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

severity="info" (onClick)="backButtonClicked()" /> - + @if (showDeleteButton()) { + + } { let fixture: ComponentFixture; let store: Store; let toastServiceMock: ToastServiceMockType; - let confirmationServiceMock: CustomConfirmationServiceMockType; const originalPointerEvent = (globalThis as unknown as { PointerEvent?: typeof Event }).PointerEvent; const mockProvider: PreprintProviderDetails = PREPRINT_PROVIDER_DETAILS_MOCK; @@ -81,16 +75,10 @@ describe('FileStepComponent', () => { }) { const signals = mergeSignalOverrides(defaultSignals, overrides?.selectorOverrides); toastServiceMock = ToastServiceMock.simple(); - confirmationServiceMock = CustomConfirmationServiceMock.simple(); TestBed.configureTestingModule({ imports: [FileStepComponent, ...MockComponents(IconComponent, FilesTreeComponent)], - providers: [ - provideOSFCore(), - MockProvider(ToastService, toastServiceMock), - MockProvider(CustomConfirmationService, confirmationServiceMock), - provideMockStore({ signals }), - ], + providers: [provideOSFCore(), MockProvider(ToastService, toastServiceMock), provideMockStore({ signals })], }); store = TestBed.inject(Store); @@ -348,4 +336,19 @@ describe('FileStepComponent', () => { expect(store.dispatch).toHaveBeenCalledWith(new FetchProjectFilesByLink('/v2/nodes/node-456/files/', 3)); }); + + it('should default showDeleteButton to false', () => { + setup(); + + expect(component.showDeleteButton()).toBe(false); + }); + + it('should update showDeleteButton when input changes', () => { + setup(); + + fixture.componentRef.setInput('showDeleteButton', true); + fixture.detectChanges(); + + expect(component.showDeleteButton()).toBe(true); + }); }); 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 28d85a2c7..48f87991d 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 @@ -46,7 +46,6 @@ import { ClearFileDirective } from '@osf/shared/directives/clear-file.directive' import { StringOrNull } from '@osf/shared/helpers/types.helper'; import { FileModel } from '@osf/shared/models/files/file.model'; import { FileFolderModel } from '@osf/shared/models/files/file-folder.model'; -import { CustomConfirmationService } from '@osf/shared/services/custom-confirmation.service'; import { ToastService } from '@osf/shared/services/toast.service'; @Component({ @@ -71,7 +70,6 @@ import { ToastService } from '@osf/shared/services/toast.service'; }) export class FileStepComponent implements OnInit { private toastService = inject(ToastService); - private customConfirmationService = inject(CustomConfirmationService); private destroyRef = inject(DestroyRef); private actions = createDispatchMap({ @@ -90,6 +88,7 @@ export class FileStepComponent implements OnInit { readonly PreprintFileSource = PreprintFileSource; provider = input.required(); + showDeleteButton = input(false); preprint = select(PreprintStepperSelectors.getPreprint); selectedFileSource = select(PreprintStepperSelectors.getSelectedFileSource); fileUploadLink = select(PreprintStepperSelectors.getUploadLink); 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 21173d764..21f2819bc 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,13 +106,15 @@

{{ 'preprints.preprintStepper.metadata.publicationCitationTitle severity="info" (onClick)="backButtonClicked()" /> - + @if (showDeleteButton()) { + + } { expect(nextClickedSpy).toHaveBeenCalled(); }); + it('should default showDeleteButton to false', () => { + setup(); + + expect(component.showDeleteButton()).toBe(false); + }); + + it('should update showDeleteButton when input changes', () => { + setup(); + + fixture.componentRef.setInput('showDeleteButton', true); + fixture.detectChanges(); + + expect(component.showDeleteButton()).toBe(true); + }); + it('should dispatch save license from createLicense', () => { setup({ detectChanges: false }); component.createLicense({ id: MOCK_LICENSE.id, licenseOptions: { year: '2024', copyrightHolders: 'A' } }); 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 2fde5a640..45b99d832 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 @@ -63,6 +63,7 @@ export class PreprintsMetadataStepComponent implements OnInit { private toastService = inject(ToastService); provider = input.required(); + showDeleteButton = input(false); nextClicked = output(); backClicked = output(); deleteClicked = output(); 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 1d4c5be30..9c6957597 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,14 +240,16 @@

{{ 'preprints.preprintStepper.review.sections.supplements.title' | translate [disabled]="isPreprintSubmitting()" (onClick)="cancelSubmission()" /> - + @if (showDeleteButton()) { + + } { expect(emitSpy).toHaveBeenCalled(); }); + + it('should default showDeleteButton to false', () => { + setup(); + + expect(component.showDeleteButton()).toBe(false); + }); + + it('should update showDeleteButton when input changes', () => { + setup(); + + fixture.componentRef.setInput('showDeleteButton', true); + fixture.detectChanges(); + + expect(component.showDeleteButton()).toBe(true); + }); }); 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 37c6086fc..4f0c10123 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 @@ -59,6 +59,7 @@ export class ReviewStepComponent implements OnInit { private readonly toastService = inject(ToastService); readonly provider = input.required(); + readonly showDeleteButton = input(false); readonly deleteClicked = output(); private readonly actions = createDispatchMap({ 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 1c6e97f4b..51ba1df77 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,14 +90,16 @@

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

severity="info" (onClick)="backButtonClicked()" /> - + @if (showDeleteButton()) { + + } { expect(store.dispatch).not.toHaveBeenCalledWith(expect.any(FetchPreprintProject)); }); + it('should default showDeleteButton to false', () => { + setup(); + + expect(component.showDeleteButton()).toBe(false); + }); + + it('should update showDeleteButton when input changes', () => { + setup(); + + fixture.componentRef.setInput('showDeleteButton', true); + fixture.detectChanges(); + + expect(component.showDeleteButton()).toBe(true); + }); + it('should dispatch available projects from debounced project search', fakeAsync(() => { setup(); (store.dispatch as jest.Mock).mockClear(); 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 86ff19826..fd1675e16 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 @@ -17,6 +17,7 @@ import { DestroyRef, effect, inject, + input, OnInit, output, signal, @@ -83,6 +84,7 @@ export class SupplementsStepComponent implements OnInit { selectedSupplementOption = signal(SupplementOptions.None); selectedProjectId = signal(null); + showDeleteButton = input(false); nextClicked = output(); backClicked = output(); deleteClicked = output(); diff --git a/src/app/features/preprints/components/stepper/title-and-abstract-step/title-and-abstract-step.component.html b/src/app/features/preprints/components/stepper/title-and-abstract-step/title-and-abstract-step.component.html index 9091466b8..8ca8dcdd1 100644 --- a/src/app/features/preprints/components/stepper/title-and-abstract-step/title-and-abstract-step.component.html +++ b/src/app/features/preprints/components/stepper/title-and-abstract-step/title-and-abstract-step.component.html @@ -53,6 +53,15 @@

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

severity="info" [routerLink]="['/preprints', providerId(), 'discover']" /> + @if (showDeleteButton()) { + + } { expect(component.titleAndAbstractForm.controls.description.value).toBe(mockPreprint.description); }); + it('should default showDeleteButton to false', () => { + setup(); + + expect(component.showDeleteButton()).toBe(false); + }); + + it('should update showDeleteButton when input changes', () => { + setup(); + + fixture.componentRef.setInput('showDeleteButton', true); + fixture.detectChanges(); + + expect(component.showDeleteButton()).toBe(true); + }); + it('should not dispatch or emit when form is invalid', () => { setup(); const emitSpy = jest.spyOn(component.nextClicked, 'emit'); @@ -118,4 +133,13 @@ describe('TitleAndAbstractStepComponent', () => { expect(emitSpy).toHaveBeenCalled(); }); + + it('should emit deleteClicked when deletePreprint is called', () => { + setup(); + const emitSpy = jest.spyOn(component.deleteClicked, 'emit'); + + component.deletePreprint(); + + expect(emitSpy).toHaveBeenCalled(); + }); }); diff --git a/src/app/features/preprints/components/stepper/title-and-abstract-step/title-and-abstract-step.component.ts b/src/app/features/preprints/components/stepper/title-and-abstract-step/title-and-abstract-step.component.ts index c15c59dd0..da832cd9f 100644 --- a/src/app/features/preprints/components/stepper/title-and-abstract-step/title-and-abstract-step.component.ts +++ b/src/app/features/preprints/components/stepper/title-and-abstract-step/title-and-abstract-step.component.ts @@ -46,7 +46,9 @@ export class TitleAndAbstractStepComponent { private readonly toastService = inject(ToastService); readonly providerId = input.required(); + readonly showDeleteButton = input(false); readonly nextClicked = output(); + readonly deleteClicked = output(); private readonly actions = createDispatchMap({ createPreprint: CreatePreprint, @@ -86,6 +88,10 @@ export class TitleAndAbstractStepComponent { }); } + deletePreprint() { + this.deleteClicked.emit(); + } + nextButtonClicked(): void { if (this.titleAndAbstractForm.invalid) { return; 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 38ce8afb7..8d979201a 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 @@ -38,13 +38,14 @@

@case (PreprintSteps.File) { } @case (PreprintSteps.Review) { - + } } 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 b69b74be7..a510bdfe7 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 @@ -41,6 +41,7 @@

@case (PreprintSteps.File) { @case (PreprintSteps.Metadata) { } @case (PreprintSteps.AuthorAssertions) { } @case (PreprintSteps.Supplements) { } @case (PreprintSteps.Review) { - + } } diff --git a/src/app/features/preprints/pages/update-preprint-stepper/update-preprint-stepper.component.html b/src/app/features/preprints/pages/update-preprint-stepper/update-preprint-stepper.component.html index 6e19e1bd8..8a8a4af30 100644 --- a/src/app/features/preprints/pages/update-preprint-stepper/update-preprint-stepper.component.html +++ b/src/app/features/preprints/pages/update-preprint-stepper/update-preprint-stepper.component.html @@ -36,26 +36,53 @@

@switch (currentStep().value) { @case (PreprintSteps.TitleAndAbstract) { - + } @case (PreprintSteps.File) { - + } @case (PreprintSteps.Metadata) { } @case (PreprintSteps.AuthorAssertions) { - + } @case (PreprintSteps.Supplements) { - + } @case (PreprintSteps.Review) { - + } }
diff --git a/src/app/features/preprints/pages/update-preprint-stepper/update-preprint-stepper.component.spec.ts b/src/app/features/preprints/pages/update-preprint-stepper/update-preprint-stepper.component.spec.ts index 95c980ceb..633733120 100644 --- a/src/app/features/preprints/pages/update-preprint-stepper/update-preprint-stepper.component.spec.ts +++ b/src/app/features/preprints/pages/update-preprint-stepper/update-preprint-stepper.component.spec.ts @@ -24,8 +24,14 @@ import { import { submitPreprintSteps } from '../../constants'; import { PreprintSteps, ReviewsState } 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 { UpdatePreprintStepperComponent } from './update-preprint-stepper.component'; @@ -35,6 +41,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'; @@ -45,6 +55,7 @@ describe('UpdatePreprintStepperComponent', () => { let brandServiceMock: BrandServiceMockType; let headerStyleMock: HeaderStyleServiceMockType; let browserTabMock: BrowserTabServiceMockType; + let draftDeletionMock: PreprintDraftDeletionServiceMockType; const mockProvider: PreprintProviderDetails = PREPRINT_PROVIDER_DETAILS_MOCK; const mockPreprint = PREPRINT_MOCK; @@ -69,6 +80,7 @@ describe('UpdatePreprintStepperComponent', () => { brandServiceMock = BrandServiceMock.simple(); headerStyleMock = HeaderStyleServiceMock.simple(); browserTabMock = BrowserTabServiceMock.simple(); + draftDeletionMock = PreprintDraftDeletionServiceMock.simple(); TestBed.configureTestingModule({ imports: [ @@ -94,6 +106,12 @@ describe('UpdatePreprintStepperComponent', () => { ], }); + TestBed.overrideComponent(UpdatePreprintStepperComponent, { + set: { + providers: [{ provide: PreprintDraftDeletionService, useValue: draftDeletionMock }], + }, + }); + store = TestBed.inject(Store); fixture = TestBed.createComponent(UpdatePreprintStepperComponent); component = fixture.componentInstance; @@ -309,4 +327,45 @@ describe('UpdatePreprintStepperComponent', () => { expect(component.currentStep()).toEqual(firstStep); }); + + it('should return false from isPreprintRejected when preprint is not rejected', () => { + setup(); + + expect(component.isPreprintRejected()).toBe(false); + }); + + it('should return true from isPreprintRejected when preprint is rejected', () => { + setup({ + selectorOverrides: [ + { + selector: PreprintStepperSelectors.getPreprint, + value: { ...mockPreprint, reviewsState: ReviewsState.Rejected }, + }, + ], + }); + + expect(component.isPreprintRejected()).toBe(true); + }); + + it('should request draft deletion with update redirect and action callbacks', () => { + setup(); + (store.dispatch as jest.Mock).mockClear(); + + component.requestDeletePreprint(); + + expect(draftDeletionMock.confirmDeleteDraft).toHaveBeenCalledWith( + expect.objectContaining({ + redirectUrl: '/my-preprints', + onDelete: expect.any(Function), + onReset: expect.any(Function), + }) + ); + + const { onDelete, onReset } = draftDeletionMock.confirmDeleteDraft.mock.calls[0][0]; + onDelete(); + onReset(); + + expect(store.dispatch).toHaveBeenCalledWith(new DeletePreprint()); + expect(store.dispatch).toHaveBeenCalledWith(new ResetPreprintStepperState()); + }); }); diff --git a/src/app/features/preprints/pages/update-preprint-stepper/update-preprint-stepper.component.ts b/src/app/features/preprints/pages/update-preprint-stepper/update-preprint-stepper.component.ts index eacb8c11e..e996bed00 100644 --- a/src/app/features/preprints/pages/update-preprint-stepper/update-preprint-stepper.component.ts +++ b/src/app/features/preprints/pages/update-preprint-stepper/update-preprint-stepper.component.ts @@ -38,8 +38,14 @@ import { } from '../../components'; import { submitPreprintSteps } from '../../constants'; import { PreprintSteps, ProviderReviewsWorkflow, ReviewsState } 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-update-preprint-stepper', @@ -57,6 +63,7 @@ import { FetchPreprintById, PreprintStepperSelectors, ResetPreprintStepperState templateUrl: './update-preprint-stepper.component.html', styleUrl: './update-preprint-stepper.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, + providers: [PreprintDraftDeletionService], }) export class UpdatePreprintStepperComponent implements OnDestroy, CanDeactivateComponent { @HostBinding('class') classes = 'flex-1 flex flex-column w-full'; @@ -65,6 +72,7 @@ export class UpdatePreprintStepperComponent implements OnDestroy, CanDeactivateC 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']))); private preprintId = toSignal(this.route.params.pipe(map((params) => params['preprintId']))); @@ -73,6 +81,7 @@ export class UpdatePreprintStepperComponent implements OnDestroy, CanDeactivateC getPreprintProviderById: GetPreprintProviderById, resetState: ResetPreprintStepperState, fetchPreprint: FetchPreprintById, + deletePreprint: DeletePreprint, }); preprintProvider = select(PreprintProvidersSelectors.getPreprintProviderDetails(this.providerId())); @@ -94,6 +103,8 @@ export class UpdatePreprintStepperComponent implements OnDestroy, CanDeactivateC return providerIsPremod && preprintIsRejected; }); + isPreprintRejected = computed(() => this.preprint()?.reviewsState === ReviewsState.Rejected); + readonly updateSteps = computed(() => { const provider = this.preprintProvider(); const preprint = this.preprint(); @@ -175,4 +186,12 @@ export class UpdatePreprintStepperComponent implements OnDestroy, CanDeactivateC this.currentStep.set(prevStep); } } + + requestDeletePreprint(): void { + this.draftDeletionService.confirmDeleteDraft({ + onDelete: () => this.actions.deletePreprint(), + onReset: () => this.actions.resetState(), + redirectUrl: '/my-preprints', + }); + } }