From 8f46b762b0b3c731a19dd214343d1941a4ebe5f1 Mon Sep 17 00:00:00 2001 From: nsemets Date: Wed, 4 Nov 2026 16:32:33 +0200 Subject: [PATCH 1/2] fix(ssr): updated preprint routes --- src/app/app.routes.server.ts | 8 + src/app/app.routes.ts | 21 -- .../nav-menu/nav-menu.component.spec.ts | 272 +++++++++--------- .../components/nav-menu/nav-menu.component.ts | 50 ++-- .../create-new-version.component.spec.ts | 2 +- .../create-new-version.component.ts | 2 +- .../preprint-details.component.html | 2 +- .../update-preprint-stepper.component.spec.ts | 2 +- .../update-preprint-stepper.component.ts | 2 +- .../features/preprints/preprints.routes.ts | 25 +- 10 files changed, 206 insertions(+), 180 deletions(-) diff --git a/src/app/app.routes.server.ts b/src/app/app.routes.server.ts index 9ffcdb6b4..52291409a 100644 --- a/src/app/app.routes.server.ts +++ b/src/app/app.routes.server.ts @@ -125,6 +125,10 @@ export const serverRoutes: ServerRoute[] = [ path: 'preprints/:providerId', renderMode: RenderMode.Server, }, + { + path: 'preprints', + renderMode: RenderMode.Server, + }, { path: 'registries/my-registrations', renderMode: RenderMode.Client, @@ -145,6 +149,10 @@ export const serverRoutes: ServerRoute[] = [ path: 'registries/:providerId', renderMode: RenderMode.Server, }, + { + path: 'registries', + renderMode: RenderMode.Server, + }, { path: 'institutions', renderMode: RenderMode.Server, diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index b61606a89..430f5e05e 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -116,27 +116,6 @@ export const routes: Routes = [ path: 'preprints', loadChildren: () => import('./features/preprints/preprints.routes').then((mod) => mod.preprintsRoutes), }, - { - path: 'preprints/:providerId/:id/pending-moderation', - loadComponent: () => - import('@osf/features/preprints/pages/preprint-pending-moderation/preprint-pending-moderation.component').then( - (mod) => mod.PreprintPendingModerationComponent - ), - }, - { - path: 'preprints/:providerId/:id/download', - loadComponent: () => - import('@osf/features/preprints/pages/preprint-download-redirect/preprint-download-redirect.component').then( - (c) => c.PreprintDownloadRedirectComponent - ), - }, - { - path: 'preprints/:providerId/:id', - loadComponent: () => - import('@osf/features/preprints/pages/preprint-details/preprint-details.component').then( - (c) => c.PreprintDetailsComponent - ), - }, { path: 'registries', loadChildren: () => import('./features/registries/registries.routes').then((mod) => mod.registriesRoutes), diff --git a/src/app/core/components/nav-menu/nav-menu.component.spec.ts b/src/app/core/components/nav-menu/nav-menu.component.spec.ts index 21f6890d2..6ed259e1f 100644 --- a/src/app/core/components/nav-menu/nav-menu.component.spec.ts +++ b/src/app/core/components/nav-menu/nav-menu.component.spec.ts @@ -1,190 +1,204 @@ -import { MockComponent, MockProvider } from 'ng-mocks'; +import { MockProvider } from 'ng-mocks'; -import { signal } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ActivatedRoute, Router } from '@angular/router'; +import { ActivatedRoute, provideRouter, Router } from '@angular/router'; import { CustomMenuItem } from '@core/models/custom-menu-item.model'; import { AuthService } from '@core/services/auth.service'; -import { ProviderSelectors } from '@osf/core/store/provider/provider.selectors'; -import { UserSelectors } from '@osf/core/store/user/user.selectors'; -import { IconComponent } from '@osf/shared/components/icon/icon.component'; +import { ProviderSelectors } from '@core/store/provider'; +import { UserSelectors } from '@core/store/user'; +import { ViewOnlyLinkHelperService } from '@osf/shared/services/view-only-link-helper.service'; import { CurrentResourceSelectors } from '@osf/shared/stores/current-resource'; -import { MOCK_USER } from '@testing/mocks/data.mock'; import { provideOSFCore } from '@testing/osf.testing.provider'; import { AuthServiceMock, AuthServiceMockType } from '@testing/providers/auth-service.mock'; import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; -import { RouterMockBuilder } from '@testing/providers/router-provider.mock'; -import { provideMockStore } from '@testing/providers/store-provider.mock'; +import { RouterMockBuilder, RouterMockType } from '@testing/providers/router-provider.mock'; +import { BaseSetupOverrides, mergeSignalOverrides, provideMockStore } from '@testing/providers/store-provider.mock'; +import { ViewOnlyLinkHelperMock } from '@testing/providers/view-only-link-helper.mock'; import { NavMenuComponent } from './nav-menu.component'; +interface SetupOverrides extends BaseSetupOverrides { + routerUrl?: string; + nestedRouteParams?: Record; + queryParams?: Record; +} + describe('NavMenuComponent', () => { let component: NavMenuComponent; let fixture: ComponentFixture; - let mockAuthService: AuthServiceMockType; - - beforeEach(() => { - Object.defineProperty(window, 'open', { - writable: true, - value: vi.fn(), - }); - mockAuthService = AuthServiceMock.simple(); + let authServiceMock: AuthServiceMockType; + let routerMock: RouterMockType; + + function findMenuItem(items: CustomMenuItem[] | undefined, id: string): CustomMenuItem | undefined { + if (!items) { + return undefined; + } + + for (const item of items) { + if (item.id === id) { + return item; + } + + const nested = findMenuItem(item.items, id); + if (nested) { + return nested; + } + } + + return undefined; + } + + function setup(overrides: SetupOverrides = {}) { + const routeBuilder = ActivatedRouteMockBuilder.create().withQueryParams(overrides.queryParams ?? {}); + + if (overrides.nestedRouteParams) { + routeBuilder.withFirstChild((featureRoute) => { + featureRoute.withFirstChild((detailsRoute) => { + detailsRoute.withParams(overrides.nestedRouteParams ?? {}); + }); + }); + } else if (overrides.routeParams) { + routeBuilder.withParams(overrides.routeParams); + } + + if (overrides.hasParent === false) { + routeBuilder.withNoParent(); + } + + const routeMock = routeBuilder.build(); + routerMock = RouterMockBuilder.create() + .withUrl(overrides.routerUrl ?? '/') + .build(); + authServiceMock = AuthServiceMock.simple(); + + const defaultSignals = [ + { selector: UserSelectors.isAuthenticated, value: true }, + { selector: UserSelectors.getCanViewReviews, value: false }, + { selector: CurrentResourceSelectors.getCurrentResource, value: null }, + { selector: ProviderSelectors.getCurrentProvider, value: null }, + ]; + const signals = mergeSignalOverrides(defaultSignals, overrides.selectorOverrides); TestBed.configureTestingModule({ - imports: [NavMenuComponent, MockComponent(IconComponent)], + imports: [NavMenuComponent], providers: [ provideOSFCore(), - provideMockStore({ - signals: [ - { selector: UserSelectors.isAuthenticated, value: signal(false) }, - { selector: UserSelectors.getCurrentUser, value: signal(MOCK_USER) }, - { selector: UserSelectors.getCanViewReviews, value: signal(false) }, - { selector: ProviderSelectors.getCurrentProvider, value: signal(null) }, - { selector: CurrentResourceSelectors.getCurrentResource, value: signal(null) }, - ], - }), - { - provide: Router, - useValue: { - ...RouterMockBuilder.create().withUrl('/test').build(), - serializeUrl: vi.fn(() => '/test'), - parseUrl: vi.fn(() => ({})), - isActive: vi.fn(() => false), - }, - }, - { - provide: ActivatedRoute, - useValue: ActivatedRouteMockBuilder.create().build(), - }, - MockProvider(AuthService, mockAuthService), + provideRouter([]), + MockProvider(ActivatedRoute, routeMock), + MockProvider(Router, routerMock), + MockProvider(AuthService, authServiceMock), + MockProvider(ViewOnlyLinkHelperService, ViewOnlyLinkHelperMock.simple()), + provideMockStore({ signals }), ], }); fixture = TestBed.createComponent(NavMenuComponent); component = fixture.componentInstance; fixture.detectChanges(); - }); + + return { fixture, component }; + } it('should create', () => { + setup(); expect(component).toBeTruthy(); }); - it('should open external links in new tab for support and donate items', () => { - const openSpy = vi.spyOn(window, 'open'); - const supportItem: CustomMenuItem = { id: 'support', url: 'https://support.example.com' }; - const donateItem: CustomMenuItem = { id: 'donate', url: 'https://donate.example.com' }; - - component.goToLink(supportItem); - expect(openSpy).toHaveBeenCalledWith('https://support.example.com', '_blank'); + it('should detect preprint details from nested route params', () => { + setup({ + routerUrl: '/preprints/socarxiv/2cpmr_v3', + nestedRouteParams: { providerId: 'socarxiv', id: '2cpmr_v3' }, + }); - component.goToLink(donateItem); - expect(openSpy).toHaveBeenCalledWith('https://donate.example.com', '_blank'); + expect(component.isPreprintRoute()).toBe(true); + expect(component.currentResourceId()).toBe('2cpmr_v3'); }); - it('should navigate to sign in for sign-in item', () => { - const signInItem: CustomMenuItem = { id: 'sign-in' }; + it('should not treat preprint provider overview as preprint details', () => { + setup({ + routerUrl: '/preprints/socarxiv', + nestedRouteParams: { providerId: 'socarxiv' }, + }); - component.goToLink(signInItem); - expect(mockAuthService.navigateToSignIn).toHaveBeenCalled(); + expect(component.isPreprintRoute()).toBe(false); + expect(component.currentResourceId()).toBeUndefined(); }); - it('should logout for log-out item', () => { - const logOutItem: CustomMenuItem = { id: 'log-out' }; + it('should show preprint details menu using nested providerId when provider store is empty', () => { + setup({ + routerUrl: '/preprints/socarxiv/2cpmr_v3', + nestedRouteParams: { providerId: 'socarxiv', id: '2cpmr_v3' }, + }); + + const detailsItem = findMenuItem(component.mainMenuItems(), 'preprints-details'); - component.goToLink(logOutItem); - expect(mockAuthService.logout).toHaveBeenCalled(); + expect(detailsItem?.visible).toBe(true); + expect(detailsItem?.items?.[0]?.routerLink).toEqual(['preprints', 'socarxiv', '2cpmr_v3']); }); - it('should emit closeMenu for items without children', () => { - const emitSpy = vi.spyOn(component.closeMenu, 'emit'); - const menuItem: CustomMenuItem = { id: 'test-item' }; + it('should use resourceId from query params when route id is missing', () => { + setup({ + routerUrl: '/collections/provider-1', + queryParams: { resourceId: 'query-resource-1' }, + }); - component.goToLink(menuItem); - expect(emitSpy).toHaveBeenCalled(); + expect(component.currentResourceId()).toBe('query-resource-1'); }); - it('should not emit closeMenu for items with children', () => { - const emitSpy = vi.spyOn(component.closeMenu, 'emit'); - const menuItemWithChildren: CustomMenuItem = { - id: 'test-item', - items: [{ id: 'child-item' }], - }; - - component.goToLink(menuItemWithChildren); - expect(emitSpy).not.toHaveBeenCalled(); - }); + it('should emit closeMenu for leaf items', () => { + setup(); + const closeMenu = vi.fn(); + component.closeMenu.subscribe(closeMenu); - it('should return false for items without items array', () => { - const menuItem: CustomMenuItem = { id: 'test-item' }; - expect(component.hasVisibleChildren(menuItem)).toBe(false); - }); + component.goToLink({ id: 'home', label: 'Home' }); - it('should return false for items with empty items array', () => { - const menuItem: CustomMenuItem = { id: 'test-item', items: [] }; - expect(component.hasVisibleChildren(menuItem)).toBe(false); + expect(closeMenu).toHaveBeenCalledTimes(1); }); - it('should return false for items with all invisible children', () => { - const menuItem: CustomMenuItem = { - id: 'test-item', - items: [ - { id: 'child1', visible: false }, - { id: 'child2', visible: false }, - ], - }; - expect(component.hasVisibleChildren(menuItem)).toBe(false); - }); + it('should not emit closeMenu for items with children', () => { + setup(); + const closeMenu = vi.fn(); + component.closeMenu.subscribe(closeMenu); + + component.goToLink({ + id: 'preprints', + label: 'Preprints', + items: [{ id: 'child', label: 'Child', visible: true }], + }); - it('should return true for items with at least one visible child', () => { - const menuItem: CustomMenuItem = { - id: 'test-item', - items: [ - { id: 'child1', visible: false }, - { id: 'child2', visible: true }, - ], - }; - expect(component.hasVisibleChildren(menuItem)).toBe(true); + expect(closeMenu).not.toHaveBeenCalled(); }); - it('should return false for items with children that have visible property undefined', () => { - const menuItem: CustomMenuItem = { - id: 'test-item', - items: [{ id: 'child1' }], - }; - expect(component.hasVisibleChildren(menuItem)).toBe(false); - }); + it('should navigate to sign in', () => { + setup(); - it('should have mainMenuItems computed property', () => { - expect(component.mainMenuItems).toBeDefined(); - expect(typeof component.mainMenuItems).toBe('function'); - }); + component.goToLink({ id: 'sign-in', label: 'Sign in' }); - it('should have currentRoute computed property', () => { - expect(component.currentRoute).toBeDefined(); + expect(authServiceMock.navigateToSignIn).toHaveBeenCalledTimes(1); }); - it('should have currentResourceId computed property', () => { - expect(component.currentResourceId).toBeDefined(); - expect(typeof component.currentResourceId).toBe('function'); - }); + it('should log out', () => { + setup(); - it('should have isCollectionsRoute computed property', () => { - expect(component.isCollectionsRoute).toBeDefined(); - expect(typeof component.isCollectionsRoute).toBe('function'); - }); + component.goToLink({ id: 'log-out', label: 'Log out' }); - it('should have isPreprintRoute computed property', () => { - expect(component.isPreprintRoute).toBeDefined(); - expect(typeof component.isPreprintRoute).toBe('function'); + expect(authServiceMock.logout).toHaveBeenCalledTimes(1); }); - it('should emit closeMenu event', () => { - const emitSpy = vi.spyOn(component.closeMenu, 'emit'); - - component.closeMenu.emit(); + it('should report visible children', () => { + setup(); - expect(emitSpy).toHaveBeenCalled(); + expect( + component.hasVisibleChildren({ + id: 'parent', + items: [ + { id: 'hidden', visible: false }, + { id: 'shown', visible: true }, + ], + }) + ).toBe(true); + expect(component.hasVisibleChildren({ id: 'leaf' })).toBe(false); }); }); diff --git a/src/app/core/components/nav-menu/nav-menu.component.ts b/src/app/core/components/nav-menu/nav-menu.component.ts index 9d6e05de6..18575f221 100644 --- a/src/app/core/components/nav-menu/nav-menu.component.ts +++ b/src/app/core/components/nav-menu/nav-menu.component.ts @@ -1,4 +1,4 @@ -import { createDispatchMap, select } from '@ngxs/store'; +import { select } from '@ngxs/store'; import { TranslateService } from '@ngx-translate/core'; @@ -7,7 +7,7 @@ import { PanelMenuModule } from 'primeng/panelmenu'; import { filter, map } from 'rxjs'; import { isPlatformBrowser } from '@angular/common'; -import { Component, computed, inject, output, PLATFORM_ID } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, inject, output, PLATFORM_ID } from '@angular/core'; import { toSignal } from '@angular/core/rxjs-interop'; import { ActivatedRoute, NavigationEnd, Router, RouterLink, RouterLinkActive } from '@angular/router'; @@ -23,13 +23,14 @@ import { CurrentResourceType } from '@osf/shared/enums/resource-type.enum'; import { ReviewPermissions } from '@osf/shared/enums/review-permissions.enum'; import { WrapFnPipe } from '@osf/shared/pipes/wrap-fn.pipe'; import { ViewOnlyLinkHelperService } from '@osf/shared/services/view-only-link-helper.service'; -import { CurrentResourceSelectors, GetResourceDetails } from '@osf/shared/stores/current-resource'; +import { CurrentResourceSelectors } from '@osf/shared/stores/current-resource'; @Component({ selector: 'osf-nav-menu', imports: [RouterLinkActive, RouterLink, PanelMenuModule, IconComponent, WrapFnPipe], templateUrl: './nav-menu.component.html', styleUrl: './nav-menu.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, }) export class NavMenuComponent { closeMenu = output(); @@ -46,8 +47,6 @@ export class NavMenuComponent { private readonly provider = select(ProviderSelectors.getCurrentProvider); private readonly translationsReady = toSignal(this.translateService.stream('navigation.overview')); - readonly actions = createDispatchMap({ getResourceDetails: GetResourceDetails }); - readonly mainMenuItems = computed(() => { this.translationsReady(); const isAuthenticated = this.isAuthenticated(); @@ -55,7 +54,7 @@ export class NavMenuComponent { const routeContext: RouteContext = { resourceId: this.currentResourceId(), - providerId: this.provider()?.id, + providerId: this.provider()?.id ?? this.currentRoute().providerId, isProject: this.currentResource()?.type === CurrentResourceType.Projects && this.currentResourceId() === this.currentResource()?.id, @@ -78,11 +77,7 @@ export class NavMenuComponent { permissions: this.currentResource()?.permissions, }; - const items = updateMenuItems(filtered, routeContext); - - this.translateMenuItems(items); - - return items; + return this.translateMenuItems(updateMenuItems(filtered, routeContext)); }); readonly currentRoute = toSignal( @@ -103,10 +98,11 @@ export class NavMenuComponent { private getRouteInfo() { const urlSegments = this.router.url.split('/').filter((segment) => segment); const resourceFromQueryParams = this.route.snapshot.queryParams['resourceId']; - const resourceId = this.route.firstChild?.snapshot.params['id'] || resourceFromQueryParams; - const providerId = this.route.firstChild?.snapshot.params['providerId']; + const params = this.getMergedRouteParams(); + const resourceId = params['id'] || resourceFromQueryParams; + const providerId = params['providerId']; const isCollectionsWithId = urlSegments[0] === 'collections' && urlSegments[1] && urlSegments[1] !== ''; - const isPreprintRoute = urlSegments[0] === 'preprints' && !!urlSegments[2]; + const isPreprintRoute = urlSegments[0] === 'preprints' && !!providerId && !!resourceId; return { resourceId, @@ -139,15 +135,23 @@ export class NavMenuComponent { readonly hasVisibleChildren = (item: CustomMenuItem): boolean => Array.isArray(item.items) && item.items.some((child) => !!child.visible); - private translateMenuItems(items: CustomMenuItem[]): void { - items.forEach((item) => { - if (item.label) { - item.label = this.translateService.instant(item.label); - } + private translateMenuItems(items: CustomMenuItem[]): CustomMenuItem[] { + return items.map((item) => ({ + ...item, + label: item.label ? this.translateService.instant(item.label) : item.label, + items: item.items?.length ? this.translateMenuItems(item.items) : item.items, + })); + } + + private getMergedRouteParams(): Record { + let current = this.route.snapshot; + let params = { ...current.params }; + + while (current.firstChild) { + current = current.firstChild; + params = { ...params, ...current.params }; + } - if (item.items && item.items.length > 0) { - this.translateMenuItems(item.items); - } - }); + return params; } } 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 95a9070ad..081007e88 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 @@ -62,7 +62,7 @@ describe('CreateNewVersionComponent', () => { routerMock = RouterMockBuilder.create().withNavigate(vi.fn().mockResolvedValue(true)).build(); const routeMock = ActivatedRouteMockBuilder.create() - .withParams({ providerId: mockProviderId, preprintId: mockPreprintId }) + .withParams({ providerId: mockProviderId, id: mockPreprintId }) .withQueryParams({}) .build(); 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 c4c950897..3027382d6 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 @@ -54,7 +54,7 @@ export class CreateNewVersionComponent implements OnDestroy, CanDeactivateCompon 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']))); + private readonly preprintId = toSignal(this.route.params.pipe(map((params) => params['id']))); private readonly actions = createDispatchMap({ getPreprintProviderById: GetPreprintProviderById, diff --git a/src/app/features/preprints/pages/preprint-details/preprint-details.component.html b/src/app/features/preprints/pages/preprint-details/preprint-details.component.html index 0c60a008f..edb93917c 100644 --- a/src/app/features/preprints/pages/preprint-details/preprint-details.component.html +++ b/src/app/features/preprints/pages/preprint-details/preprint-details.component.html @@ -1,4 +1,4 @@ -
+
@if (isPreprintProviderLoading() || isPreprintLoading()) { 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 be59800d6..ebb811289 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 @@ -63,7 +63,7 @@ describe('UpdatePreprintStepperComponent', () => { const signals = mergeSignalOverrides(defaultSignals, overrides?.selectorOverrides); const routeMock = ActivatedRouteMockBuilder.create() - .withParams({ providerId: mockProviderId, preprintId: mockPreprintId }) + .withParams({ providerId: mockProviderId, id: mockPreprintId }) .build(); brandServiceMock = BrandServiceMock.simple(); 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 5be1a80b7..46168eb44 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 @@ -66,7 +66,7 @@ export class UpdatePreprintStepperComponent implements OnDestroy, CanDeactivateC private readonly browserTabHelper = inject(BrowserTabService); private readonly providerId = toSignal(this.route.params.pipe(map((params) => params['providerId']))); - private readonly preprintId = toSignal(this.route.params.pipe(map((params) => params['preprintId']))); + private readonly preprintId = toSignal(this.route.params.pipe(map((params) => params['id']))); private readonly actions = createDispatchMap({ getPreprintProviderById: GetPreprintProviderById, diff --git a/src/app/features/preprints/preprints.routes.ts b/src/app/features/preprints/preprints.routes.ts index 77d2991a4..0bf3bc090 100644 --- a/src/app/features/preprints/preprints.routes.ts +++ b/src/app/features/preprints/preprints.routes.ts @@ -62,7 +62,7 @@ export const preprintsRoutes: Routes = [ providers: [provideStates([ProjectsState])], }, { - path: ':providerId/edit/:preprintId', + path: ':providerId/edit/:id', canActivate: [authGuard], loadComponent: () => import('@osf/features/preprints/pages/update-preprint-stepper/update-preprint-stepper.component').then( @@ -87,7 +87,7 @@ export const preprintsRoutes: Routes = [ providers: [provideStates([PreprintModerationState])], }, { - path: ':providerId/new-version/:preprintId', + path: ':providerId/new-version/:id', canActivate: [authGuard], loadComponent: () => import('@osf/features/preprints/pages/create-new-version/create-new-version.component').then( @@ -95,6 +95,27 @@ export const preprintsRoutes: Routes = [ ), canDeactivate: [ConfirmLeavingGuard], }, + { + path: ':providerId/:id/pending-moderation', + loadComponent: () => + import('@osf/features/preprints/pages/preprint-pending-moderation/preprint-pending-moderation.component').then( + (mod) => mod.PreprintPendingModerationComponent + ), + }, + { + path: ':providerId/:id/download', + loadComponent: () => + import('@osf/features/preprints/pages/preprint-download-redirect/preprint-download-redirect.component').then( + (c) => c.PreprintDownloadRedirectComponent + ), + }, + { + path: ':providerId/:id', + loadComponent: () => + import('@osf/features/preprints/pages/preprint-details/preprint-details.component').then( + (c) => c.PreprintDetailsComponent + ), + }, { path: ':providerId', loadComponent: () => From bd8daad1909c189e56f6aea83e02afe6f5f9a866 Mon Sep 17 00:00:00 2001 From: nsemets Date: Mon, 9 Nov 2026 12:49:21 +0200 Subject: [PATCH 2/2] fix(ssr): updated preprint routes --- src/app/app.routes.server.ts | 8 + src/app/app.routes.ts | 21 -- .../nav-menu/nav-menu.component.spec.ts | 272 +++++++++--------- .../components/nav-menu/nav-menu.component.ts | 50 ++-- .../create-new-version.component.spec.ts | 2 +- .../create-new-version.component.ts | 2 +- .../preprint-details.component.html | 2 +- .../update-preprint-stepper.component.spec.ts | 2 +- .../update-preprint-stepper.component.ts | 2 +- .../features/preprints/preprints.routes.ts | 25 +- 10 files changed, 206 insertions(+), 180 deletions(-) diff --git a/src/app/app.routes.server.ts b/src/app/app.routes.server.ts index 9ffcdb6b4..52291409a 100644 --- a/src/app/app.routes.server.ts +++ b/src/app/app.routes.server.ts @@ -125,6 +125,10 @@ export const serverRoutes: ServerRoute[] = [ path: 'preprints/:providerId', renderMode: RenderMode.Server, }, + { + path: 'preprints', + renderMode: RenderMode.Server, + }, { path: 'registries/my-registrations', renderMode: RenderMode.Client, @@ -145,6 +149,10 @@ export const serverRoutes: ServerRoute[] = [ path: 'registries/:providerId', renderMode: RenderMode.Server, }, + { + path: 'registries', + renderMode: RenderMode.Server, + }, { path: 'institutions', renderMode: RenderMode.Server, diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index b61606a89..430f5e05e 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -116,27 +116,6 @@ export const routes: Routes = [ path: 'preprints', loadChildren: () => import('./features/preprints/preprints.routes').then((mod) => mod.preprintsRoutes), }, - { - path: 'preprints/:providerId/:id/pending-moderation', - loadComponent: () => - import('@osf/features/preprints/pages/preprint-pending-moderation/preprint-pending-moderation.component').then( - (mod) => mod.PreprintPendingModerationComponent - ), - }, - { - path: 'preprints/:providerId/:id/download', - loadComponent: () => - import('@osf/features/preprints/pages/preprint-download-redirect/preprint-download-redirect.component').then( - (c) => c.PreprintDownloadRedirectComponent - ), - }, - { - path: 'preprints/:providerId/:id', - loadComponent: () => - import('@osf/features/preprints/pages/preprint-details/preprint-details.component').then( - (c) => c.PreprintDetailsComponent - ), - }, { path: 'registries', loadChildren: () => import('./features/registries/registries.routes').then((mod) => mod.registriesRoutes), diff --git a/src/app/core/components/nav-menu/nav-menu.component.spec.ts b/src/app/core/components/nav-menu/nav-menu.component.spec.ts index 21f6890d2..6ed259e1f 100644 --- a/src/app/core/components/nav-menu/nav-menu.component.spec.ts +++ b/src/app/core/components/nav-menu/nav-menu.component.spec.ts @@ -1,190 +1,204 @@ -import { MockComponent, MockProvider } from 'ng-mocks'; +import { MockProvider } from 'ng-mocks'; -import { signal } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ActivatedRoute, Router } from '@angular/router'; +import { ActivatedRoute, provideRouter, Router } from '@angular/router'; import { CustomMenuItem } from '@core/models/custom-menu-item.model'; import { AuthService } from '@core/services/auth.service'; -import { ProviderSelectors } from '@osf/core/store/provider/provider.selectors'; -import { UserSelectors } from '@osf/core/store/user/user.selectors'; -import { IconComponent } from '@osf/shared/components/icon/icon.component'; +import { ProviderSelectors } from '@core/store/provider'; +import { UserSelectors } from '@core/store/user'; +import { ViewOnlyLinkHelperService } from '@osf/shared/services/view-only-link-helper.service'; import { CurrentResourceSelectors } from '@osf/shared/stores/current-resource'; -import { MOCK_USER } from '@testing/mocks/data.mock'; import { provideOSFCore } from '@testing/osf.testing.provider'; import { AuthServiceMock, AuthServiceMockType } from '@testing/providers/auth-service.mock'; import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; -import { RouterMockBuilder } from '@testing/providers/router-provider.mock'; -import { provideMockStore } from '@testing/providers/store-provider.mock'; +import { RouterMockBuilder, RouterMockType } from '@testing/providers/router-provider.mock'; +import { BaseSetupOverrides, mergeSignalOverrides, provideMockStore } from '@testing/providers/store-provider.mock'; +import { ViewOnlyLinkHelperMock } from '@testing/providers/view-only-link-helper.mock'; import { NavMenuComponent } from './nav-menu.component'; +interface SetupOverrides extends BaseSetupOverrides { + routerUrl?: string; + nestedRouteParams?: Record; + queryParams?: Record; +} + describe('NavMenuComponent', () => { let component: NavMenuComponent; let fixture: ComponentFixture; - let mockAuthService: AuthServiceMockType; - - beforeEach(() => { - Object.defineProperty(window, 'open', { - writable: true, - value: vi.fn(), - }); - mockAuthService = AuthServiceMock.simple(); + let authServiceMock: AuthServiceMockType; + let routerMock: RouterMockType; + + function findMenuItem(items: CustomMenuItem[] | undefined, id: string): CustomMenuItem | undefined { + if (!items) { + return undefined; + } + + for (const item of items) { + if (item.id === id) { + return item; + } + + const nested = findMenuItem(item.items, id); + if (nested) { + return nested; + } + } + + return undefined; + } + + function setup(overrides: SetupOverrides = {}) { + const routeBuilder = ActivatedRouteMockBuilder.create().withQueryParams(overrides.queryParams ?? {}); + + if (overrides.nestedRouteParams) { + routeBuilder.withFirstChild((featureRoute) => { + featureRoute.withFirstChild((detailsRoute) => { + detailsRoute.withParams(overrides.nestedRouteParams ?? {}); + }); + }); + } else if (overrides.routeParams) { + routeBuilder.withParams(overrides.routeParams); + } + + if (overrides.hasParent === false) { + routeBuilder.withNoParent(); + } + + const routeMock = routeBuilder.build(); + routerMock = RouterMockBuilder.create() + .withUrl(overrides.routerUrl ?? '/') + .build(); + authServiceMock = AuthServiceMock.simple(); + + const defaultSignals = [ + { selector: UserSelectors.isAuthenticated, value: true }, + { selector: UserSelectors.getCanViewReviews, value: false }, + { selector: CurrentResourceSelectors.getCurrentResource, value: null }, + { selector: ProviderSelectors.getCurrentProvider, value: null }, + ]; + const signals = mergeSignalOverrides(defaultSignals, overrides.selectorOverrides); TestBed.configureTestingModule({ - imports: [NavMenuComponent, MockComponent(IconComponent)], + imports: [NavMenuComponent], providers: [ provideOSFCore(), - provideMockStore({ - signals: [ - { selector: UserSelectors.isAuthenticated, value: signal(false) }, - { selector: UserSelectors.getCurrentUser, value: signal(MOCK_USER) }, - { selector: UserSelectors.getCanViewReviews, value: signal(false) }, - { selector: ProviderSelectors.getCurrentProvider, value: signal(null) }, - { selector: CurrentResourceSelectors.getCurrentResource, value: signal(null) }, - ], - }), - { - provide: Router, - useValue: { - ...RouterMockBuilder.create().withUrl('/test').build(), - serializeUrl: vi.fn(() => '/test'), - parseUrl: vi.fn(() => ({})), - isActive: vi.fn(() => false), - }, - }, - { - provide: ActivatedRoute, - useValue: ActivatedRouteMockBuilder.create().build(), - }, - MockProvider(AuthService, mockAuthService), + provideRouter([]), + MockProvider(ActivatedRoute, routeMock), + MockProvider(Router, routerMock), + MockProvider(AuthService, authServiceMock), + MockProvider(ViewOnlyLinkHelperService, ViewOnlyLinkHelperMock.simple()), + provideMockStore({ signals }), ], }); fixture = TestBed.createComponent(NavMenuComponent); component = fixture.componentInstance; fixture.detectChanges(); - }); + + return { fixture, component }; + } it('should create', () => { + setup(); expect(component).toBeTruthy(); }); - it('should open external links in new tab for support and donate items', () => { - const openSpy = vi.spyOn(window, 'open'); - const supportItem: CustomMenuItem = { id: 'support', url: 'https://support.example.com' }; - const donateItem: CustomMenuItem = { id: 'donate', url: 'https://donate.example.com' }; - - component.goToLink(supportItem); - expect(openSpy).toHaveBeenCalledWith('https://support.example.com', '_blank'); + it('should detect preprint details from nested route params', () => { + setup({ + routerUrl: '/preprints/socarxiv/2cpmr_v3', + nestedRouteParams: { providerId: 'socarxiv', id: '2cpmr_v3' }, + }); - component.goToLink(donateItem); - expect(openSpy).toHaveBeenCalledWith('https://donate.example.com', '_blank'); + expect(component.isPreprintRoute()).toBe(true); + expect(component.currentResourceId()).toBe('2cpmr_v3'); }); - it('should navigate to sign in for sign-in item', () => { - const signInItem: CustomMenuItem = { id: 'sign-in' }; + it('should not treat preprint provider overview as preprint details', () => { + setup({ + routerUrl: '/preprints/socarxiv', + nestedRouteParams: { providerId: 'socarxiv' }, + }); - component.goToLink(signInItem); - expect(mockAuthService.navigateToSignIn).toHaveBeenCalled(); + expect(component.isPreprintRoute()).toBe(false); + expect(component.currentResourceId()).toBeUndefined(); }); - it('should logout for log-out item', () => { - const logOutItem: CustomMenuItem = { id: 'log-out' }; + it('should show preprint details menu using nested providerId when provider store is empty', () => { + setup({ + routerUrl: '/preprints/socarxiv/2cpmr_v3', + nestedRouteParams: { providerId: 'socarxiv', id: '2cpmr_v3' }, + }); + + const detailsItem = findMenuItem(component.mainMenuItems(), 'preprints-details'); - component.goToLink(logOutItem); - expect(mockAuthService.logout).toHaveBeenCalled(); + expect(detailsItem?.visible).toBe(true); + expect(detailsItem?.items?.[0]?.routerLink).toEqual(['preprints', 'socarxiv', '2cpmr_v3']); }); - it('should emit closeMenu for items without children', () => { - const emitSpy = vi.spyOn(component.closeMenu, 'emit'); - const menuItem: CustomMenuItem = { id: 'test-item' }; + it('should use resourceId from query params when route id is missing', () => { + setup({ + routerUrl: '/collections/provider-1', + queryParams: { resourceId: 'query-resource-1' }, + }); - component.goToLink(menuItem); - expect(emitSpy).toHaveBeenCalled(); + expect(component.currentResourceId()).toBe('query-resource-1'); }); - it('should not emit closeMenu for items with children', () => { - const emitSpy = vi.spyOn(component.closeMenu, 'emit'); - const menuItemWithChildren: CustomMenuItem = { - id: 'test-item', - items: [{ id: 'child-item' }], - }; - - component.goToLink(menuItemWithChildren); - expect(emitSpy).not.toHaveBeenCalled(); - }); + it('should emit closeMenu for leaf items', () => { + setup(); + const closeMenu = vi.fn(); + component.closeMenu.subscribe(closeMenu); - it('should return false for items without items array', () => { - const menuItem: CustomMenuItem = { id: 'test-item' }; - expect(component.hasVisibleChildren(menuItem)).toBe(false); - }); + component.goToLink({ id: 'home', label: 'Home' }); - it('should return false for items with empty items array', () => { - const menuItem: CustomMenuItem = { id: 'test-item', items: [] }; - expect(component.hasVisibleChildren(menuItem)).toBe(false); + expect(closeMenu).toHaveBeenCalledTimes(1); }); - it('should return false for items with all invisible children', () => { - const menuItem: CustomMenuItem = { - id: 'test-item', - items: [ - { id: 'child1', visible: false }, - { id: 'child2', visible: false }, - ], - }; - expect(component.hasVisibleChildren(menuItem)).toBe(false); - }); + it('should not emit closeMenu for items with children', () => { + setup(); + const closeMenu = vi.fn(); + component.closeMenu.subscribe(closeMenu); + + component.goToLink({ + id: 'preprints', + label: 'Preprints', + items: [{ id: 'child', label: 'Child', visible: true }], + }); - it('should return true for items with at least one visible child', () => { - const menuItem: CustomMenuItem = { - id: 'test-item', - items: [ - { id: 'child1', visible: false }, - { id: 'child2', visible: true }, - ], - }; - expect(component.hasVisibleChildren(menuItem)).toBe(true); + expect(closeMenu).not.toHaveBeenCalled(); }); - it('should return false for items with children that have visible property undefined', () => { - const menuItem: CustomMenuItem = { - id: 'test-item', - items: [{ id: 'child1' }], - }; - expect(component.hasVisibleChildren(menuItem)).toBe(false); - }); + it('should navigate to sign in', () => { + setup(); - it('should have mainMenuItems computed property', () => { - expect(component.mainMenuItems).toBeDefined(); - expect(typeof component.mainMenuItems).toBe('function'); - }); + component.goToLink({ id: 'sign-in', label: 'Sign in' }); - it('should have currentRoute computed property', () => { - expect(component.currentRoute).toBeDefined(); + expect(authServiceMock.navigateToSignIn).toHaveBeenCalledTimes(1); }); - it('should have currentResourceId computed property', () => { - expect(component.currentResourceId).toBeDefined(); - expect(typeof component.currentResourceId).toBe('function'); - }); + it('should log out', () => { + setup(); - it('should have isCollectionsRoute computed property', () => { - expect(component.isCollectionsRoute).toBeDefined(); - expect(typeof component.isCollectionsRoute).toBe('function'); - }); + component.goToLink({ id: 'log-out', label: 'Log out' }); - it('should have isPreprintRoute computed property', () => { - expect(component.isPreprintRoute).toBeDefined(); - expect(typeof component.isPreprintRoute).toBe('function'); + expect(authServiceMock.logout).toHaveBeenCalledTimes(1); }); - it('should emit closeMenu event', () => { - const emitSpy = vi.spyOn(component.closeMenu, 'emit'); - - component.closeMenu.emit(); + it('should report visible children', () => { + setup(); - expect(emitSpy).toHaveBeenCalled(); + expect( + component.hasVisibleChildren({ + id: 'parent', + items: [ + { id: 'hidden', visible: false }, + { id: 'shown', visible: true }, + ], + }) + ).toBe(true); + expect(component.hasVisibleChildren({ id: 'leaf' })).toBe(false); }); }); diff --git a/src/app/core/components/nav-menu/nav-menu.component.ts b/src/app/core/components/nav-menu/nav-menu.component.ts index 9d6e05de6..18575f221 100644 --- a/src/app/core/components/nav-menu/nav-menu.component.ts +++ b/src/app/core/components/nav-menu/nav-menu.component.ts @@ -1,4 +1,4 @@ -import { createDispatchMap, select } from '@ngxs/store'; +import { select } from '@ngxs/store'; import { TranslateService } from '@ngx-translate/core'; @@ -7,7 +7,7 @@ import { PanelMenuModule } from 'primeng/panelmenu'; import { filter, map } from 'rxjs'; import { isPlatformBrowser } from '@angular/common'; -import { Component, computed, inject, output, PLATFORM_ID } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, inject, output, PLATFORM_ID } from '@angular/core'; import { toSignal } from '@angular/core/rxjs-interop'; import { ActivatedRoute, NavigationEnd, Router, RouterLink, RouterLinkActive } from '@angular/router'; @@ -23,13 +23,14 @@ import { CurrentResourceType } from '@osf/shared/enums/resource-type.enum'; import { ReviewPermissions } from '@osf/shared/enums/review-permissions.enum'; import { WrapFnPipe } from '@osf/shared/pipes/wrap-fn.pipe'; import { ViewOnlyLinkHelperService } from '@osf/shared/services/view-only-link-helper.service'; -import { CurrentResourceSelectors, GetResourceDetails } from '@osf/shared/stores/current-resource'; +import { CurrentResourceSelectors } from '@osf/shared/stores/current-resource'; @Component({ selector: 'osf-nav-menu', imports: [RouterLinkActive, RouterLink, PanelMenuModule, IconComponent, WrapFnPipe], templateUrl: './nav-menu.component.html', styleUrl: './nav-menu.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, }) export class NavMenuComponent { closeMenu = output(); @@ -46,8 +47,6 @@ export class NavMenuComponent { private readonly provider = select(ProviderSelectors.getCurrentProvider); private readonly translationsReady = toSignal(this.translateService.stream('navigation.overview')); - readonly actions = createDispatchMap({ getResourceDetails: GetResourceDetails }); - readonly mainMenuItems = computed(() => { this.translationsReady(); const isAuthenticated = this.isAuthenticated(); @@ -55,7 +54,7 @@ export class NavMenuComponent { const routeContext: RouteContext = { resourceId: this.currentResourceId(), - providerId: this.provider()?.id, + providerId: this.provider()?.id ?? this.currentRoute().providerId, isProject: this.currentResource()?.type === CurrentResourceType.Projects && this.currentResourceId() === this.currentResource()?.id, @@ -78,11 +77,7 @@ export class NavMenuComponent { permissions: this.currentResource()?.permissions, }; - const items = updateMenuItems(filtered, routeContext); - - this.translateMenuItems(items); - - return items; + return this.translateMenuItems(updateMenuItems(filtered, routeContext)); }); readonly currentRoute = toSignal( @@ -103,10 +98,11 @@ export class NavMenuComponent { private getRouteInfo() { const urlSegments = this.router.url.split('/').filter((segment) => segment); const resourceFromQueryParams = this.route.snapshot.queryParams['resourceId']; - const resourceId = this.route.firstChild?.snapshot.params['id'] || resourceFromQueryParams; - const providerId = this.route.firstChild?.snapshot.params['providerId']; + const params = this.getMergedRouteParams(); + const resourceId = params['id'] || resourceFromQueryParams; + const providerId = params['providerId']; const isCollectionsWithId = urlSegments[0] === 'collections' && urlSegments[1] && urlSegments[1] !== ''; - const isPreprintRoute = urlSegments[0] === 'preprints' && !!urlSegments[2]; + const isPreprintRoute = urlSegments[0] === 'preprints' && !!providerId && !!resourceId; return { resourceId, @@ -139,15 +135,23 @@ export class NavMenuComponent { readonly hasVisibleChildren = (item: CustomMenuItem): boolean => Array.isArray(item.items) && item.items.some((child) => !!child.visible); - private translateMenuItems(items: CustomMenuItem[]): void { - items.forEach((item) => { - if (item.label) { - item.label = this.translateService.instant(item.label); - } + private translateMenuItems(items: CustomMenuItem[]): CustomMenuItem[] { + return items.map((item) => ({ + ...item, + label: item.label ? this.translateService.instant(item.label) : item.label, + items: item.items?.length ? this.translateMenuItems(item.items) : item.items, + })); + } + + private getMergedRouteParams(): Record { + let current = this.route.snapshot; + let params = { ...current.params }; + + while (current.firstChild) { + current = current.firstChild; + params = { ...params, ...current.params }; + } - if (item.items && item.items.length > 0) { - this.translateMenuItems(item.items); - } - }); + return params; } } 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 95a9070ad..081007e88 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 @@ -62,7 +62,7 @@ describe('CreateNewVersionComponent', () => { routerMock = RouterMockBuilder.create().withNavigate(vi.fn().mockResolvedValue(true)).build(); const routeMock = ActivatedRouteMockBuilder.create() - .withParams({ providerId: mockProviderId, preprintId: mockPreprintId }) + .withParams({ providerId: mockProviderId, id: mockPreprintId }) .withQueryParams({}) .build(); 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 c4c950897..3027382d6 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 @@ -54,7 +54,7 @@ export class CreateNewVersionComponent implements OnDestroy, CanDeactivateCompon 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']))); + private readonly preprintId = toSignal(this.route.params.pipe(map((params) => params['id']))); private readonly actions = createDispatchMap({ getPreprintProviderById: GetPreprintProviderById, diff --git a/src/app/features/preprints/pages/preprint-details/preprint-details.component.html b/src/app/features/preprints/pages/preprint-details/preprint-details.component.html index 0c60a008f..edb93917c 100644 --- a/src/app/features/preprints/pages/preprint-details/preprint-details.component.html +++ b/src/app/features/preprints/pages/preprint-details/preprint-details.component.html @@ -1,4 +1,4 @@ -
+
@if (isPreprintProviderLoading() || isPreprintLoading()) { 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 be59800d6..ebb811289 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 @@ -63,7 +63,7 @@ describe('UpdatePreprintStepperComponent', () => { const signals = mergeSignalOverrides(defaultSignals, overrides?.selectorOverrides); const routeMock = ActivatedRouteMockBuilder.create() - .withParams({ providerId: mockProviderId, preprintId: mockPreprintId }) + .withParams({ providerId: mockProviderId, id: mockPreprintId }) .build(); brandServiceMock = BrandServiceMock.simple(); 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 5be1a80b7..46168eb44 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 @@ -66,7 +66,7 @@ export class UpdatePreprintStepperComponent implements OnDestroy, CanDeactivateC private readonly browserTabHelper = inject(BrowserTabService); private readonly providerId = toSignal(this.route.params.pipe(map((params) => params['providerId']))); - private readonly preprintId = toSignal(this.route.params.pipe(map((params) => params['preprintId']))); + private readonly preprintId = toSignal(this.route.params.pipe(map((params) => params['id']))); private readonly actions = createDispatchMap({ getPreprintProviderById: GetPreprintProviderById, diff --git a/src/app/features/preprints/preprints.routes.ts b/src/app/features/preprints/preprints.routes.ts index 77d2991a4..0bf3bc090 100644 --- a/src/app/features/preprints/preprints.routes.ts +++ b/src/app/features/preprints/preprints.routes.ts @@ -62,7 +62,7 @@ export const preprintsRoutes: Routes = [ providers: [provideStates([ProjectsState])], }, { - path: ':providerId/edit/:preprintId', + path: ':providerId/edit/:id', canActivate: [authGuard], loadComponent: () => import('@osf/features/preprints/pages/update-preprint-stepper/update-preprint-stepper.component').then( @@ -87,7 +87,7 @@ export const preprintsRoutes: Routes = [ providers: [provideStates([PreprintModerationState])], }, { - path: ':providerId/new-version/:preprintId', + path: ':providerId/new-version/:id', canActivate: [authGuard], loadComponent: () => import('@osf/features/preprints/pages/create-new-version/create-new-version.component').then( @@ -95,6 +95,27 @@ export const preprintsRoutes: Routes = [ ), canDeactivate: [ConfirmLeavingGuard], }, + { + path: ':providerId/:id/pending-moderation', + loadComponent: () => + import('@osf/features/preprints/pages/preprint-pending-moderation/preprint-pending-moderation.component').then( + (mod) => mod.PreprintPendingModerationComponent + ), + }, + { + path: ':providerId/:id/download', + loadComponent: () => + import('@osf/features/preprints/pages/preprint-download-redirect/preprint-download-redirect.component').then( + (c) => c.PreprintDownloadRedirectComponent + ), + }, + { + path: ':providerId/:id', + loadComponent: () => + import('@osf/features/preprints/pages/preprint-details/preprint-details.component').then( + (c) => c.PreprintDetailsComponent + ), + }, { path: ':providerId', loadComponent: () =>