diff --git a/src/app/features/registries/pages/my-registrations/my-registrations.component.html b/src/app/features/registries/pages/my-registrations/my-registrations.component.html index 2795b3018..d9197ccaa 100644 --- a/src/app/features/registries/pages/my-registrations/my-registrations.component.html +++ b/src/app/features/registries/pages/my-registrations/my-registrations.component.html @@ -10,7 +10,7 @@
- + @if (!isMobile()) { @for (tab of tabOptions; track tab.value) { @@ -25,7 +25,8 @@ class="block mb-4" [options]="tabOptions" [fullWidth]="true" - [(selectedValue)]="selectedTab" + [selectedValue]="selectedTab()" + (selectedValueChange)="onTabChange($event)" > } diff --git a/src/app/features/registries/pages/my-registrations/my-registrations.component.spec.ts b/src/app/features/registries/pages/my-registrations/my-registrations.component.spec.ts index 5cff08442..5f1c0f4e4 100644 --- a/src/app/features/registries/pages/my-registrations/my-registrations.component.spec.ts +++ b/src/app/features/registries/pages/my-registrations/my-registrations.component.spec.ts @@ -1,9 +1,12 @@ import { MockComponents, MockProvider } from 'ng-mocks'; +import { of } from 'rxjs'; + import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute, Router } from '@angular/router'; import { UserSelectors } from '@core/store/user'; +import { RegistrationTab } from '@osf/features/registries/enums'; import { RegistriesSelectors } from '@osf/features/registries/store'; import { CustomPaginatorComponent } from '@osf/shared/components/custom-paginator/custom-paginator.component'; import { RegistrationCardComponent } from '@osf/shared/components/registration-card/registration-card.component'; @@ -23,6 +26,8 @@ describe('MyRegistrationsComponent', () => { let fixture: ComponentFixture; let mockRouter: ReturnType; let mockActivatedRoute: Partial; + let customConfirmationService: jest.Mocked; + let toastService: jest.Mocked; beforeEach(async () => { mockRouter = RouterMockBuilder.create().withUrl('/registries/me').build(); @@ -55,6 +60,8 @@ describe('MyRegistrationsComponent', () => { fixture = TestBed.createComponent(MyRegistrationsComponent); component = fixture.componentInstance; + customConfirmationService = TestBed.inject(CustomConfirmationService) as jest.Mocked; + toastService = TestBed.inject(ToastService) as jest.Mocked; fixture.detectChanges(); }); @@ -62,25 +69,92 @@ describe('MyRegistrationsComponent', () => { expect(component).toBeTruthy(); }); - it('should default to submitted tab and fetch submitted registrations', () => { + it('should default to submitted tab when no query param', () => { + expect(component.selectedTab()).toBe(RegistrationTab.Submitted); + }); + + it('should switch to drafts tab when query param is drafts', () => { + (mockActivatedRoute.snapshot as any).queryParams = { tab: 'drafts' }; + + fixture = TestBed.createComponent(MyRegistrationsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + expect(component.selectedTab()).toBe(RegistrationTab.Drafts); + }); + + it('should switch to submitted tab when query param is submitted', () => { + (mockActivatedRoute.snapshot as any).queryParams = { tab: 'submitted' }; + + fixture = TestBed.createComponent(MyRegistrationsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + expect(component.selectedTab()).toBe(RegistrationTab.Submitted); + }); + + it('should handle tab change and update query params', () => { const actionsMock = { getDraftRegistrations: jest.fn(), getSubmittedRegistrations: jest.fn(), deleteDraft: jest.fn(), } as any; Object.defineProperty(component, 'actions', { value: actionsMock }); + const navigateSpy = jest.spyOn(mockRouter, 'navigate'); + + component.onTabChange(RegistrationTab.Drafts); + + expect(component.selectedTab()).toBe(RegistrationTab.Drafts); + expect(component.draftFirst).toBe(0); + expect(actionsMock.getDraftRegistrations).toHaveBeenCalledWith(); + expect(navigateSpy).toHaveBeenCalledWith([], { + relativeTo: mockActivatedRoute, + queryParams: { tab: 'drafts' }, + queryParamsHandling: 'merge', + }); + }); - component.selectedTab.set(component.RegistrationTab.Drafts); - fixture.detectChanges(); - component.selectedTab.set(component.RegistrationTab.Submitted); - fixture.detectChanges(); - expect(actionsMock.getSubmittedRegistrations).toHaveBeenCalledWith('user-1'); + it('should handle tab change to submitted and update query params', () => { + const actionsMock = { + getDraftRegistrations: jest.fn(), + getSubmittedRegistrations: jest.fn(), + deleteDraft: jest.fn(), + } as any; + Object.defineProperty(component, 'actions', { value: actionsMock }); + const navigateSpy = jest.spyOn(mockRouter, 'navigate'); + + component.onTabChange(RegistrationTab.Submitted); + + expect(component.selectedTab()).toBe(RegistrationTab.Submitted); + expect(component.submittedFirst).toBe(0); + expect(actionsMock.getSubmittedRegistrations).toHaveBeenCalledWith(); + expect(navigateSpy).toHaveBeenCalledWith([], { + relativeTo: mockActivatedRoute, + queryParams: { tab: 'submitted' }, + queryParamsHandling: 'merge', + }); + }); + + it('should not process tab change if tab is not a number', () => { + const actionsMock = { + getDraftRegistrations: jest.fn(), + getSubmittedRegistrations: jest.fn(), + deleteDraft: jest.fn(), + } as any; + Object.defineProperty(component, 'actions', { value: actionsMock }); + const initialTab = component.selectedTab(); + + component.onTabChange('invalid' as any); + + expect(component.selectedTab()).toBe(initialTab); + expect(actionsMock.getDraftRegistrations).not.toHaveBeenCalled(); + expect(actionsMock.getSubmittedRegistrations).not.toHaveBeenCalled(); }); it('should navigate to create registration page', () => { - const navSpy = jest.spyOn(TestBed.inject(Router), 'navigate'); + const navSpy = jest.spyOn(mockRouter, 'navigate'); component.goToCreateRegistration(); - expect(navSpy).toHaveBeenCalledWith(['/registries/osf/new']); + expect(navSpy).toHaveBeenLastCalledWith(['/registries', 'osf', 'new']); }); it('should handle drafts pagination', () => { @@ -95,23 +169,75 @@ describe('MyRegistrationsComponent', () => { const actionsMock = { getSubmittedRegistrations: jest.fn() } as any; Object.defineProperty(component, 'actions', { value: actionsMock }); component.onSubmittedPageChange({ page: 1, first: 10 } as any); - expect(actionsMock.getSubmittedRegistrations).toHaveBeenCalledWith('user-1', 2); + expect(actionsMock.getSubmittedRegistrations).toHaveBeenCalledWith(2); expect(component.submittedFirst).toBe(10); }); - it('should switch to drafts tab based on query param and fetch drafts', async () => { - (mockActivatedRoute.snapshot as any).queryParams = { tab: 'drafts' }; - const actionsMock = { getDraftRegistrations: jest.fn(), getSubmittedRegistrations: jest.fn() } as any; - fixture = TestBed.createComponent(MyRegistrationsComponent); - component = fixture.componentInstance; + it('should delete draft after confirmation', () => { + const actionsMock = { + getDraftRegistrations: jest.fn(), + getSubmittedRegistrations: jest.fn(), + deleteDraft: jest.fn(() => of({})), + } as any; Object.defineProperty(component, 'actions', { value: actionsMock }); - fixture.detectChanges(); - - expect(component.selectedTab()).toBe(0); - component.selectedTab.set(component.RegistrationTab.Submitted); - fixture.detectChanges(); - component.selectedTab.set(component.RegistrationTab.Drafts); - fixture.detectChanges(); + customConfirmationService.confirmDelete.mockImplementation(({ onConfirm }) => { + onConfirm(); + }); + + component.onDeleteDraft('draft-123'); + + expect(customConfirmationService.confirmDelete).toHaveBeenCalledWith({ + headerKey: 'registries.deleteDraft', + messageKey: 'registries.confirmDeleteDraft', + onConfirm: expect.any(Function), + }); + expect(actionsMock.deleteDraft).toHaveBeenCalledWith('draft-123'); expect(actionsMock.getDraftRegistrations).toHaveBeenCalled(); + expect(toastService.showSuccess).toHaveBeenCalledWith('registries.successDeleteDraft'); + }); + + it('should not delete draft if confirmation is cancelled', () => { + const actionsMock = { + getDraftRegistrations: jest.fn(), + getSubmittedRegistrations: jest.fn(), + deleteDraft: jest.fn(), + } as any; + Object.defineProperty(component, 'actions', { value: actionsMock }); + customConfirmationService.confirmDelete.mockImplementation(() => {}); + + component.onDeleteDraft('draft-123'); + + expect(customConfirmationService.confirmDelete).toHaveBeenCalled(); + expect(actionsMock.deleteDraft).not.toHaveBeenCalled(); + expect(actionsMock.getDraftRegistrations).not.toHaveBeenCalled(); + expect(toastService.showSuccess).not.toHaveBeenCalled(); + }); + + it('should reset draftFirst when switching to drafts tab', () => { + component.draftFirst = 20; + const actionsMock = { + getDraftRegistrations: jest.fn(), + getSubmittedRegistrations: jest.fn(), + deleteDraft: jest.fn(), + } as any; + Object.defineProperty(component, 'actions', { value: actionsMock }); + + component.onTabChange(RegistrationTab.Drafts); + + expect(component.draftFirst).toBe(0); + }); + + it('should reset submittedFirst when switching to submitted tab', () => { + component.submittedFirst = 20; + const actionsMock = { + getDraftRegistrations: jest.fn(), + getSubmittedRegistrations: jest.fn(), + deleteDraft: jest.fn(), + } as any; + Object.defineProperty(component, 'actions', { value: actionsMock }); + + component.onTabChange(RegistrationTab.Submitted); + + expect(component.submittedFirst).toBe(0); }); }); diff --git a/src/app/features/registries/pages/my-registrations/my-registrations.component.ts b/src/app/features/registries/pages/my-registrations/my-registrations.component.ts index 89c8e10e8..106db16e9 100644 --- a/src/app/features/registries/pages/my-registrations/my-registrations.component.ts +++ b/src/app/features/registries/pages/my-registrations/my-registrations.component.ts @@ -8,18 +8,18 @@ import { Skeleton } from 'primeng/skeleton'; import { TabsModule } from 'primeng/tabs'; import { NgTemplateOutlet } from '@angular/common'; -import { ChangeDetectionStrategy, Component, effect, inject, signal } from '@angular/core'; +import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core'; import { toSignal } from '@angular/core/rxjs-interop'; import { FormsModule } from '@angular/forms'; import { ActivatedRoute, Router, RouterLink } from '@angular/router'; import { ENVIRONMENT } from '@core/provider/environment.provider'; -import { UserSelectors } from '@core/store/user'; import { CustomPaginatorComponent } from '@osf/shared/components/custom-paginator/custom-paginator.component'; import { RegistrationCardComponent } from '@osf/shared/components/registration-card/registration-card.component'; import { SelectComponent } from '@osf/shared/components/select/select.component'; import { SubHeaderComponent } from '@osf/shared/components/sub-header/sub-header.component'; import { IS_XSMALL } from '@osf/shared/helpers/breakpoints.tokens'; +import { Primitive } from '@osf/shared/helpers/types.helper'; import { CustomConfirmationService } from '@osf/shared/services/custom-confirmation.service'; import { ToastService } from '@osf/shared/services/toast.service'; @@ -57,7 +57,6 @@ export class MyRegistrationsComponent { readonly isMobile = toSignal(inject(IS_XSMALL)); readonly tabOptions = REGISTRATIONS_TABS; - private currentUser = select(UserSelectors.getCurrentUser); draftRegistrations = select(RegistriesSelectors.getDraftRegistrations); draftRegistrationsTotalCount = select(RegistriesSelectors.getDraftRegistrationsTotalCount); isDraftRegistrationsLoading = select(RegistriesSelectors.isDraftRegistrationsLoading); @@ -83,33 +82,37 @@ export class MyRegistrationsComponent { constructor() { const initialTab = this.route.snapshot.queryParams['tab']; - if (initialTab == 'drafts') { - this.selectedTab.set(RegistrationTab.Drafts); - } else { - this.selectedTab.set(RegistrationTab.Submitted); + const selectedTab = initialTab == 'drafts' ? RegistrationTab.Drafts : RegistrationTab.Submitted; + this.onTabChange(selectedTab); + } + + onTabChange(tab: Primitive): void { + if (typeof tab !== 'number') { + return; } - effect(() => { - const tab = this.selectedTab(); - - if (tab === 0) { - this.draftFirst = 0; - this.actions.getDraftRegistrations(); - } else { - this.submittedFirst = 0; - this.actions.getSubmittedRegistrations(this.currentUser()?.id); - } - - this.router.navigate([], { - relativeTo: this.route, - queryParams: { tab: tab === RegistrationTab.Drafts ? 'drafts' : 'submitted' }, - queryParamsHandling: 'merge', - }); + this.selectedTab.set(tab); + this.loadTabData(tab); + + this.router.navigate([], { + relativeTo: this.route, + queryParams: { tab: tab === RegistrationTab.Drafts ? 'drafts' : 'submitted' }, + queryParamsHandling: 'merge', }); } + private loadTabData(tab: number): void { + if (tab === RegistrationTab.Drafts) { + this.draftFirst = 0; + this.actions.getDraftRegistrations(); + } else { + this.submittedFirst = 0; + this.actions.getSubmittedRegistrations(); + } + } + goToCreateRegistration(): void { - this.router.navigate([`/registries/${this.provider}/new`]); + this.router.navigate(['/registries', this.provider, 'new']); } onDeleteDraft(id: string): void { @@ -133,7 +136,7 @@ export class MyRegistrationsComponent { } onSubmittedPageChange(event: PaginatorState): void { - this.actions.getSubmittedRegistrations(this.currentUser()?.id, event.page! + 1); + this.actions.getSubmittedRegistrations(event.page! + 1); this.submittedFirst = event.first!; } } diff --git a/src/app/features/registries/store/registries.actions.ts b/src/app/features/registries/store/registries.actions.ts index e506d972d..45db0f8f9 100644 --- a/src/app/features/registries/store/registries.actions.ts +++ b/src/app/features/registries/store/registries.actions.ts @@ -111,7 +111,6 @@ export class FetchSubmittedRegistrations { static readonly type = '[Registries] Fetch Submitted Registrations'; constructor( - public userId: string | undefined, public page = 1, public pageSize = 10 ) {} diff --git a/src/app/features/registries/store/registries.state.ts b/src/app/features/registries/store/registries.state.ts index 85d9f7198..6a8ae7120 100644 --- a/src/app/features/registries/store/registries.state.ts +++ b/src/app/features/registries/store/registries.state.ts @@ -1,10 +1,11 @@ -import { Action, State, StateContext } from '@ngxs/store'; +import { Action, State, StateContext, Store } from '@ngxs/store'; import { catchError, tap } from 'rxjs/operators'; import { inject, Injectable } from '@angular/core'; import { ENVIRONMENT } from '@core/provider/environment.provider'; +import { UserSelectors } from '@core/store/user'; import { ResourceType } from '@osf/shared/enums/resource-type.enum'; import { getResourceTypeStringFromEnum } from '@osf/shared/helpers/get-resource-types.helper'; import { handleSectionError } from '@osf/shared/helpers/state-error.handler'; @@ -56,6 +57,7 @@ export class RegistriesState { searchService = inject(GlobalSearchService); registriesService = inject(RegistriesService); private readonly environment = inject(ENVIRONMENT); + private readonly store = inject(Store); providersHandler = inject(ProvidersHandlers); projectsHandler = inject(ProjectsHandlers); @@ -311,18 +313,20 @@ export class RegistriesState { @Action(FetchSubmittedRegistrations) fetchSubmittedRegistrations( ctx: StateContext, - { userId, page, pageSize }: FetchSubmittedRegistrations + { page, pageSize }: FetchSubmittedRegistrations ) { const state = ctx.getState(); + const currentUser = this.store.selectSnapshot(UserSelectors.getCurrentUser); + ctx.patchState({ submittedRegistrations: { ...state.submittedRegistrations, isLoading: true, error: null }, }); - if (!userId) { + if (!currentUser) { return; } - return this.registriesService.getSubmittedRegistrations(userId, page, pageSize).pipe( + return this.registriesService.getSubmittedRegistrations(currentUser.id, page, pageSize).pipe( tap((submittedRegistrations) => { ctx.patchState({ submittedRegistrations: {