diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4048acc107..0354a4b3c8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -33,7 +33,7 @@ jobs: #CHROME_VERSION: "90.0.4430.212-1" # Bump Node heap size (OOM in CI after upgrading to Angular 15) NODE_OPTIONS: '--max-old-space-size=4096' - # Project name to use when running docker-compose prior to e2e tests + # Project name to use when running "docker compose" prior to e2e tests COMPOSE_PROJECT_NAME: 'ci' strategy: # Create a matrix of Node versions to test against (in parallel) @@ -108,12 +108,12 @@ jobs: path: 'coverage/dspace-angular/lcov.info' retention-days: 14 - # Using docker-compose start backend using CI configuration + # Using "docker compose" start backend using CI configuration # and load assetstore from a cached copy - name: Start DSpace REST Backend via Docker (for e2e tests) run: | - docker-compose -f ./docker/docker-compose-ci.yml up -d - docker-compose -f ./docker/cli.yml -f ./docker/cli.assetstore.yml run --rm dspace-cli + docker compose -f ./docker/docker-compose-ci.yml up -d + docker compose -f ./docker/cli.yml -f ./docker/cli.assetstore.yml run --rm dspace-cli docker container ls # Run integration tests via Cypress.io @@ -182,7 +182,7 @@ jobs: run: kill -9 $(lsof -t -i:4000) - name: Shutdown Docker containers - run: docker-compose -f ./docker/docker-compose-ci.yml down + run: docker compose -f ./docker/docker-compose-ci.yml down # Codecov upload is a separate job in order to allow us to restart this separate from the entire build/test # job above. This is necessary because Codecov uploads seem to randomly fail at times. diff --git a/package.json b/package.json index 5152458ae0..6b9d2a4c06 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "date-fns-tz": "^1.3.7", "deepmerge": "^4.3.1", "ejs": "^3.1.9", - "express": "^4.18.2", + "express": "^4.19.2", "express-rate-limit": "^5.1.3", "fast-json-patch": "^3.1.1", "filesize": "^6.1.0", diff --git a/src/app/access-control/access-control-routes.ts b/src/app/access-control/access-control-routes.ts index dfa62ec4cd..a7cce461ef 100644 --- a/src/app/access-control/access-control-routes.ts +++ b/src/app/access-control/access-control-routes.ts @@ -1,11 +1,14 @@ import { AbstractControl } from '@angular/forms'; -import { Route } from '@angular/router'; +import { + mapToCanActivate, + Route, +} from '@angular/router'; import { DYNAMIC_ERROR_MESSAGES_MATCHER, DynamicErrorMessagesMatcher, } from '@ng-dynamic-forms/core'; -import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; +import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; import { GroupAdministratorGuard } from '../core/data/feature-authorization/feature-authorization-guard/group-administrator.guard'; import { SiteAdministratorGuard } from '../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard'; import { @@ -39,76 +42,76 @@ export const ROUTES: Route[] = [ path: EPERSON_PATH, component: EPeopleRegistryComponent, resolve: { - breadcrumb: I18nBreadcrumbResolver, + breadcrumb: i18nBreadcrumbResolver, }, providers, data: { title: 'admin.access-control.epeople.title', breadcrumbKey: 'admin.access-control.epeople' }, - canActivate: [SiteAdministratorGuard], + canActivate: mapToCanActivate([SiteAdministratorGuard]), }, { path: `${EPERSON_PATH}/create`, component: EPersonFormComponent, resolve: { - breadcrumb: I18nBreadcrumbResolver, + breadcrumb: i18nBreadcrumbResolver, }, providers, data: { title: 'admin.access-control.epeople.add.title', breadcrumbKey: 'admin.access-control.epeople.add' }, - canActivate: [SiteAdministratorGuard], + canActivate: mapToCanActivate([SiteAdministratorGuard]), }, { path: `${EPERSON_PATH}/:id/edit`, component: EPersonFormComponent, resolve: { - breadcrumb: I18nBreadcrumbResolver, + breadcrumb: i18nBreadcrumbResolver, ePerson: EPersonResolver, }, providers, data: { title: 'admin.access-control.epeople.edit.title', breadcrumbKey: 'admin.access-control.epeople.edit' }, - canActivate: [SiteAdministratorGuard], + canActivate: mapToCanActivate([SiteAdministratorGuard]), }, { path: GROUP_PATH, component: GroupsRegistryComponent, resolve: { - breadcrumb: I18nBreadcrumbResolver, + breadcrumb: i18nBreadcrumbResolver, }, providers, data: { title: 'admin.access-control.groups.title', breadcrumbKey: 'admin.access-control.groups' }, - canActivate: [GroupAdministratorGuard], + canActivate: mapToCanActivate([GroupAdministratorGuard]), }, { path: `${GROUP_PATH}/create`, component: GroupFormComponent, resolve: { - breadcrumb: I18nBreadcrumbResolver, + breadcrumb: i18nBreadcrumbResolver, }, providers, data: { title: 'admin.access-control.groups.title.addGroup', breadcrumbKey: 'admin.access-control.groups.addGroup', }, - canActivate: [GroupAdministratorGuard], + canActivate: mapToCanActivate([GroupAdministratorGuard]), }, { path: `${GROUP_PATH}/:groupId/edit`, component: GroupFormComponent, resolve: { - breadcrumb: I18nBreadcrumbResolver, + breadcrumb: i18nBreadcrumbResolver, }, providers, data: { title: 'admin.access-control.groups.title.singleGroup', breadcrumbKey: 'admin.access-control.groups.singleGroup', }, - canActivate: [GroupPageGuard], + canActivate: mapToCanActivate([GroupPageGuard]), }, { path: 'bulk-access', component: BulkAccessComponent, resolve: { - breadcrumb: I18nBreadcrumbResolver, + breadcrumb: i18nBreadcrumbResolver, }, data: { title: 'admin.access-control.bulk-access.title', breadcrumbKey: 'admin.access-control.bulk-access' }, - canActivate: [SiteAdministratorGuard], + canActivate: mapToCanActivate([SiteAdministratorGuard]), }, ]; diff --git a/src/app/access-control/group-registry/group-form/group-form.component.spec.ts b/src/app/access-control/group-registry/group-form/group-form.component.spec.ts index 0c8c64a470..02de06f415 100644 --- a/src/app/access-control/group-registry/group-form/group-form.component.spec.ts +++ b/src/app/access-control/group-registry/group-form/group-form.component.spec.ts @@ -53,6 +53,7 @@ import { HALEndpointService } from '../../../core/shared/hal-endpoint.service'; import { NoContent } from '../../../core/shared/NoContent.model'; import { PageInfo } from '../../../core/shared/page-info.model'; import { UUIDService } from '../../../core/shared/uuid.service'; +import { XSRFService } from '../../../core/xsrf/xsrf.service'; import { AlertComponent } from '../../../shared/alert/alert.component'; import { ContextHelpDirective } from '../../../shared/context-help.directive'; import { FormBuilderService } from '../../../shared/form/builder/form-builder.service'; @@ -244,6 +245,7 @@ describe('GroupFormComponent', () => { { provide: HttpClient, useValue: {} }, { provide: ObjectCacheService, useValue: {} }, { provide: UUIDService, useValue: {} }, + { provide: XSRFService, useValue: {} }, { provide: Store, useValue: {} }, { provide: RemoteDataBuildService, useValue: {} }, { provide: HALEndpointService, useValue: {} }, diff --git a/src/app/admin/admin-ldn-services/admin-ldn-services-routes.ts b/src/app/admin/admin-ldn-services/admin-ldn-services-routes.ts index 1299ed53dc..66420f7a7b 100644 --- a/src/app/admin/admin-ldn-services/admin-ldn-services-routes.ts +++ b/src/app/admin/admin-ldn-services/admin-ldn-services-routes.ts @@ -1,7 +1,7 @@ import { Routes } from '@angular/router'; -import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver'; -import { NavigationBreadcrumbResolver } from '../../core/breadcrumbs/navigation-breadcrumb.resolver'; +import { i18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver'; +import { navigationBreadcrumbResolver } from '../../core/breadcrumbs/navigation-breadcrumb.resolver'; import { LdnServiceFormComponent } from './ldn-service-form/ldn-service-form.component'; import { LdnServicesOverviewComponent } from './ldn-services-directory/ldn-services-directory.component'; @@ -10,18 +10,18 @@ const moduleRoutes: Routes = [ path: '', pathMatch: 'full', component: LdnServicesOverviewComponent, - resolve: { breadcrumb: I18nBreadcrumbResolver }, + resolve: { breadcrumb: i18nBreadcrumbResolver }, data: { title: 'ldn-registered-services.title', breadcrumbKey: 'ldn-registered-services.new' }, }, { path: 'new', - resolve: { breadcrumb: NavigationBreadcrumbResolver }, + resolve: { breadcrumb: navigationBreadcrumbResolver }, component: LdnServiceFormComponent, data: { title: 'ldn-register-new-service.title', breadcrumbKey: 'ldn-register-new-service' }, }, { path: 'edit/:serviceId', - resolve: { breadcrumb: NavigationBreadcrumbResolver }, + resolve: { breadcrumb: navigationBreadcrumbResolver }, component: LdnServiceFormComponent, data: { title: 'ldn-edit-service.title', breadcrumbKey: 'ldn-edit-service' }, }, diff --git a/src/app/admin/admin-notifications/admin-notifications-routes.ts b/src/app/admin/admin-notifications/admin-notifications-routes.ts index 2d85da41bc..43cfc2945a 100644 --- a/src/app/admin/admin-notifications/admin-notifications-routes.ts +++ b/src/app/admin/admin-notifications/admin-notifications-routes.ts @@ -1,12 +1,12 @@ import { Route } from '@angular/router'; -import { AuthenticatedGuard } from '../../core/auth/authenticated.guard'; -import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver'; -import { QualityAssuranceBreadcrumbResolver } from '../../core/breadcrumbs/quality-assurance-breadcrumb.resolver'; +import { authenticatedGuard } from '../../core/auth/authenticated.guard'; +import { i18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver'; +import { qualityAssuranceBreadcrumbResolver } from '../../core/breadcrumbs/quality-assurance-breadcrumb.resolver'; import { AdminNotificationsPublicationClaimPageResolver } from '../../quality-assurance-notifications-pages/notifications-suggestion-targets-page/notifications-suggestion-targets-page-resolver.service'; import { QualityAssuranceEventsPageComponent } from '../../quality-assurance-notifications-pages/quality-assurance-events-page/quality-assurance-events-page.component'; -import { QualityAssuranceEventsPageResolver } from '../../quality-assurance-notifications-pages/quality-assurance-events-page/quality-assurance-events-page.resolver'; -import { SourceDataResolver } from '../../quality-assurance-notifications-pages/quality-assurance-source-page-component/quality-assurance-source-data.resolver'; +import { qualityAssuranceEventsPageResolver } from '../../quality-assurance-notifications-pages/quality-assurance-events-page/quality-assurance-events-page.resolver'; +import { qualityAssuranceSourceDataResolver } from '../../quality-assurance-notifications-pages/quality-assurance-source-page-component/quality-assurance-source-data.resolver'; import { QualityAssuranceSourcePageComponent } from '../../quality-assurance-notifications-pages/quality-assurance-source-page-component/quality-assurance-source-page.component'; import { QualityAssuranceSourcePageResolver } from '../../quality-assurance-notifications-pages/quality-assurance-source-page-component/quality-assurance-source-page-resolver.service'; import { QualityAssuranceTopicsPageComponent } from '../../quality-assurance-notifications-pages/quality-assurance-topics-page/quality-assurance-topics-page.component'; @@ -19,12 +19,12 @@ import { export const ROUTES: Route[] = [ { - canActivate: [ AuthenticatedGuard ], + canActivate: [ authenticatedGuard ], path: `${PUBLICATION_CLAIMS_PATH}`, component: AdminNotificationsPublicationClaimPageComponent, pathMatch: 'full', resolve: { - breadcrumb: I18nBreadcrumbResolver, + breadcrumb: i18nBreadcrumbResolver, suggestionTargetParams: AdminNotificationsPublicationClaimPageResolver, }, data: { @@ -34,12 +34,12 @@ export const ROUTES: Route[] = [ }, }, { - canActivate: [AuthenticatedGuard], + canActivate: [authenticatedGuard], path: `${QUALITY_ASSURANCE_EDIT_PATH}/:sourceId`, component: QualityAssuranceTopicsPageComponent, pathMatch: 'full', resolve: { - breadcrumb: QualityAssuranceBreadcrumbResolver, + breadcrumb: qualityAssuranceBreadcrumbResolver, openaireQualityAssuranceTopicsParams: QualityAssuranceTopicsPageResolver, }, data: { @@ -49,12 +49,12 @@ export const ROUTES: Route[] = [ }, }, { - canActivate: [ AuthenticatedGuard ], + canActivate: [ authenticatedGuard ], path: `${QUALITY_ASSURANCE_EDIT_PATH}/:sourceId/target/:targetId`, component: QualityAssuranceTopicsPageComponent, pathMatch: 'full', resolve: { - breadcrumb: I18nBreadcrumbResolver, + breadcrumb: i18nBreadcrumbResolver, openaireQualityAssuranceTopicsParams: QualityAssuranceTopicsPageResolver, }, data: { @@ -64,14 +64,14 @@ export const ROUTES: Route[] = [ }, }, { - canActivate: [AuthenticatedGuard], + canActivate: [authenticatedGuard], path: `${QUALITY_ASSURANCE_EDIT_PATH}`, component: QualityAssuranceSourcePageComponent, pathMatch: 'full', resolve: { - breadcrumb: I18nBreadcrumbResolver, + breadcrumb: i18nBreadcrumbResolver, openaireQualityAssuranceSourceParams: QualityAssuranceSourcePageResolver, - sourceData: SourceDataResolver, + sourceData: qualityAssuranceSourceDataResolver, }, data: { title: 'admin.notifications.source.breadcrumbs', @@ -80,13 +80,13 @@ export const ROUTES: Route[] = [ }, }, { - canActivate: [AuthenticatedGuard], + canActivate: [authenticatedGuard], path: `${QUALITY_ASSURANCE_EDIT_PATH}/:sourceId/:topicId`, component: QualityAssuranceEventsPageComponent, pathMatch: 'full', resolve: { - breadcrumb: QualityAssuranceBreadcrumbResolver, - openaireQualityAssuranceEventsParams: QualityAssuranceEventsPageResolver, + breadcrumb: qualityAssuranceBreadcrumbResolver, + openaireQualityAssuranceEventsParams: qualityAssuranceEventsPageResolver, }, data: { title: 'admin.notifications.event.page.title', diff --git a/src/app/admin/admin-notify-dashboard/admin-notify-dashboard-routes.ts b/src/app/admin/admin-notify-dashboard/admin-notify-dashboard-routes.ts index b280d3eff6..c193148cc4 100644 --- a/src/app/admin/admin-notify-dashboard/admin-notify-dashboard-routes.ts +++ b/src/app/admin/admin-notify-dashboard/admin-notify-dashboard-routes.ts @@ -1,7 +1,10 @@ -import { Route } from '@angular/router'; +import { + mapToCanActivate, + Route, +} from '@angular/router'; -import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver'; -import { NotifyInfoGuard } from '../../core/coar-notify/notify-info/notify-info.guard'; +import { i18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver'; +import { notifyInfoGuard } from '../../core/coar-notify/notify-info/notify-info.guard'; import { SiteAdministratorGuard } from '../../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard'; import { AdminNotifyDashboardComponent } from './admin-notify-dashboard.component'; import { AdminNotifyIncomingComponent } from './admin-notify-logs/admin-notify-incoming/admin-notify-incoming.component'; @@ -9,10 +12,10 @@ import { AdminNotifyOutgoingComponent } from './admin-notify-logs/admin-notify-o export const ROUTES: Route[] = [ { - canActivate: [SiteAdministratorGuard, NotifyInfoGuard], + canActivate: [...mapToCanActivate([SiteAdministratorGuard]), notifyInfoGuard], path: '', resolve: { - breadcrumb: I18nBreadcrumbResolver, + breadcrumb: i18nBreadcrumbResolver, }, component: AdminNotifyDashboardComponent, pathMatch: 'full', @@ -24,10 +27,10 @@ export const ROUTES: Route[] = [ { path: 'inbound', resolve: { - breadcrumb: I18nBreadcrumbResolver, + breadcrumb: i18nBreadcrumbResolver, }, component: AdminNotifyIncomingComponent, - canActivate: [SiteAdministratorGuard, NotifyInfoGuard], + canActivate: [...mapToCanActivate([SiteAdministratorGuard]), notifyInfoGuard], data: { title: 'admin.notify.dashboard.page.title', breadcrumbKey: 'admin.notify.dashboard', @@ -36,10 +39,10 @@ export const ROUTES: Route[] = [ { path: 'outbound', resolve: { - breadcrumb: I18nBreadcrumbResolver, + breadcrumb: i18nBreadcrumbResolver, }, component: AdminNotifyOutgoingComponent, - canActivate: [SiteAdministratorGuard, NotifyInfoGuard], + canActivate: [...mapToCanActivate([SiteAdministratorGuard]), notifyInfoGuard], data: { title: 'admin.notify.dashboard.page.title', breadcrumbKey: 'admin.notify.dashboard', diff --git a/src/app/admin/admin-registries/admin-registries-routes.ts b/src/app/admin/admin-registries/admin-registries-routes.ts index b7c1bf1751..06aaa934ab 100644 --- a/src/app/admin/admin-registries/admin-registries-routes.ts +++ b/src/app/admin/admin-registries/admin-registries-routes.ts @@ -1,6 +1,6 @@ import { Route } from '@angular/router'; -import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver'; +import { i18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver'; import { BITSTREAMFORMATS_MODULE_PATH } from './admin-registries-routing-paths'; import { MetadataRegistryComponent } from './metadata-registry/metadata-registry.component'; import { MetadataSchemaComponent } from './metadata-schema/metadata-schema.component'; @@ -8,7 +8,7 @@ import { MetadataSchemaComponent } from './metadata-schema/metadata-schema.compo export const ROUTES: Route[] = [ { path: 'metadata', - resolve: { breadcrumb: I18nBreadcrumbResolver }, + resolve: { breadcrumb: i18nBreadcrumbResolver }, data: { title: 'admin.registries.metadata.title', breadcrumbKey: 'admin.registries.metadata' }, children: [ { @@ -17,7 +17,7 @@ export const ROUTES: Route[] = [ }, { path: ':schemaName', - resolve: { breadcrumb: I18nBreadcrumbResolver }, + resolve: { breadcrumb: i18nBreadcrumbResolver }, component: MetadataSchemaComponent, data: { title: 'admin.registries.schema.title', breadcrumbKey: 'admin.registries.schema' }, }, @@ -25,7 +25,7 @@ export const ROUTES: Route[] = [ }, { path: BITSTREAMFORMATS_MODULE_PATH, - resolve: { breadcrumb: I18nBreadcrumbResolver }, + resolve: { breadcrumb: i18nBreadcrumbResolver }, loadChildren: () => import('./bitstream-formats/bitstream-formats-routes') .then((m) => m.ROUTES), data: { title: 'admin.registries.bitstream-formats.title', breadcrumbKey: 'admin.registries.bitstream-formats' }, diff --git a/src/app/admin/admin-registries/bitstream-formats/bitstream-formats-routes.ts b/src/app/admin/admin-registries/bitstream-formats/bitstream-formats-routes.ts index 27af558c44..5c85d194a5 100644 --- a/src/app/admin/admin-registries/bitstream-formats/bitstream-formats-routes.ts +++ b/src/app/admin/admin-registries/bitstream-formats/bitstream-formats-routes.ts @@ -1,9 +1,9 @@ import { Route } from '@angular/router'; -import { I18nBreadcrumbResolver } from '../../../core/breadcrumbs/i18n-breadcrumb.resolver'; +import { i18nBreadcrumbResolver } from '../../../core/breadcrumbs/i18n-breadcrumb.resolver'; import { AddBitstreamFormatComponent } from './add-bitstream-format/add-bitstream-format.component'; import { BitstreamFormatsComponent } from './bitstream-formats.component'; -import { BitstreamFormatsResolver } from './bitstream-formats.resolver'; +import { bitstreamFormatsResolver } from './bitstream-formats.resolver'; import { EditBitstreamFormatComponent } from './edit-bitstream-format/edit-bitstream-format.component'; const BITSTREAMFORMAT_EDIT_PATH = ':id/edit'; @@ -19,7 +19,7 @@ export const ROUTES: Route[] = [ }, { path: BITSTREAMFORMAT_ADD_PATH, - resolve: { breadcrumb: I18nBreadcrumbResolver }, + resolve: { breadcrumb: i18nBreadcrumbResolver }, providers, component: AddBitstreamFormatComponent, data: { breadcrumbKey: 'admin.registries.bitstream-formats.create' }, @@ -29,8 +29,8 @@ export const ROUTES: Route[] = [ providers, component: EditBitstreamFormatComponent, resolve: { - bitstreamFormat: BitstreamFormatsResolver, - breadcrumb: I18nBreadcrumbResolver, + bitstreamFormat: bitstreamFormatsResolver, + breadcrumb: i18nBreadcrumbResolver, }, data: { breadcrumbKey: 'admin.registries.bitstream-formats.edit' }, }, diff --git a/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.spec.ts b/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.spec.ts index 321d946907..9ee767cda0 100644 --- a/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.spec.ts +++ b/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.spec.ts @@ -31,6 +31,7 @@ import { GroupDataService } from '../../../core/eperson/group-data.service'; import { PaginationService } from '../../../core/pagination/pagination.service'; import { BitstreamFormat } from '../../../core/shared/bitstream-format.model'; import { BitstreamFormatSupportLevel } from '../../../core/shared/bitstream-format-support-level'; +import { XSRFService } from '../../../core/xsrf/xsrf.service'; import { HostWindowService } from '../../../shared/host-window.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { PaginationComponent } from '../../../shared/pagination/pagination.component'; @@ -143,6 +144,7 @@ describe('BitstreamFormatsComponent', () => { { provide: PaginationService, useValue: paginationService }, { provide: GroupDataService, useValue: groupDataService }, { provide: ConfigurationDataService, useValue: configurationDataService }, + { provide: XSRFService, useValue: {} }, ], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); diff --git a/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.resolver.ts b/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.resolver.ts index 561491bc64..366f5a682b 100644 --- a/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.resolver.ts +++ b/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.resolver.ts @@ -1,6 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; import { Observable } from 'rxjs'; @@ -11,24 +12,20 @@ import { BitstreamFormat } from '../../../core/shared/bitstream-format.model'; import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; /** - * This class represents a resolver that requests a specific bitstreamFormat before the route is activated + * Method for resolving an bitstreamFormat based on the parameters in the current route + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state + * @param {BitstreamFormatDataService} bitstreamFormatDataService The BitstreamFormatDataService + * @returns Observable<> Emits the found bitstreamFormat based on the parameters in the current route, + * or an error if something went wrong */ -@Injectable({ providedIn: 'root' }) -export class BitstreamFormatsResolver { - constructor(private bitstreamFormatDataService: BitstreamFormatDataService) { - } - - /** - * Method for resolving an bitstreamFormat based on the parameters in the current route - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - * @param {RouterStateSnapshot} state The current RouterStateSnapshot - * @returns Observable<> Emits the found bitstreamFormat based on the parameters in the current route, - * or an error if something went wrong - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - return this.bitstreamFormatDataService.findById(route.params.id) - .pipe( - getFirstCompletedRemoteData(), - ); - } -} +export const bitstreamFormatsResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + bitstreamFormatDataService: BitstreamFormatDataService = inject(BitstreamFormatDataService), +): Observable> => { + return bitstreamFormatDataService.findById(route.params.id) + .pipe( + getFirstCompletedRemoteData(), + ); +}; diff --git a/src/app/admin/admin-reports/admin-reports-routes.ts b/src/app/admin/admin-reports/admin-reports-routes.ts index 7f19cc4bce..be1f7cc7a0 100644 --- a/src/app/admin/admin-reports/admin-reports-routes.ts +++ b/src/app/admin/admin-reports/admin-reports-routes.ts @@ -1,13 +1,13 @@ import { Route } from '@angular/router'; -import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver'; +import { i18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver'; import { FilteredCollectionsComponent } from './filtered-collections/filtered-collections.component'; import { FilteredItemsComponent } from './filtered-items/filtered-items.component'; export const ROUTES: Route[] = [ { path: 'collections', - resolve: { breadcrumb: I18nBreadcrumbResolver }, + resolve: { breadcrumb: i18nBreadcrumbResolver }, data: { title: 'admin.reports.collections.title', breadcrumbKey: 'admin.reports.collections' }, children: [ { @@ -18,7 +18,7 @@ export const ROUTES: Route[] = [ }, { path: 'queries', - resolve: { breadcrumb: I18nBreadcrumbResolver }, + resolve: { breadcrumb: i18nBreadcrumbResolver }, data: { title: 'admin.reports.items.title', breadcrumbKey: 'admin.reports.items' }, children: [ { diff --git a/src/app/admin/admin-routes.ts b/src/app/admin/admin-routes.ts index 7df3bd289a..a89dbb2c0a 100644 --- a/src/app/admin/admin-routes.ts +++ b/src/app/admin/admin-routes.ts @@ -1,6 +1,6 @@ import { Route } from '@angular/router'; -import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; +import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; import { AdminCurationTasksComponent } from './admin-curation-tasks/admin-curation-tasks.component'; import { BatchImportPageComponent } from './admin-import-batch-page/batch-import-page.component'; import { MetadataImportPageComponent } from './admin-import-metadata-page/metadata-import-page.component'; @@ -27,37 +27,37 @@ export const ROUTES: Route[] = [ }, { path: 'search', - resolve: { breadcrumb: I18nBreadcrumbResolver }, + resolve: { breadcrumb: i18nBreadcrumbResolver }, component: AdminSearchPageComponent, data: { title: 'admin.search.title', breadcrumbKey: 'admin.search' }, }, { path: 'workflow', - resolve: { breadcrumb: I18nBreadcrumbResolver }, + resolve: { breadcrumb: i18nBreadcrumbResolver }, component: AdminWorkflowPageComponent, data: { title: 'admin.workflow.title', breadcrumbKey: 'admin.workflow' }, }, { path: 'curation-tasks', - resolve: { breadcrumb: I18nBreadcrumbResolver }, + resolve: { breadcrumb: i18nBreadcrumbResolver }, component: AdminCurationTasksComponent, data: { title: 'admin.curation-tasks.title', breadcrumbKey: 'admin.curation-tasks' }, }, { path: 'metadata-import', - resolve: { breadcrumb: I18nBreadcrumbResolver }, + resolve: { breadcrumb: i18nBreadcrumbResolver }, component: MetadataImportPageComponent, data: { title: 'admin.metadata-import.title', breadcrumbKey: 'admin.metadata-import' }, }, { path: 'batch-import', - resolve: { breadcrumb: I18nBreadcrumbResolver }, + resolve: { breadcrumb: i18nBreadcrumbResolver }, component: BatchImportPageComponent, data: { title: 'admin.batch-import.title', breadcrumbKey: 'admin.batch-import' }, }, { path: 'system-wide-alert', - resolve: { breadcrumb: I18nBreadcrumbResolver }, + resolve: { breadcrumb: i18nBreadcrumbResolver }, loadChildren: () => import('../system-wide-alert/system-wide-alert-routes').then((m) => m.ROUTES), data: { title: 'admin.system-wide-alert.title', breadcrumbKey: 'admin.system-wide-alert' }, }, diff --git a/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component.spec.ts b/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component.spec.ts index fbb936080f..cd722542f6 100644 --- a/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component.spec.ts +++ b/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component.spec.ts @@ -17,6 +17,7 @@ import { AuthorizationDataService } from '../../../../../core/data/feature-autho import { Item } from '../../../../../core/shared/item.model'; import { ViewMode } from '../../../../../core/shared/view-mode.model'; import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model'; +import { XSRFService } from '../../../../../core/xsrf/xsrf.service'; import { AuthServiceMock } from '../../../../../shared/mocks/auth.service.mock'; import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock'; import { getMockLinkService } from '../../../../../shared/mocks/link-service.mock'; @@ -67,6 +68,7 @@ describe('WorkflowItemSearchResultAdminWorkflowListElementComponent', () => { { provide: ThemeService, useValue: getMockThemeService() }, { provide: AuthService, useValue: new AuthServiceMock() }, { provide: AuthorizationDataService, useValue: {} }, + { provide: XSRFService, useValue: {} }, ], schemas: [NO_ERRORS_SCHEMA], }) diff --git a/src/app/app-routes.ts b/src/app/app-routes.ts index 3d5dbbaba4..8b7f6acd47 100644 --- a/src/app/app-routes.ts +++ b/src/app/app-routes.ts @@ -1,5 +1,6 @@ import { InMemoryScrollingOptions, + mapToCanActivate, Route, RouterConfigOptions, } from '@angular/router'; @@ -23,18 +24,18 @@ import { } from './app-routing-paths'; import { COLLECTION_MODULE_PATH } from './collection-page/collection-page-routing-paths'; import { COMMUNITY_MODULE_PATH } from './community-page/community-page-routing-paths'; -import { AuthBlockingGuard } from './core/auth/auth-blocking.guard'; -import { AuthenticatedGuard } from './core/auth/authenticated.guard'; +import { authBlockingGuard } from './core/auth/auth-blocking.guard'; +import { authenticatedGuard } from './core/auth/authenticated.guard'; import { GroupAdministratorGuard } from './core/data/feature-authorization/feature-authorization-guard/group-administrator.guard'; import { SiteAdministratorGuard } from './core/data/feature-authorization/feature-authorization-guard/site-administrator.guard'; import { SiteRegisterGuard } from './core/data/feature-authorization/feature-authorization-guard/site-register.guard'; import { EndUserAgreementCurrentUserGuard } from './core/end-user-agreement/end-user-agreement-current-user.guard'; -import { ReloadGuard } from './core/reload/reload.guard'; +import { reloadGuard } from './core/reload/reload.guard'; import { ForgotPasswordCheckGuard } from './core/rest-property/forgot-password-check-guard.guard'; import { ServerCheckGuard } from './core/server-check/server-check.guard'; import { ThemedForbiddenComponent } from './forbidden/themed-forbidden.component'; import { ITEM_MODULE_PATH } from './item-page/item-page-routing-paths'; -import { MenuResolver } from './menu.resolver'; +import { menuResolver } from './menuResolver'; import { provideSuggestionNotificationsState } from './notifications/provide-suggestion-notifications-state'; import { ThemedPageErrorComponent } from './page-error/themed-page-error.component'; import { ThemedPageInternalServerErrorComponent } from './page-internal-server-error/themed-page-internal-server-error.component'; @@ -48,16 +49,16 @@ export const APP_ROUTES: Route[] = [ { path: ERROR_PAGE, component: ThemedPageErrorComponent }, { path: '', - canActivate: [AuthBlockingGuard], + canActivate: [authBlockingGuard], canActivateChild: [ServerCheckGuard], - resolve: [MenuResolver], + resolve: [menuResolver], children: [ { path: '', redirectTo: '/home', pathMatch: 'full' }, { path: 'reload/:rnd', component: ThemedPageNotFoundComponent, pathMatch: 'full', - canActivate: [ReloadGuard], + canActivate: [reloadGuard], }, { path: 'home', @@ -65,105 +66,105 @@ export const APP_ROUTES: Route[] = [ .then((m) => m.ROUTES), data: { showBreadcrumbs: false }, providers: [provideSuggestionNotificationsState()], - canActivate: [EndUserAgreementCurrentUserGuard], + canActivate: mapToCanActivate([EndUserAgreementCurrentUserGuard]), }, { path: 'community-list', loadChildren: () => import('./community-list-page/community-list-page-routes') .then((m) => m.ROUTES), - canActivate: [EndUserAgreementCurrentUserGuard], + canActivate: mapToCanActivate([EndUserAgreementCurrentUserGuard]), }, { path: 'id', loadChildren: () => import('./lookup-by-id/lookup-by-id-routes') .then((m) => m.ROUTES), - canActivate: [EndUserAgreementCurrentUserGuard], + canActivate: mapToCanActivate([EndUserAgreementCurrentUserGuard]), }, { path: 'handle', loadChildren: () => import('./lookup-by-id/lookup-by-id-routes') .then((m) => m.ROUTES), - canActivate: [EndUserAgreementCurrentUserGuard], + canActivate: mapToCanActivate([EndUserAgreementCurrentUserGuard]), }, { path: REGISTER_PATH, loadChildren: () => import('./register-page/register-page-routes') .then((m) => m.ROUTES), - canActivate: [SiteRegisterGuard], + canActivate: mapToCanActivate([SiteRegisterGuard]), }, { path: FORGOT_PASSWORD_PATH, loadChildren: () => import('./forgot-password/forgot-password-routes') .then((m) => m.ROUTES), - canActivate: [EndUserAgreementCurrentUserGuard, ForgotPasswordCheckGuard], + canActivate: mapToCanActivate([EndUserAgreementCurrentUserGuard, ForgotPasswordCheckGuard]), }, { path: COMMUNITY_MODULE_PATH, loadChildren: () => import('./community-page/community-page-routes') .then((m) => m.ROUTES), - canActivate: [EndUserAgreementCurrentUserGuard], + canActivate: mapToCanActivate([EndUserAgreementCurrentUserGuard]), }, { path: COLLECTION_MODULE_PATH, loadChildren: () => import('./collection-page/collection-page-routes') .then((m) => m.ROUTES), - canActivate: [EndUserAgreementCurrentUserGuard], + canActivate: mapToCanActivate([EndUserAgreementCurrentUserGuard]), }, { path: ITEM_MODULE_PATH, loadChildren: () => import('./item-page/item-page-routes') .then((m) => m.ROUTES), - canActivate: [EndUserAgreementCurrentUserGuard], + canActivate: mapToCanActivate([EndUserAgreementCurrentUserGuard]), }, { path: 'entities/:entity-type', loadChildren: () => import('./item-page/item-page-routes') .then((m) => m.ROUTES), - canActivate: [EndUserAgreementCurrentUserGuard], + canActivate: mapToCanActivate([EndUserAgreementCurrentUserGuard]), }, { path: LEGACY_BITSTREAM_MODULE_PATH, loadChildren: () => import('./bitstream-page/bitstream-page-routes') .then((m) => m.ROUTES), - canActivate: [EndUserAgreementCurrentUserGuard], + canActivate: mapToCanActivate([EndUserAgreementCurrentUserGuard]), }, { path: BITSTREAM_MODULE_PATH, loadChildren: () => import('./bitstream-page/bitstream-page-routes') .then((m) => m.ROUTES), - canActivate: [EndUserAgreementCurrentUserGuard], + canActivate: mapToCanActivate([EndUserAgreementCurrentUserGuard]), }, { path: 'mydspace', loadChildren: () => import('./my-dspace-page/my-dspace-page-routes') .then((m) => m.ROUTES), providers: [provideSuggestionNotificationsState()], - canActivate: [AuthenticatedGuard, EndUserAgreementCurrentUserGuard], + canActivate: [authenticatedGuard, ...mapToCanActivate([EndUserAgreementCurrentUserGuard])], }, { path: 'search', loadChildren: () => import('./search-page/search-page-routes') .then((m) => m.ROUTES), - canActivate: [EndUserAgreementCurrentUserGuard], + canActivate: mapToCanActivate([EndUserAgreementCurrentUserGuard]), }, { path: 'browse', loadChildren: () => import('./browse-by/browse-by-page-routes') .then((m) => m.ROUTES), - canActivate: [EndUserAgreementCurrentUserGuard], + canActivate: mapToCanActivate([EndUserAgreementCurrentUserGuard]), }, { path: ADMIN_MODULE_PATH, loadChildren: () => import('./admin/admin-routes') .then((m) => m.ROUTES), - canActivate: [SiteAdministratorGuard, EndUserAgreementCurrentUserGuard], + canActivate: mapToCanActivate([SiteAdministratorGuard, EndUserAgreementCurrentUserGuard]), }, { path: NOTIFICATIONS_MODULE_PATH, loadChildren: () => import('./quality-assurance-notifications-pages/notifications-pages-routes') .then((m) => m.ROUTES), providers: [provideSuggestionNotificationsState()], - canActivate: [AuthenticatedGuard, EndUserAgreementCurrentUserGuard], + canActivate: [authenticatedGuard, ...mapToCanActivate([EndUserAgreementCurrentUserGuard])], }, { path: 'login', @@ -180,47 +181,47 @@ export const APP_ROUTES: Route[] = [ loadChildren: () => import('./submit-page/submit-page-routes') .then((m) => m.ROUTES), providers: [provideSubmissionState()], - canActivate: [EndUserAgreementCurrentUserGuard], + canActivate: mapToCanActivate([EndUserAgreementCurrentUserGuard]), }, { path: 'import-external', loadChildren: () => import('./import-external-page/import-external-page-routes') .then((m) => m.ROUTES), - canActivate: [EndUserAgreementCurrentUserGuard], + canActivate: mapToCanActivate([EndUserAgreementCurrentUserGuard]), }, { path: 'workspaceitems', loadChildren: () => import('./workspaceitems-edit-page/workspaceitems-edit-page-routes') .then((m) => m.ROUTES), providers: [provideSubmissionState()], - canActivate: [EndUserAgreementCurrentUserGuard], + canActivate: mapToCanActivate([EndUserAgreementCurrentUserGuard]), }, { path: WORKFLOW_ITEM_MODULE_PATH, providers: [provideSubmissionState()], loadChildren: () => import('./workflowitems-edit-page/workflowitems-edit-page-routes') .then((m) => m.ROUTES), - canActivate: [EndUserAgreementCurrentUserGuard], + canActivate: mapToCanActivate([EndUserAgreementCurrentUserGuard]), }, { path: PROFILE_MODULE_PATH, loadChildren: () => import('./profile-page/profile-page-routes') .then((m) => m.ROUTES), providers: [provideSuggestionNotificationsState()], - canActivate: [AuthenticatedGuard, EndUserAgreementCurrentUserGuard], + canActivate: [authenticatedGuard, ...mapToCanActivate([EndUserAgreementCurrentUserGuard])], }, { path: PROCESS_MODULE_PATH, loadChildren: () => import('./process-page/process-page-routes') .then((m) => m.ROUTES), - canActivate: [AuthenticatedGuard, EndUserAgreementCurrentUserGuard], + canActivate: [authenticatedGuard, ...mapToCanActivate([EndUserAgreementCurrentUserGuard])], }, { path: SUGGESTION_MODULE_PATH, loadChildren: () => import('./suggestions-page/suggestions-page-routes') .then((m) => m.ROUTES), providers: [provideSuggestionNotificationsState()], - canActivate: [AuthenticatedGuard, EndUserAgreementCurrentUserGuard], + canActivate: [authenticatedGuard, ...mapToCanActivate([EndUserAgreementCurrentUserGuard])], }, { path: INFO_MODULE_PATH, @@ -229,7 +230,7 @@ export const APP_ROUTES: Route[] = [ { path: REQUEST_COPY_MODULE_PATH, loadChildren: () => import('./request-copy/request-copy-routes').then((m) => m.ROUTES), - canActivate: [EndUserAgreementCurrentUserGuard], + canActivate: mapToCanActivate([EndUserAgreementCurrentUserGuard]), }, { path: FORBIDDEN_PATH, @@ -239,7 +240,7 @@ export const APP_ROUTES: Route[] = [ path: 'statistics', loadChildren: () => import('./statistics-page/statistics-page-routes') .then((m) => m.ROUTES), - canActivate: [EndUserAgreementCurrentUserGuard], + canActivate: mapToCanActivate([EndUserAgreementCurrentUserGuard]), }, { path: HEALTH_PAGE_PATH, @@ -249,13 +250,13 @@ export const APP_ROUTES: Route[] = [ { path: ACCESS_CONTROL_MODULE_PATH, loadChildren: () => import('./access-control/access-control-routes').then((m) => m.ROUTES), - canActivate: [GroupAdministratorGuard, EndUserAgreementCurrentUserGuard], + canActivate: mapToCanActivate([GroupAdministratorGuard, EndUserAgreementCurrentUserGuard]), }, { path: 'subscriptions', loadChildren: () => import('./subscriptions-page/subscriptions-page-routes') .then((m) => m.ROUTES), - canActivate: [AuthenticatedGuard], + canActivate: [authenticatedGuard], }, { path: '**', pathMatch: 'full', component: ThemedPageNotFoundComponent }, ], diff --git a/src/app/app-routing-paths.ts b/src/app/app-routing-paths.ts index 9b95ee0d0a..d6df53ffaf 100644 --- a/src/app/app-routing-paths.ts +++ b/src/app/app-routing-paths.ts @@ -36,7 +36,7 @@ export function getBitstreamRequestACopyRoute(item, bitstream): { routerLink: st } export const COAR_NOTIFY_SUPPORT = 'coar-notify-support'; -export const HOME_PAGE_PATH = 'admin'; +export const HOME_PAGE_PATH = 'home'; export function getHomePageRoute() { return `/${HOME_PAGE_PATH}`; diff --git a/src/app/bitstream-page/bitstream-page-routes.ts b/src/app/bitstream-page/bitstream-page-routes.ts index 6f4c3378ce..73848a7f4e 100644 --- a/src/app/bitstream-page/bitstream-page-routes.ts +++ b/src/app/bitstream-page/bitstream-page-routes.ts @@ -1,17 +1,17 @@ import { Route } from '@angular/router'; -import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; -import { BitstreamBreadcrumbResolver } from '../core/breadcrumbs/bitstream-breadcrumb.resolver'; -import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; +import { authenticatedGuard } from '../core/auth/authenticated.guard'; +import { bitstreamBreadcrumbResolver } from '../core/breadcrumbs/bitstream-breadcrumb.resolver'; +import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; import { ResourcePolicyCreateComponent } from '../shared/resource-policies/create/resource-policy-create.component'; import { ResourcePolicyEditComponent } from '../shared/resource-policies/edit/resource-policy-edit.component'; -import { ResourcePolicyResolver } from '../shared/resource-policies/resolvers/resource-policy.resolver'; -import { ResourcePolicyTargetResolver } from '../shared/resource-policies/resolvers/resource-policy-target.resolver'; +import { resourcePolicyResolver } from '../shared/resource-policies/resolvers/resource-policy.resolver'; +import { resourcePolicyTargetResolver } from '../shared/resource-policies/resolvers/resource-policy-target.resolver'; import { BitstreamAuthorizationsComponent } from './bitstream-authorizations/bitstream-authorizations.component'; import { BitstreamDownloadPageComponent } from './bitstream-download-page/bitstream-download-page.component'; -import { BitstreamPageResolver } from './bitstream-page.resolver'; +import { bitstreamPageResolver } from './bitstream-page.resolver'; import { ThemedEditBitstreamPageComponent } from './edit-bitstream-page/themed-edit-bitstream-page.component'; -import { LegacyBitstreamUrlResolver } from './legacy-bitstream-url.resolver'; +import { legacyBitstreamUrlResolver } from './legacy-bitstream-url.resolver'; const EDIT_BITSTREAM_PATH = ':id/edit'; const EDIT_BITSTREAM_AUTHORIZATIONS_PATH = ':id/authorizations'; @@ -25,7 +25,7 @@ export const ROUTES: Route[] = [ path: 'handle/:prefix/:suffix/:filename', component: BitstreamDownloadPageComponent, resolve: { - bitstream: LegacyBitstreamUrlResolver, + bitstream: legacyBitstreamUrlResolver, }, }, { @@ -33,7 +33,7 @@ export const ROUTES: Route[] = [ path: ':prefix/:suffix/:sequence_id/:filename', component: BitstreamDownloadPageComponent, resolve: { - bitstream: LegacyBitstreamUrlResolver, + bitstream: legacyBitstreamUrlResolver, }, }, { @@ -41,17 +41,17 @@ export const ROUTES: Route[] = [ path: ':id/download', component: BitstreamDownloadPageComponent, resolve: { - bitstream: BitstreamPageResolver, + bitstream: bitstreamPageResolver, }, }, { path: EDIT_BITSTREAM_PATH, component: ThemedEditBitstreamPageComponent, resolve: { - bitstream: BitstreamPageResolver, - breadcrumb: BitstreamBreadcrumbResolver, + bitstream: bitstreamPageResolver, + breadcrumb: bitstreamBreadcrumbResolver, }, - canActivate: [AuthenticatedGuard], + canActivate: [authenticatedGuard], }, { path: EDIT_BITSTREAM_AUTHORIZATIONS_PATH, @@ -59,7 +59,7 @@ export const ROUTES: Route[] = [ { path: 'create', resolve: { - resourcePolicyTarget: ResourcePolicyTargetResolver, + resourcePolicyTarget: resourcePolicyTargetResolver, }, component: ResourcePolicyCreateComponent, data: { title: 'resource-policies.create.page.title', showBreadcrumbs: true }, @@ -67,8 +67,8 @@ export const ROUTES: Route[] = [ { path: 'edit', resolve: { - breadcrumb: I18nBreadcrumbResolver, - resourcePolicy: ResourcePolicyResolver, + breadcrumb: i18nBreadcrumbResolver, + resourcePolicy: resourcePolicyResolver, }, component: ResourcePolicyEditComponent, data: { breadcrumbKey: 'item.edit', title: 'resource-policies.edit.page.title', showBreadcrumbs: true }, @@ -76,8 +76,8 @@ export const ROUTES: Route[] = [ { path: '', resolve: { - bitstream: BitstreamPageResolver, - breadcrumb: BitstreamBreadcrumbResolver, + bitstream: bitstreamPageResolver, + breadcrumb: bitstreamBreadcrumbResolver, }, component: BitstreamAuthorizationsComponent, data: { title: 'bitstream.edit.authorizations.title', showBreadcrumbs: true }, diff --git a/src/app/bitstream-page/bitstream-page.resolver.ts b/src/app/bitstream-page/bitstream-page.resolver.ts index 7f6f5c1740..f6a039b1d8 100644 --- a/src/app/bitstream-page/bitstream-page.resolver.ts +++ b/src/app/bitstream-page/bitstream-page.resolver.ts @@ -1,6 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; import { Observable } from 'rxjs'; @@ -24,32 +25,20 @@ export const BITSTREAM_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig[] = [ ]; /** - * This class represents a resolver that requests a specific bitstream before the route is activated + * Method for resolving a bitstream based on the parameters in the current route + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @param {BitstreamDataService} bitstreamService + * @returns Observable<> Emits the found bitstream based on the parameters in the current route, + * or an error if something went wrong */ -@Injectable({ providedIn: 'root' }) -export class BitstreamPageResolver { - constructor(private bitstreamService: BitstreamDataService) { - } - - /** - * Method for resolving a bitstream based on the parameters in the current route - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - * @param {RouterStateSnapshot} state The current RouterStateSnapshot - * @returns Observable<> Emits the found bitstream based on the parameters in the current route, - * or an error if something went wrong - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - return this.bitstreamService.findById(route.params.id, true, false, ...this.followLinks) - .pipe( - getFirstCompletedRemoteData(), - ); - } - /** - * Method that returns the follow links to already resolve - * The self links defined in this list are expected to be requested somewhere in the near future - * Requesting them as embeds will limit the number of requests - */ - get followLinks(): FollowLinkConfig[] { - return BITSTREAM_PAGE_LINKS_TO_FOLLOW; - } -} +export const bitstreamPageResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + bitstreamService: BitstreamDataService = inject(BitstreamDataService), +): Observable> => { + return bitstreamService.findById(route.params.id, true, false, ...BITSTREAM_PAGE_LINKS_TO_FOLLOW) + .pipe( + getFirstCompletedRemoteData(), + ); +}; diff --git a/src/app/bitstream-page/legacy-bitstream-url.resolver.spec.ts b/src/app/bitstream-page/legacy-bitstream-url.resolver.spec.ts index 9fd2dc315a..ec67d530df 100644 --- a/src/app/bitstream-page/legacy-bitstream-url.resolver.spec.ts +++ b/src/app/bitstream-page/legacy-bitstream-url.resolver.spec.ts @@ -4,10 +4,10 @@ import { TestScheduler } from 'rxjs/testing'; import { BitstreamDataService } from '../core/data/bitstream-data.service'; import { RemoteData } from '../core/data/remote-data'; import { RequestEntryState } from '../core/data/request-entry-state.model'; -import { LegacyBitstreamUrlResolver } from './legacy-bitstream-url.resolver'; +import { legacyBitstreamUrlResolver } from './legacy-bitstream-url.resolver'; -describe(`LegacyBitstreamUrlResolver`, () => { - let resolver: LegacyBitstreamUrlResolver; +describe(`legacyBitstreamUrlResolver`, () => { + let resolver: any; let bitstreamDataService: BitstreamDataService; let testScheduler; let remoteDataMocks; @@ -33,7 +33,7 @@ describe(`LegacyBitstreamUrlResolver`, () => { bitstreamDataService = { findByItemHandle: () => undefined, } as any; - resolver = new LegacyBitstreamUrlResolver(bitstreamDataService); + resolver = legacyBitstreamUrlResolver; }); describe(`resolve`, () => { @@ -51,7 +51,7 @@ describe(`LegacyBitstreamUrlResolver`, () => { }); it(`should call findByItemHandle with the handle, sequence id, and filename from the route`, () => { testScheduler.run(() => { - resolver.resolve(route, state); + resolver(route, state, bitstreamDataService); expect(bitstreamDataService.findByItemHandle).toHaveBeenCalledWith( `${route.params.prefix}/${route.params.suffix}`, route.params.sequence_id, @@ -78,7 +78,7 @@ describe(`LegacyBitstreamUrlResolver`, () => { }); it(`should call findByItemHandle with the handle and filename from the route, and the sequence ID from the queryParams`, () => { testScheduler.run(() => { - resolver.resolve(route, state); + resolver(route, state, bitstreamDataService); expect(bitstreamDataService.findByItemHandle).toHaveBeenCalledWith( `${route.params.prefix}/${route.params.suffix}`, route.queryParams.sequenceId, @@ -100,7 +100,7 @@ describe(`LegacyBitstreamUrlResolver`, () => { }); it(`should call findByItemHandle with the handle, and filename from the route`, () => { testScheduler.run(() => { - resolver.resolve(route, state); + resolver(route, state, bitstreamDataService); expect(bitstreamDataService.findByItemHandle).toHaveBeenCalledWith( `${route.params.prefix}/${route.params.suffix}`, undefined, @@ -123,7 +123,7 @@ describe(`LegacyBitstreamUrlResolver`, () => { c: remoteDataMocks.Error, }; - expectObservable(resolver.resolve(route, state)).toBe(expected, values); + expectObservable(resolver(route, state, bitstreamDataService)).toBe(expected, values); }); }); it(`...succeeded`, () => { @@ -138,7 +138,7 @@ describe(`LegacyBitstreamUrlResolver`, () => { c: remoteDataMocks.Success, }; - expectObservable(resolver.resolve(route, state)).toBe(expected, values); + expectObservable(resolver(route, state, bitstreamDataService)).toBe(expected, values); }); }); }); diff --git a/src/app/bitstream-page/legacy-bitstream-url.resolver.ts b/src/app/bitstream-page/legacy-bitstream-url.resolver.ts index de5af179c4..8b9b1127b1 100644 --- a/src/app/bitstream-page/legacy-bitstream-url.resolver.ts +++ b/src/app/bitstream-page/legacy-bitstream-url.resolver.ts @@ -1,6 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; import { Observable } from 'rxjs'; @@ -12,41 +13,34 @@ import { getFirstCompletedRemoteData } from '../core/shared/operators'; import { hasNoValue } from '../shared/empty.util'; /** - * This class resolves a bitstream based on the DSpace 6 XMLUI or JSPUI bitstream download URLs + * Resolve a bitstream based on the handle of the item, and the sequence id or the filename of the + * bitstream + * + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @param {BitstreamDataService} bitstreamDataService + * @returns Observable<> Emits the found bitstream based on the parameters in + * current route, or an error if something went wrong */ -@Injectable({ - providedIn: 'root', -}) -export class LegacyBitstreamUrlResolver { - constructor(protected bitstreamDataService: BitstreamDataService) { +export const legacyBitstreamUrlResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + bitstreamDataService: BitstreamDataService = inject(BitstreamDataService), +): Observable> => { + const prefix = route.params.prefix; + const suffix = route.params.suffix; + const filename = route.params.filename; + + let sequenceId = route.params.sequence_id; + if (hasNoValue(sequenceId)) { + sequenceId = route.queryParams.sequenceId; } - /** - * Resolve a bitstream based on the handle of the item, and the sequence id or the filename of the - * bitstream - * - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - * @param {RouterStateSnapshot} state The current RouterStateSnapshot - * @returns Observable<> Emits the found bitstream based on the parameters in - * current route, or an error if something went wrong - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): - Observable> { - const prefix = route.params.prefix; - const suffix = route.params.suffix; - const filename = route.params.filename; - - let sequenceId = route.params.sequence_id; - if (hasNoValue(sequenceId)) { - sequenceId = route.queryParams.sequenceId; - } - - return this.bitstreamDataService.findByItemHandle( - `${prefix}/${suffix}`, - sequenceId, - filename, - ).pipe( - getFirstCompletedRemoteData(), - ); - } -} + return bitstreamDataService.findByItemHandle( + `${prefix}/${suffix}`, + sequenceId, + filename, + ).pipe( + getFirstCompletedRemoteData(), + ); +}; diff --git a/src/app/browse-by/browse-by-dso-breadcrumb.resolver.ts b/src/app/browse-by/browse-by-dso-breadcrumb.resolver.ts index 7650376955..a5a233dcd6 100644 --- a/src/app/browse-by/browse-by-dso-breadcrumb.resolver.ts +++ b/src/app/browse-by/browse-by-dso-breadcrumb.resolver.ts @@ -1,6 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; import { Observable } from 'rxjs'; @@ -19,30 +20,28 @@ import { import { hasValue } from '../shared/empty.util'; /** - * The class that resolves the BreadcrumbConfig object for a DSpaceObject on a browse by page + * Method for resolving a breadcrumb config object + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @param {DSOBreadcrumbsService} breadcrumbService + * @param {DSpaceObjectDataService} dataService + * @returns BreadcrumbConfig object */ -@Injectable({ providedIn: 'root' }) -export class BrowseByDSOBreadcrumbResolver { - constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: DSpaceObjectDataService) { +export const browseByDSOBreadcrumbResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + breadcrumbService: DSOBreadcrumbsService = inject(DSOBreadcrumbsService), + dataService: DSpaceObjectDataService = inject(DSpaceObjectDataService), +): Observable> => { + const uuid = route.queryParams.scope; + if (hasValue(uuid)) { + return dataService.findById(uuid).pipe( + getFirstSucceededRemoteData(), + getRemoteDataPayload(), + map((object: Community | Collection) => { + return { provider: breadcrumbService, key: object, url: getDSORoute(object) }; + }), + ); } - - /** - * Method for resolving a breadcrumb config object - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - * @param {RouterStateSnapshot} state The current RouterStateSnapshot - * @returns BreadcrumbConfig object - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - const uuid = route.queryParams.scope; - if (hasValue(uuid)) { - return this.dataService.findById(uuid).pipe( - getFirstSucceededRemoteData(), - getRemoteDataPayload(), - map((object: Community | Collection) => { - return { provider: this.breadcrumbService, key: object, url: getDSORoute(object) }; - }), - ); - } - return undefined; - } -} + return undefined; +}; diff --git a/src/app/browse-by/browse-by-guard.spec.ts b/src/app/browse-by/browse-by-guard.spec.ts index 04a0b99651..1037ca4d95 100644 --- a/src/app/browse-by/browse-by-guard.spec.ts +++ b/src/app/browse-by/browse-by-guard.spec.ts @@ -6,12 +6,12 @@ import { createSuccessfulRemoteDataObject$, } from '../shared/remote-data.utils'; import { RouterStub } from '../shared/testing/router.stub'; -import { BrowseByGuard } from './browse-by-guard'; +import { browseByGuard } from './browse-by-guard'; import { BrowseByDataType } from './browse-by-switcher/browse-by-data-type'; -describe('BrowseByGuard', () => { +describe('browseByGuard', () => { describe('canActivate', () => { - let guard: BrowseByGuard; + let guard: any; let translateService: any; let browseDefinitionService: any; let router: any; @@ -35,7 +35,7 @@ describe('BrowseByGuard', () => { router = new RouterStub() as any; - guard = new BrowseByGuard(translateService, browseDefinitionService, router); + guard = browseByGuard; }); it('should return true, and sets up the data correctly, with a scope and value', () => { @@ -53,7 +53,7 @@ describe('BrowseByGuard', () => { value, }, }; - guard.canActivate(scopedRoute as any, undefined) + guard(scopedRoute as any, undefined, browseDefinitionService, router, translateService) .pipe(first()) .subscribe( (canActivate) => { @@ -86,7 +86,7 @@ describe('BrowseByGuard', () => { }, }; - guard.canActivate(scopedNoValueRoute as any, undefined) + guard(scopedNoValueRoute, undefined, browseDefinitionService, router, translateService) .pipe(first()) .subscribe( (canActivate) => { @@ -123,7 +123,7 @@ describe('BrowseByGuard', () => { }, }; - guard.canActivate(scopedNoValueRoute as any, undefined).pipe( + guard(scopedNoValueRoute as any, undefined, browseDefinitionService, router, translateService).pipe( first(), ).subscribe((canActivate) => { const result = { @@ -154,7 +154,8 @@ describe('BrowseByGuard', () => { value, }, }; - guard.canActivate(route as any, undefined) + + guard(route as any, undefined, browseDefinitionService, router, translateService) .pipe(first()) .subscribe( (canActivate) => { @@ -189,7 +190,8 @@ describe('BrowseByGuard', () => { value, }, }; - guard.canActivate(scopedRoute as any, undefined) + + guard(scopedRoute as any, undefined, browseDefinitionService, router, translateService) .pipe(first()) .subscribe((canActivate) => { expect(router.navigate).toHaveBeenCalled(); diff --git a/src/app/browse-by/browse-by-guard.ts b/src/app/browse-by/browse-by-guard.ts index a0975be72e..62fff2fbc6 100644 --- a/src/app/browse-by/browse-by-guard.ts +++ b/src/app/browse-by/browse-by-guard.ts @@ -1,6 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, + CanActivateFn, Data, Router, RouterStateSnapshot, @@ -25,55 +26,47 @@ import { hasValue, } from '../shared/empty.util'; -@Injectable({ providedIn: 'root' }) -/** - * A guard taking care of the correct route.data being set for the Browse-By components - */ -export class BrowseByGuard { - - constructor( - protected translate: TranslateService, - protected browseDefinitionService: BrowseDefinitionDataService, - protected router: Router, - ) { - } - - canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - const title = route.data.title; - const id = route.params.id || route.queryParams.id || route.data.id; - let browseDefinition$: Observable; - if (hasNoValue(route.data.browseDefinition) && hasValue(id)) { - browseDefinition$ = this.browseDefinitionService.findById(id).pipe( - getFirstCompletedRemoteData(), - map((browseDefinitionRD: RemoteData) => browseDefinitionRD.payload), - ); - } else { - browseDefinition$ = observableOf(route.data.browseDefinition); - } - const scope = route.queryParams.scope ?? route.parent?.params.id; - const value = route.queryParams.value; - const metadataTranslated = this.translate.instant(`browse.metadata.${id}`); - return browseDefinition$.pipe( - switchMap((browseDefinition: BrowseDefinition | undefined) => { - if (hasValue(browseDefinition)) { - route.data = this.createData(title, id, browseDefinition, metadataTranslated, value, route, scope); - return observableOf(true); - } else { - void this.router.navigate([PAGE_NOT_FOUND_PATH]); - return observableOf(false); - } - }), +export const browseByGuard: CanActivateFn = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + browseDefinitionService: BrowseDefinitionDataService = inject(BrowseDefinitionDataService), + router: Router = inject(Router), + translate: TranslateService = inject(TranslateService), +): Observable => { + const title = route.data.title; + const id = route.params.id || route.queryParams.id || route.data.id; + let browseDefinition$: Observable; + if (hasNoValue(route.data.browseDefinition) && hasValue(id)) { + browseDefinition$ = browseDefinitionService.findById(id).pipe( + getFirstCompletedRemoteData(), + map((browseDefinitionRD: RemoteData) => browseDefinitionRD.payload), ); + } else { + browseDefinition$ = observableOf(route.data.browseDefinition); } + const scope = route.queryParams.scope ?? route.parent?.params.id; + const value = route.queryParams.value; + const metadataTranslated = translate.instant(`browse.metadata.${id}`); + return browseDefinition$.pipe( + switchMap((browseDefinition: BrowseDefinition | undefined) => { + if (hasValue(browseDefinition)) { + route.data = createData(title, id, browseDefinition, metadataTranslated, value, route, scope); + return observableOf(true); + } else { + void router.navigate([PAGE_NOT_FOUND_PATH]); + return observableOf(false); + } + }), + ); +}; - private createData(title: string, id: string, browseDefinition: BrowseDefinition, field: string, value: string, route: ActivatedRouteSnapshot, scope: string): Data { - return Object.assign({}, route.data, { - title: title, - id: id, - browseDefinition: browseDefinition, - field: field, - value: hasValue(value) ? `"${value}"` : '', - scope: scope, - }); - } +function createData(title: string, id: string, browseDefinition: BrowseDefinition, field: string, value: string, route: ActivatedRouteSnapshot, scope: string): Data { + return Object.assign({}, route.data, { + title: title, + id: id, + browseDefinition: browseDefinition, + field: field, + value: hasValue(value) ? `"${value}"` : '', + scope: scope, + }); } diff --git a/src/app/browse-by/browse-by-i18n-breadcrumb.resolver.ts b/src/app/browse-by/browse-by-i18n-breadcrumb.resolver.ts index 3107f9aad5..64cf6df6db 100644 --- a/src/app/browse-by/browse-by-i18n-breadcrumb.resolver.ts +++ b/src/app/browse-by/browse-by-i18n-breadcrumb.resolver.ts @@ -1,32 +1,23 @@ -import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; import { BreadcrumbConfig } from '../breadcrumbs/breadcrumb/breadcrumb-config.model'; -import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; -import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service'; +import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; /** - * This class resolves a BreadcrumbConfig object with an i18n key string for a route - * It adds the metadata field of the current browse-by page + * Method for resolving a browse-by i18n breadcrumb configuration object + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @returns BreadcrumbConfig object for a browse-by page */ -@Injectable({ providedIn: 'root' }) -export class BrowseByI18nBreadcrumbResolver extends I18nBreadcrumbResolver { - constructor(protected breadcrumbService: I18nBreadcrumbsService) { - super(breadcrumbService); - } - - /** - * Method for resolving a browse-by i18n breadcrumb configuration object - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - * @param {RouterStateSnapshot} state The current RouterStateSnapshot - * @returns BreadcrumbConfig object for a browse-by page - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): BreadcrumbConfig { - const extendedBreadcrumbKey = route.data.breadcrumbKey + '.' + route.params.id; - route.data = Object.assign({}, route.data, { breadcrumbKey: extendedBreadcrumbKey }); - return super.resolve(route, state); - } -} +export const browseByI18nBreadcrumbResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, +): BreadcrumbConfig => { + const extendedBreadcrumbKey = route.data.breadcrumbKey + '.' + route.params.id; + route.data = Object.assign({}, route.data, { breadcrumbKey: extendedBreadcrumbKey }); + return i18nBreadcrumbResolver(route, state) as BreadcrumbConfig; +}; diff --git a/src/app/browse-by/browse-by-page-routes.ts b/src/app/browse-by/browse-by-page-routes.ts index 9e4c9cc786..9c7e16ab39 100644 --- a/src/app/browse-by/browse-by-page-routes.ts +++ b/src/app/browse-by/browse-by-page-routes.ts @@ -1,24 +1,24 @@ import { Route } from '@angular/router'; -import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver'; -import { BrowseByDSOBreadcrumbResolver } from './browse-by-dso-breadcrumb.resolver'; -import { BrowseByGuard } from './browse-by-guard'; -import { BrowseByI18nBreadcrumbResolver } from './browse-by-i18n-breadcrumb.resolver'; +import { dsoEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver'; +import { browseByDSOBreadcrumbResolver } from './browse-by-dso-breadcrumb.resolver'; +import { browseByGuard } from './browse-by-guard'; +import { browseByI18nBreadcrumbResolver } from './browse-by-i18n-breadcrumb.resolver'; import { BrowseByPageComponent } from './browse-by-page/browse-by-page.component'; export const ROUTES: Route[] = [ { path: '', resolve: { - breadcrumb: BrowseByDSOBreadcrumbResolver, - menu: DSOEditMenuResolver, + breadcrumb: browseByDSOBreadcrumbResolver, + menu: dsoEditMenuResolver, }, children: [ { path: ':id', component: BrowseByPageComponent, - canActivate: [BrowseByGuard], - resolve: { breadcrumb: BrowseByI18nBreadcrumbResolver }, + canActivate: [browseByGuard], + resolve: { breadcrumb: browseByI18nBreadcrumbResolver }, data: { title: 'browse.title.page', breadcrumbKey: 'browse.metadata' }, }, ], diff --git a/src/app/collection-page/collection-page-administrator.guard.ts b/src/app/collection-page/collection-page-administrator.guard.ts index caecd6ef43..63e969b7c8 100644 --- a/src/app/collection-page/collection-page-administrator.guard.ts +++ b/src/app/collection-page/collection-page-administrator.guard.ts @@ -1,6 +1,7 @@ import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, Router, RouterStateSnapshot, } from '@angular/router'; @@ -13,8 +14,9 @@ import { AuthService } from '../core/auth/auth.service'; import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service'; import { DsoPageSingleFeatureGuard } from '../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard'; import { FeatureID } from '../core/data/feature-authorization/feature-id'; +import { RemoteData } from '../core/data/remote-data'; import { Collection } from '../core/shared/collection.model'; -import { CollectionPageResolver } from './collection-page.resolver'; +import { collectionPageResolver } from './collection-page.resolver'; @Injectable({ providedIn: 'root', @@ -23,11 +25,13 @@ import { CollectionPageResolver } from './collection-page.resolver'; * Guard for preventing unauthorized access to certain {@link Collection} pages requiring administrator rights */ export class CollectionPageAdministratorGuard extends DsoPageSingleFeatureGuard { - constructor(protected resolver: CollectionPageResolver, - protected authorizationService: AuthorizationDataService, + + protected resolver: ResolveFn> = collectionPageResolver; + + constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) { - super(resolver, authorizationService, router, authService); + super(authorizationService, router, authService); } /** diff --git a/src/app/collection-page/collection-page-routes.ts b/src/app/collection-page/collection-page-routes.ts index 0d8cfe85d6..91e9d59992 100644 --- a/src/app/collection-page/collection-page-routes.ts +++ b/src/app/collection-page/collection-page-routes.ts @@ -1,16 +1,19 @@ -import { Route } from '@angular/router'; +import { + mapToCanActivate, + Route, +} from '@angular/router'; -import { BrowseByGuard } from '../browse-by/browse-by-guard'; -import { BrowseByI18nBreadcrumbResolver } from '../browse-by/browse-by-i18n-breadcrumb.resolver'; -import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; -import { CollectionBreadcrumbResolver } from '../core/breadcrumbs/collection-breadcrumb.resolver'; -import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; +import { browseByGuard } from '../browse-by/browse-by-guard'; +import { browseByI18nBreadcrumbResolver } from '../browse-by/browse-by-i18n-breadcrumb.resolver'; +import { authenticatedGuard } from '../core/auth/authenticated.guard'; +import { collectionBreadcrumbResolver } from '../core/breadcrumbs/collection-breadcrumb.resolver'; +import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; import { ComcolBrowseByComponent } from '../shared/comcol/sections/comcol-browse-by/comcol-browse-by.component'; import { ComcolSearchSectionComponent } from '../shared/comcol/sections/comcol-search-section/comcol-search-section.component'; -import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver'; +import { dsoEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver'; import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model'; import { MenuItemType } from '../shared/menu/menu-item-type.model'; -import { CollectionPageResolver } from './collection-page.resolver'; +import { collectionPageResolver } from './collection-page.resolver'; import { CollectionPageAdministratorGuard } from './collection-page-administrator.guard'; import { COLLECTION_CREATE_PATH, @@ -18,9 +21,9 @@ import { ITEMTEMPLATE_PATH, } from './collection-page-routing-paths'; import { CreateCollectionPageComponent } from './create-collection-page/create-collection-page.component'; -import { CreateCollectionPageGuard } from './create-collection-page/create-collection-page.guard'; +import { createCollectionPageGuard } from './create-collection-page/create-collection-page.guard'; import { DeleteCollectionPageComponent } from './delete-collection-page/delete-collection-page.component'; -import { ItemTemplatePageResolver } from './edit-item-template-page/item-template-page.resolver'; +import { itemTemplatePageResolver } from './edit-item-template-page/item-template-page.resolver'; import { ThemedEditItemTemplatePageComponent } from './edit-item-template-page/themed-edit-item-template-page.component'; import { ThemedCollectionPageComponent } from './themed-collection-page.component'; @@ -29,14 +32,14 @@ export const ROUTES: Route[] = [ { path: COLLECTION_CREATE_PATH, component: CreateCollectionPageComponent, - canActivate: [AuthenticatedGuard, CreateCollectionPageGuard], + canActivate: [authenticatedGuard, createCollectionPageGuard], }, { path: ':id', resolve: { - dso: CollectionPageResolver, - breadcrumb: CollectionBreadcrumbResolver, - menu: DSOEditMenuResolver, + dso: collectionPageResolver, + breadcrumb: collectionBreadcrumbResolver, + menu: dsoEditMenuResolver, }, runGuardsAndResolvers: 'always', children: [ @@ -44,21 +47,21 @@ export const ROUTES: Route[] = [ path: COLLECTION_EDIT_PATH, loadChildren: () => import('./edit-collection-page/edit-collection-page-routes') .then((m) => m.ROUTES), - canActivate: [CollectionPageAdministratorGuard], + canActivate: mapToCanActivate([CollectionPageAdministratorGuard]), }, { path: 'delete', pathMatch: 'full', component: DeleteCollectionPageComponent, - canActivate: [AuthenticatedGuard], + canActivate: [authenticatedGuard], }, { path: ITEMTEMPLATE_PATH, component: ThemedEditItemTemplatePageComponent, - canActivate: [AuthenticatedGuard], + canActivate: [authenticatedGuard], resolve: { - item: ItemTemplatePageResolver, - breadcrumb: I18nBreadcrumbResolver, + item: itemTemplatePageResolver, + breadcrumb: i18nBreadcrumbResolver, }, data: { title: 'collection.edit.template.title', breadcrumbKey: 'collection.edit.template' }, }, @@ -75,9 +78,9 @@ export const ROUTES: Route[] = [ path: 'browse/:id', pathMatch: 'full', component: ComcolBrowseByComponent, - canActivate: [BrowseByGuard], + canActivate: [browseByGuard], resolve: { - breadcrumb: BrowseByI18nBreadcrumbResolver, + breadcrumb: browseByI18nBreadcrumbResolver, }, data: { breadcrumbKey: 'browse.metadata' }, }, diff --git a/src/app/collection-page/collection-page.resolver.spec.ts b/src/app/collection-page/collection-page.resolver.spec.ts index 32d8952ff7..2c1e466729 100644 --- a/src/app/collection-page/collection-page.resolver.spec.ts +++ b/src/app/collection-page/collection-page.resolver.spec.ts @@ -1,11 +1,12 @@ +import { Observable } from 'rxjs'; import { first } from 'rxjs/operators'; import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; -import { CollectionPageResolver } from './collection-page.resolver'; +import { collectionPageResolver } from './collection-page.resolver'; -describe('CollectionPageResolver', () => { +describe('collectionPageResolver', () => { describe('resolve', () => { - let resolver: CollectionPageResolver; + let resolver: any; let collectionService: any; let store: any; const uuid = '1234-65487-12354-1235'; @@ -17,12 +18,11 @@ describe('CollectionPageResolver', () => { store = jasmine.createSpyObj('store', { dispatch: {}, }); - resolver = new CollectionPageResolver(collectionService, store); + resolver = collectionPageResolver; }); it('should resolve a collection with the correct id', (done) => { - resolver.resolve({ params: { id: uuid } } as any, { url: 'current-url' } as any) - .pipe(first()) + (resolver({ params: { id: uuid } } as any, { url: 'current-url' } as any, collectionService, store) as Observable).pipe(first()) .subscribe( (resolved) => { expect(resolved.payload.id).toEqual(uuid); diff --git a/src/app/collection-page/collection-page.resolver.ts b/src/app/collection-page/collection-page.resolver.ts index 56d9e91fc1..6befefc152 100644 --- a/src/app/collection-page/collection-page.resolver.ts +++ b/src/app/collection-page/collection-page.resolver.ts @@ -1,11 +1,13 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; +import { AppState } from '../app.reducer'; import { CollectionDataService } from '../core/data/collection-data.service'; import { RemoteData } from '../core/data/remote-data'; import { ResolvedAction } from '../core/resolving/resolver.actions'; @@ -28,37 +30,32 @@ export const COLLECTION_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig[] = [ ]; /** - * This class represents a resolver that requests a specific collection before the route is activated + * Method for resolving a collection based on the parameters in the current route + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @param collectionService + * @param store + * @returns Observable<> Emits the found collection based on the parameters in the current route, + * or an error if something went wrong */ -@Injectable({ providedIn: 'root' }) -export class CollectionPageResolver { - constructor( - private collectionService: CollectionDataService, - private store: Store, - ) { - } +export const collectionPageResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + collectionService: CollectionDataService = inject(CollectionDataService), + store: Store = inject(Store), +): Observable> => { + const collectionRD$ = collectionService.findById( + route.params.id, + true, + false, + ...COLLECTION_PAGE_LINKS_TO_FOLLOW, + ).pipe( + getFirstCompletedRemoteData(), + ); - /** - * Method for resolving a collection based on the parameters in the current route - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - * @param {RouterStateSnapshot} state The current RouterStateSnapshot - * @returns Observable<> Emits the found collection based on the parameters in the current route, - * or an error if something went wrong - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - const collectionRD$ = this.collectionService.findById( - route.params.id, - true, - false, - ...COLLECTION_PAGE_LINKS_TO_FOLLOW, - ).pipe( - getFirstCompletedRemoteData(), - ); + collectionRD$.subscribe((collectionRD: RemoteData) => { + store.dispatch(new ResolvedAction(state.url, collectionRD.payload)); + }); - collectionRD$.subscribe((collectionRD: RemoteData) => { - this.store.dispatch(new ResolvedAction(state.url, collectionRD.payload)); - }); - - return collectionRD$; - } -} + return collectionRD$; +}; diff --git a/src/app/collection-page/create-collection-page/create-collection-page.guard.spec.ts b/src/app/collection-page/create-collection-page/create-collection-page.guard.spec.ts index b476f38889..b7c362118b 100644 --- a/src/app/collection-page/create-collection-page/create-collection-page.guard.spec.ts +++ b/src/app/collection-page/create-collection-page/create-collection-page.guard.spec.ts @@ -6,11 +6,11 @@ import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$, } from '../../shared/remote-data.utils'; -import { CreateCollectionPageGuard } from './create-collection-page.guard'; +import { createCollectionPageGuard } from './create-collection-page.guard'; -describe('CreateCollectionPageGuard', () => { +describe('createCollectionPageGuard', () => { describe('canActivate', () => { - let guard: CreateCollectionPageGuard; + let guard: any; let router; let communityDataServiceStub: any; @@ -28,11 +28,11 @@ describe('CreateCollectionPageGuard', () => { }; router = new RouterMock(); - guard = new CreateCollectionPageGuard(router, communityDataServiceStub); + guard = createCollectionPageGuard; }); it('should return true when the parent ID resolves to a community', () => { - guard.canActivate({ queryParams: { parent: 'valid-id' } } as any, undefined) + guard({ queryParams: { parent: 'valid-id' } } as any, undefined, communityDataServiceStub, router) .pipe(first()) .subscribe( (canActivate) => @@ -41,7 +41,7 @@ describe('CreateCollectionPageGuard', () => { }); it('should return false when no parent ID has been provided', () => { - guard.canActivate({ queryParams: { } } as any, undefined) + guard({ queryParams: { } } as any, undefined, communityDataServiceStub, router) .pipe(first()) .subscribe( (canActivate) => @@ -50,7 +50,7 @@ describe('CreateCollectionPageGuard', () => { }); it('should return false when the parent ID does not resolve to a community', () => { - guard.canActivate({ queryParams: { parent: 'invalid-id' } } as any, undefined) + guard({ queryParams: { parent: 'invalid-id' } } as any, undefined, communityDataServiceStub, router) .pipe(first()) .subscribe( (canActivate) => @@ -59,7 +59,7 @@ describe('CreateCollectionPageGuard', () => { }); it('should return false when the parent ID resolves to an error response', () => { - guard.canActivate({ queryParams: { parent: 'error-id' } } as any, undefined) + guard({ queryParams: { parent: 'error-id' } } as any, undefined, communityDataServiceStub, router) .pipe(first()) .subscribe( (canActivate) => diff --git a/src/app/collection-page/create-collection-page/create-collection-page.guard.ts b/src/app/collection-page/create-collection-page/create-collection-page.guard.ts index 0734fa6f2f..099578c550 100644 --- a/src/app/collection-page/create-collection-page/create-collection-page.guard.ts +++ b/src/app/collection-page/create-collection-page/create-collection-page.guard.ts @@ -1,7 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, - CanActivate, + CanActivateFn, Router, RouterStateSnapshot, } from '@angular/router'; @@ -24,34 +24,29 @@ import { } from '../../shared/empty.util'; /** - * Prevent creation of a collection without a parent community provided - * @class CreateCollectionPageGuard + * True when either a parent ID query parameter has been provided and the parent ID resolves to a valid parent community + * Reroutes to a 404 page when the page cannot be activated */ -@Injectable({ providedIn: 'root' }) -export class CreateCollectionPageGuard implements CanActivate { - public constructor(private router: Router, private communityService: CommunityDataService) { +export const createCollectionPageGuard: CanActivateFn = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + communityService: CommunityDataService = inject(CommunityDataService), + router: Router = inject(Router), +): Observable => { + const parentID = route.queryParams.parent; + if (hasNoValue(parentID)) { + router.navigate(['/404']); + return observableOf(false); } + return communityService.findById(parentID) + .pipe( + getFirstCompletedRemoteData(), + map((communityRD: RemoteData) => hasValue(communityRD) && communityRD.hasSucceeded && hasValue(communityRD.payload)), + tap((isValid: boolean) => { + if (!isValid) { + router.navigate(['/404']); + } + }), + ); +}; - /** - * True when either a parent ID query parameter has been provided and the parent ID resolves to a valid parent community - * Reroutes to a 404 page when the page cannot be activated - * @method canActivate - */ - canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - const parentID = route.queryParams.parent; - if (hasNoValue(parentID)) { - this.router.navigate(['/404']); - return observableOf(false); - } - return this.communityService.findById(parentID) - .pipe( - getFirstCompletedRemoteData(), - map((communityRD: RemoteData) => hasValue(communityRD) && communityRD.hasSucceeded && hasValue(communityRD.payload)), - tap((isValid: boolean) => { - if (!isValid) { - this.router.navigate(['/404']); - } - }), - ); - } -} diff --git a/src/app/collection-page/edit-collection-page/edit-collection-page-routes.ts b/src/app/collection-page/edit-collection-page/edit-collection-page-routes.ts index ed46d88616..cf550c3223 100644 --- a/src/app/collection-page/edit-collection-page/edit-collection-page-routes.ts +++ b/src/app/collection-page/edit-collection-page/edit-collection-page-routes.ts @@ -1,11 +1,14 @@ -import { Route } from '@angular/router'; +import { + mapToCanActivate, + Route, +} from '@angular/router'; -import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver'; +import { i18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver'; import { CollectionAdministratorGuard } from '../../core/data/feature-authorization/feature-authorization-guard/collection-administrator.guard'; import { ResourcePolicyCreateComponent } from '../../shared/resource-policies/create/resource-policy-create.component'; import { ResourcePolicyEditComponent } from '../../shared/resource-policies/edit/resource-policy-edit.component'; -import { ResourcePolicyResolver } from '../../shared/resource-policies/resolvers/resource-policy.resolver'; -import { ResourcePolicyTargetResolver } from '../../shared/resource-policies/resolvers/resource-policy-target.resolver'; +import { resourcePolicyResolver } from '../../shared/resource-policies/resolvers/resource-policy.resolver'; +import { resourcePolicyTargetResolver } from '../../shared/resource-policies/resolvers/resource-policy-target.resolver'; import { CollectionItemMapperComponent } from '../collection-item-mapper/collection-item-mapper.component'; import { CollectionAccessControlComponent } from './collection-access-control/collection-access-control.component'; import { CollectionAuthorizationsComponent } from './collection-authorizations/collection-authorizations.component'; @@ -23,11 +26,11 @@ export const ROUTES: Route[] = [ { path: '', resolve: { - breadcrumb: I18nBreadcrumbResolver, + breadcrumb: i18nBreadcrumbResolver, }, data: { breadcrumbKey: 'collection.edit' }, component: EditCollectionPageComponent, - canActivate: [CollectionAdministratorGuard], + canActivate: mapToCanActivate([CollectionAdministratorGuard]), children: [ { path: '', @@ -70,7 +73,7 @@ export const ROUTES: Route[] = [ { path: 'create', resolve: { - resourcePolicyTarget: ResourcePolicyTargetResolver, + resourcePolicyTarget: resourcePolicyTargetResolver, }, component: ResourcePolicyCreateComponent, data: { title: 'resource-policies.create.page.title' }, @@ -78,7 +81,7 @@ export const ROUTES: Route[] = [ { path: 'edit', resolve: { - resourcePolicy: ResourcePolicyResolver, + resourcePolicy: resourcePolicyResolver, }, component: ResourcePolicyEditComponent, data: { title: 'resource-policies.edit.page.title' }, diff --git a/src/app/collection-page/edit-item-template-page/item-template-page.resolver.spec.ts b/src/app/collection-page/edit-item-template-page/item-template-page.resolver.spec.ts index ced1ca9323..c622c62267 100644 --- a/src/app/collection-page/edit-item-template-page/item-template-page.resolver.spec.ts +++ b/src/app/collection-page/edit-item-template-page/item-template-page.resolver.spec.ts @@ -1,27 +1,24 @@ +import { Observable } from 'rxjs'; import { first } from 'rxjs/operators'; -import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; -import { DSONameServiceMock } from '../../shared/mocks/dso-name.service.mock'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; -import { ItemTemplatePageResolver } from './item-template-page.resolver'; +import { itemTemplatePageResolver } from './item-template-page.resolver'; -describe('ItemTemplatePageResolver', () => { +describe('itemTemplatePageResolver', () => { describe('resolve', () => { - let resolver: ItemTemplatePageResolver; + let resolver: any; let itemTemplateService: any; - let dsoNameService: DSONameServiceMock; const uuid = '1234-65487-12354-1235'; beforeEach(() => { itemTemplateService = { findByCollectionID: (id: string) => createSuccessfulRemoteDataObject$({ id }), }; - dsoNameService = new DSONameServiceMock(); - resolver = new ItemTemplatePageResolver(dsoNameService as DSONameService, itemTemplateService); + resolver = itemTemplatePageResolver; }); it('should resolve an item template with the correct id', (done) => { - resolver.resolve({ params: { id: uuid } } as any, undefined) + (resolver({ params: { id: uuid } } as any, undefined, itemTemplateService) as Observable) .pipe(first()) .subscribe( (resolved) => { diff --git a/src/app/collection-page/edit-item-template-page/item-template-page.resolver.ts b/src/app/collection-page/edit-item-template-page/item-template-page.resolver.ts index 05f0f050d6..d35cd0a3b0 100644 --- a/src/app/collection-page/edit-item-template-page/item-template-page.resolver.ts +++ b/src/app/collection-page/edit-item-template-page/item-template-page.resolver.ts @@ -1,38 +1,23 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; import { Observable } from 'rxjs'; -import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { ItemTemplateDataService } from '../../core/data/item-template-data.service'; import { RemoteData } from '../../core/data/remote-data'; import { Item } from '../../core/shared/item.model'; import { getFirstCompletedRemoteData } from '../../core/shared/operators'; import { followLink } from '../../shared/utils/follow-link-config.model'; -/** - * This class represents a resolver that requests a specific collection's item template before the route is activated - */ -@Injectable({ providedIn: 'root' }) -export class ItemTemplatePageResolver { - constructor( - public dsoNameService: DSONameService, - private itemTemplateService: ItemTemplateDataService, - ) { - } - - /** - * Method for resolving a collection's item template based on the parameters in the current route - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - * @param {RouterStateSnapshot} state The current RouterStateSnapshot - * @returns Observable<> Emits the found item template based on the parameters in the current route, - * or an error if something went wrong - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - return this.itemTemplateService.findByCollectionID(route.params.id, true, false, followLink('templateItemOf')).pipe( - getFirstCompletedRemoteData(), - ); - } -} +export const itemTemplatePageResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + itemTemplateService: ItemTemplateDataService = inject(ItemTemplateDataService), +): Observable> => { + return itemTemplateService.findByCollectionID(route.params.id, true, false, followLink('templateItemOf')).pipe( + getFirstCompletedRemoteData(), + ); +}; diff --git a/src/app/community-list-page/community-list-page-routes.ts b/src/app/community-list-page/community-list-page-routes.ts index e1f9f9ff3b..9990efb437 100644 --- a/src/app/community-list-page/community-list-page-routes.ts +++ b/src/app/community-list-page/community-list-page-routes.ts @@ -1,6 +1,6 @@ import { Route } from '@angular/router'; -import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; +import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; import { ThemedCommunityListPageComponent } from './themed-community-list-page.component'; /** @@ -12,7 +12,7 @@ export const ROUTES: Route[] = [ component: ThemedCommunityListPageComponent, pathMatch: 'full', resolve: { - breadcrumb: I18nBreadcrumbResolver, + breadcrumb: i18nBreadcrumbResolver, }, data: { title: 'communityList.tabTitle', breadcrumbKey: 'communityList' }, }, diff --git a/src/app/community-page/community-page-administrator.guard.ts b/src/app/community-page/community-page-administrator.guard.ts index 4a3816b225..23d429b484 100644 --- a/src/app/community-page/community-page-administrator.guard.ts +++ b/src/app/community-page/community-page-administrator.guard.ts @@ -1,6 +1,7 @@ import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, Router, RouterStateSnapshot, } from '@angular/router'; @@ -13,8 +14,9 @@ import { AuthService } from '../core/auth/auth.service'; import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service'; import { DsoPageSingleFeatureGuard } from '../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard'; import { FeatureID } from '../core/data/feature-authorization/feature-id'; +import { RemoteData } from '../core/data/remote-data'; import { Community } from '../core/shared/community.model'; -import { CommunityPageResolver } from './community-page.resolver'; +import { communityPageResolver } from './community-page.resolver'; @Injectable({ providedIn: 'root', @@ -23,11 +25,13 @@ import { CommunityPageResolver } from './community-page.resolver'; * Guard for preventing unauthorized access to certain {@link Community} pages requiring administrator rights */ export class CommunityPageAdministratorGuard extends DsoPageSingleFeatureGuard { - constructor(protected resolver: CommunityPageResolver, - protected authorizationService: AuthorizationDataService, + + protected resolver: ResolveFn> = communityPageResolver; + + constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) { - super(resolver, authorizationService, router, authService); + super(authorizationService, router, authService); } /** diff --git a/src/app/community-page/community-page-routes.ts b/src/app/community-page/community-page-routes.ts index 9e4a35e1da..995af8274f 100644 --- a/src/app/community-page/community-page-routes.ts +++ b/src/app/community-page/community-page-routes.ts @@ -1,23 +1,26 @@ -import { Route } from '@angular/router'; +import { + mapToCanActivate, + Route, +} from '@angular/router'; -import { BrowseByGuard } from '../browse-by/browse-by-guard'; -import { BrowseByI18nBreadcrumbResolver } from '../browse-by/browse-by-i18n-breadcrumb.resolver'; -import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; -import { CommunityBreadcrumbResolver } from '../core/breadcrumbs/community-breadcrumb.resolver'; -import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; +import { browseByGuard } from '../browse-by/browse-by-guard'; +import { browseByI18nBreadcrumbResolver } from '../browse-by/browse-by-i18n-breadcrumb.resolver'; +import { authenticatedGuard } from '../core/auth/authenticated.guard'; +import { communityBreadcrumbResolver } from '../core/breadcrumbs/community-breadcrumb.resolver'; +import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; import { ComcolBrowseByComponent } from '../shared/comcol/sections/comcol-browse-by/comcol-browse-by.component'; import { ComcolSearchSectionComponent } from '../shared/comcol/sections/comcol-search-section/comcol-search-section.component'; -import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver'; +import { dsoEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver'; import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model'; import { MenuItemType } from '../shared/menu/menu-item-type.model'; -import { CommunityPageResolver } from './community-page.resolver'; +import { communityPageResolver } from './community-page.resolver'; import { CommunityPageAdministratorGuard } from './community-page-administrator.guard'; import { COMMUNITY_CREATE_PATH, COMMUNITY_EDIT_PATH, } from './community-page-routing-paths'; import { CreateCommunityPageComponent } from './create-community-page/create-community-page.component'; -import { CreateCommunityPageGuard } from './create-community-page/create-community-page.guard'; +import { createCommunityPageGuard } from './create-community-page/create-community-page.guard'; import { DeleteCommunityPageComponent } from './delete-community-page/delete-community-page.component'; import { SubComColSectionComponent } from './sections/sub-com-col-section/sub-com-col-section.component'; import { ThemedCommunityPageComponent } from './themed-community-page.component'; @@ -26,14 +29,14 @@ export const ROUTES: Route[] = [ { path: COMMUNITY_CREATE_PATH, component: CreateCommunityPageComponent, - canActivate: [AuthenticatedGuard, CreateCommunityPageGuard], + canActivate: [authenticatedGuard, createCommunityPageGuard], }, { path: ':id', resolve: { - dso: CommunityPageResolver, - breadcrumb: CommunityBreadcrumbResolver, - menu: DSOEditMenuResolver, + dso: communityPageResolver, + breadcrumb: communityBreadcrumbResolver, + menu: dsoEditMenuResolver, }, runGuardsAndResolvers: 'always', children: [ @@ -41,13 +44,13 @@ export const ROUTES: Route[] = [ path: COMMUNITY_EDIT_PATH, loadChildren: () => import('./edit-community-page/edit-community-page-routes') .then((m) => m.ROUTES), - canActivate: [CommunityPageAdministratorGuard], + canActivate: mapToCanActivate([CommunityPageAdministratorGuard]), }, { path: 'delete', pathMatch: 'full', component: DeleteCommunityPageComponent, - canActivate: [AuthenticatedGuard], + canActivate: [authenticatedGuard], }, { path: '', @@ -63,7 +66,7 @@ export const ROUTES: Route[] = [ pathMatch: 'full', component: SubComColSectionComponent, resolve: { - breadcrumb: I18nBreadcrumbResolver, + breadcrumb: i18nBreadcrumbResolver, }, data: { breadcrumbKey: 'community.subcoms-cols' }, }, @@ -71,9 +74,9 @@ export const ROUTES: Route[] = [ path: 'browse/:id', pathMatch: 'full', component: ComcolBrowseByComponent, - canActivate: [BrowseByGuard], + canActivate: [browseByGuard], resolve: { - breadcrumb: BrowseByI18nBreadcrumbResolver, + breadcrumb: browseByI18nBreadcrumbResolver, }, data: { breadcrumbKey: 'browse.metadata' }, }, diff --git a/src/app/community-page/community-page.resolver.spec.ts b/src/app/community-page/community-page.resolver.spec.ts index 70bb0075e3..e429ecf17c 100644 --- a/src/app/community-page/community-page.resolver.spec.ts +++ b/src/app/community-page/community-page.resolver.spec.ts @@ -1,11 +1,12 @@ +import { Observable } from 'rxjs'; import { first } from 'rxjs/operators'; import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; -import { CommunityPageResolver } from './community-page.resolver'; +import { communityPageResolver } from './community-page.resolver'; -describe('CommunityPageResolver', () => { +describe('communityPageResolver', () => { describe('resolve', () => { - let resolver: CommunityPageResolver; + let resolver: any; let communityService: any; let store: any; const uuid = '1234-65487-12354-1235'; @@ -17,11 +18,11 @@ describe('CommunityPageResolver', () => { store = jasmine.createSpyObj('store', { dispatch: {}, }); - resolver = new CommunityPageResolver(communityService, store); + resolver = communityPageResolver; }); it('should resolve a community with the correct id', (done) => { - resolver.resolve({ params: { id: uuid } } as any, { url: 'current-url' } as any) + (resolver({ params: { id: uuid } } as any, { url: 'current-url' } as any, communityService, store) as Observable) .pipe(first()) .subscribe( (resolved) => { diff --git a/src/app/community-page/community-page.resolver.ts b/src/app/community-page/community-page.resolver.ts index b505064786..b8820629e7 100644 --- a/src/app/community-page/community-page.resolver.ts +++ b/src/app/community-page/community-page.resolver.ts @@ -1,11 +1,13 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; +import { AppState } from '../app.reducer'; import { CommunityDataService } from '../core/data/community-data.service'; import { RemoteData } from '../core/data/remote-data'; import { ResolvedAction } from '../core/resolving/resolver.actions'; @@ -28,37 +30,32 @@ export const COMMUNITY_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig[] = [ ]; /** - * This class represents a resolver that requests a specific community before the route is activated + * Method for resolving a community based on the parameters in the current route + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @param {CommunityDataService} communityService + * @param {Store} store + * @returns Observable<> Emits the found community based on the parameters in the current route, + * or an error if something went wrong */ -@Injectable({ providedIn: 'root' }) -export class CommunityPageResolver { - constructor( - private communityService: CommunityDataService, - private store: Store, - ) { - } +export const communityPageResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + communityService: CommunityDataService = inject(CommunityDataService), + store: Store = inject(Store), +): Observable> => { + const communityRD$ = communityService.findById( + route.params.id, + true, + false, + ...COMMUNITY_PAGE_LINKS_TO_FOLLOW, + ).pipe( + getFirstCompletedRemoteData(), + ); - /** - * Method for resolving a community based on the parameters in the current route - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - * @param {RouterStateSnapshot} state The current RouterStateSnapshot - * @returns Observable<> Emits the found community based on the parameters in the current route, - * or an error if something went wrong - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - const communityRD$ = this.communityService.findById( - route.params.id, - true, - false, - ...COMMUNITY_PAGE_LINKS_TO_FOLLOW, - ).pipe( - getFirstCompletedRemoteData(), - ); + communityRD$.subscribe((communityRD: RemoteData) => { + store.dispatch(new ResolvedAction(state.url, communityRD.payload)); + }); - communityRD$.subscribe((communityRD: RemoteData) => { - this.store.dispatch(new ResolvedAction(state.url, communityRD.payload)); - }); - - return communityRD$; - } -} + return communityRD$; +}; diff --git a/src/app/community-page/create-community-page/create-community-page.guard.spec.ts b/src/app/community-page/create-community-page/create-community-page.guard.spec.ts index 8f1f3fb18a..363db42fa2 100644 --- a/src/app/community-page/create-community-page/create-community-page.guard.spec.ts +++ b/src/app/community-page/create-community-page/create-community-page.guard.spec.ts @@ -6,11 +6,11 @@ import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$, } from '../../shared/remote-data.utils'; -import { CreateCommunityPageGuard } from './create-community-page.guard'; +import { createCommunityPageGuard } from './create-community-page.guard'; -describe('CreateCommunityPageGuard', () => { +describe('createCommunityPageGuard', () => { describe('canActivate', () => { - let guard: CreateCommunityPageGuard; + let guard: any; let router; let communityDataServiceStub: any; @@ -28,11 +28,11 @@ describe('CreateCommunityPageGuard', () => { }; router = new RouterMock(); - guard = new CreateCommunityPageGuard(router, communityDataServiceStub); + guard = createCommunityPageGuard; }); it('should return true when the parent ID resolves to a community', () => { - guard.canActivate({ queryParams: { parent: 'valid-id' } } as any, undefined) + guard({ queryParams: { parent: 'valid-id' } } as any, undefined, communityDataServiceStub, router) .pipe(first()) .subscribe( (canActivate) => @@ -41,7 +41,7 @@ describe('CreateCommunityPageGuard', () => { }); it('should return true when no parent ID has been provided', () => { - guard.canActivate({ queryParams: { } } as any, undefined) + guard({ queryParams: { } } as any, undefined, communityDataServiceStub, router) .pipe(first()) .subscribe( (canActivate) => @@ -50,7 +50,7 @@ describe('CreateCommunityPageGuard', () => { }); it('should return false when the parent ID does not resolve to a community', () => { - guard.canActivate({ queryParams: { parent: 'invalid-id' } } as any, undefined) + guard({ queryParams: { parent: 'invalid-id' } } as any, undefined, communityDataServiceStub, router) .pipe(first()) .subscribe( (canActivate) => @@ -59,7 +59,7 @@ describe('CreateCommunityPageGuard', () => { }); it('should return false when the parent ID resolves to an error response', () => { - guard.canActivate({ queryParams: { parent: 'error-id' } } as any, undefined) + guard({ queryParams: { parent: 'error-id' } } as any, undefined, communityDataServiceStub, router) .pipe(first()) .subscribe( (canActivate) => diff --git a/src/app/community-page/create-community-page/create-community-page.guard.ts b/src/app/community-page/create-community-page/create-community-page.guard.ts index a7e8b8f9f5..c3ee8c7091 100644 --- a/src/app/community-page/create-community-page/create-community-page.guard.ts +++ b/src/app/community-page/create-community-page/create-community-page.guard.ts @@ -1,6 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, + CanActivateFn, Router, RouterStateSnapshot, } from '@angular/router'; @@ -23,35 +24,29 @@ import { } from '../../shared/empty.util'; /** - * Prevent creation of a community with an invalid parent community provided - * @class CreateCommunityPageGuard + * True when either NO parent ID query parameter has been provided, or the parent ID resolves to a valid parent community + * Reroutes to a 404 page when the page cannot be activated */ -@Injectable({ providedIn: 'root' }) -export class CreateCommunityPageGuard { - public constructor(private router: Router, private communityService: CommunityDataService) { +export const createCommunityPageGuard: CanActivateFn = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + communityService: CommunityDataService = inject(CommunityDataService), + router: Router = inject(Router), +): Observable => { + const parentID = route.queryParams.parent; + if (hasNoValue(parentID)) { + return observableOf(true); } - /** - * True when either NO parent ID query parameter has been provided, or the parent ID resolves to a valid parent community - * Reroutes to a 404 page when the page cannot be activated - * @method canActivate - */ - canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - const parentID = route.queryParams.parent; - if (hasNoValue(parentID)) { - return observableOf(true); - } - - return this.communityService.findById(parentID) - .pipe( - getFirstCompletedRemoteData(), - map((communityRD: RemoteData) => hasValue(communityRD) && communityRD.hasSucceeded && hasValue(communityRD.payload)), - tap((isValid: boolean) => { - if (!isValid) { - this.router.navigate(['/404']); - } - }, - ), - ); - } -} + return communityService.findById(parentID) + .pipe( + getFirstCompletedRemoteData(), + map((communityRD: RemoteData) => hasValue(communityRD) && communityRD.hasSucceeded && hasValue(communityRD.payload)), + tap((isValid: boolean) => { + if (!isValid) { + router.navigate(['/404']); + } + }, + ), + ); +}; diff --git a/src/app/community-page/edit-community-page/edit-community-page-routes.ts b/src/app/community-page/edit-community-page/edit-community-page-routes.ts index 588e125053..a15312a216 100644 --- a/src/app/community-page/edit-community-page/edit-community-page-routes.ts +++ b/src/app/community-page/edit-community-page/edit-community-page-routes.ts @@ -1,11 +1,14 @@ -import { Route } from '@angular/router'; +import { + mapToCanActivate, + Route, +} from '@angular/router'; -import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver'; +import { i18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver'; import { CommunityAdministratorGuard } from '../../core/data/feature-authorization/feature-authorization-guard/community-administrator.guard'; import { ResourcePolicyCreateComponent } from '../../shared/resource-policies/create/resource-policy-create.component'; import { ResourcePolicyEditComponent } from '../../shared/resource-policies/edit/resource-policy-edit.component'; -import { ResourcePolicyResolver } from '../../shared/resource-policies/resolvers/resource-policy.resolver'; -import { ResourcePolicyTargetResolver } from '../../shared/resource-policies/resolvers/resource-policy-target.resolver'; +import { resourcePolicyResolver } from '../../shared/resource-policies/resolvers/resource-policy.resolver'; +import { resourcePolicyTargetResolver } from '../../shared/resource-policies/resolvers/resource-policy-target.resolver'; import { CommunityAccessControlComponent } from './community-access-control/community-access-control.component'; import { CommunityAuthorizationsComponent } from './community-authorizations/community-authorizations.component'; import { CommunityCurateComponent } from './community-curate/community-curate.component'; @@ -21,11 +24,11 @@ export const ROUTES: Route[] = [ { path: '', resolve: { - breadcrumb: I18nBreadcrumbResolver, + breadcrumb: i18nBreadcrumbResolver, }, data: { breadcrumbKey: 'community.edit' }, component: EditCommunityPageComponent, - canActivate: [CommunityAdministratorGuard], + canActivate: mapToCanActivate([CommunityAdministratorGuard]), children: [ { path: '', @@ -63,7 +66,7 @@ export const ROUTES: Route[] = [ { path: 'create', resolve: { - resourcePolicyTarget: ResourcePolicyTargetResolver, + resourcePolicyTarget: resourcePolicyTargetResolver, }, component: ResourcePolicyCreateComponent, data: { title: 'resource-policies.create.page.title' }, @@ -71,7 +74,7 @@ export const ROUTES: Route[] = [ { path: 'edit', resolve: { - resourcePolicy: ResourcePolicyResolver, + resourcePolicy: resourcePolicyResolver, }, component: ResourcePolicyEditComponent, data: { title: 'resource-policies.edit.page.title' }, diff --git a/src/app/core/auth/auth-blocking.guard.spec.ts b/src/app/core/auth/auth-blocking.guard.spec.ts index 8cc071ec8e..295e5b1e75 100644 --- a/src/app/core/auth/auth-blocking.guard.spec.ts +++ b/src/app/core/auth/auth-blocking.guard.spec.ts @@ -17,10 +17,10 @@ import { storeModuleConfig, } from '../../app.reducer'; import { authReducer } from './auth.reducer'; -import { AuthBlockingGuard } from './auth-blocking.guard'; +import { authBlockingGuard } from './auth-blocking.guard'; -describe('AuthBlockingGuard', () => { - let guard: AuthBlockingGuard; +describe('authBlockingGuard', () => { + let guard: any; let initialState; let store: Store; let mockStore: MockStore; @@ -44,7 +44,7 @@ describe('AuthBlockingGuard', () => { ], providers: [ provideMockStore({ initialState }), - { provide: AuthBlockingGuard, useValue: guard }, + { provide: authBlockingGuard, useValue: guard }, ], }).compileComponents(); })); @@ -52,14 +52,14 @@ describe('AuthBlockingGuard', () => { beforeEach(() => { store = TestBed.inject(Store); mockStore = store as MockStore; - guard = new AuthBlockingGuard(store); + guard = authBlockingGuard; }); describe(`canActivate`, () => { describe(`when authState.blocking is undefined`, () => { it(`should not emit anything`, (done) => { - expect(guard.canActivate()).toBeObservable(cold('-')); + expect(guard(null, null, store)).toBeObservable(cold('-')); done(); }); }); @@ -77,7 +77,7 @@ describe('AuthBlockingGuard', () => { }); it(`should not emit anything`, (done) => { - expect(guard.canActivate()).toBeObservable(cold('-')); + expect(guard(null, null, store)).toBeObservable(cold('-')); done(); }); }); @@ -95,7 +95,7 @@ describe('AuthBlockingGuard', () => { }); it(`should succeed`, (done) => { - expect(guard.canActivate()).toBeObservable(cold('(a|)', { a: true })); + expect(guard(null, null, store)).toBeObservable(cold('(a|)', { a: true })); done(); }); }); diff --git a/src/app/core/auth/auth-blocking.guard.ts b/src/app/core/auth/auth-blocking.guard.ts index a327b66cf9..c76480ec0d 100644 --- a/src/app/core/auth/auth-blocking.guard.ts +++ b/src/app/core/auth/auth-blocking.guard.ts @@ -1,4 +1,9 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; +import { + ActivatedRouteSnapshot, + CanActivateFn, + RouterStateSnapshot, +} from '@angular/router'; import { select, Store, @@ -19,24 +24,16 @@ import { isAuthenticationBlocking } from './selectors'; * route until the authentication status has loaded. * To ensure all rest requests get the correct auth header. */ -@Injectable({ - providedIn: 'root', -}) -export class AuthBlockingGuard { +export const authBlockingGuard: CanActivateFn = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + store: Store = inject(Store), +): Observable => { + return store.pipe(select(isAuthenticationBlocking)).pipe( + map((isBlocking: boolean) => isBlocking === false), + distinctUntilChanged(), + filter((finished: boolean) => finished === true), + take(1), + ); +}; - constructor(private store: Store) { - } - - /** - * True when the authentication isn't blocking everything - */ - canActivate(): Observable { - return this.store.pipe(select(isAuthenticationBlocking)).pipe( - map((isBlocking: boolean) => isBlocking === false), - distinctUntilChanged(), - filter((finished: boolean) => finished === true), - take(1), - ); - } - -} diff --git a/src/app/core/auth/authenticated.guard.ts b/src/app/core/auth/authenticated.guard.ts index a2f426d747..eba6dc89f9 100644 --- a/src/app/core/auth/authenticated.guard.ts +++ b/src/app/core/auth/authenticated.guard.ts @@ -1,6 +1,8 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, + CanActivateChildFn, + CanActivateFn, Router, RouterStateSnapshot, UrlTree, @@ -16,7 +18,7 @@ import { switchMap, } from 'rxjs/operators'; -import { CoreState } from '../core-state.model'; +import { AppState } from '../../app.reducer'; import { AuthService, LOGIN_ROUTE, @@ -28,49 +30,35 @@ import { /** * Prevent unauthorized activating and loading of routes - * @class AuthenticatedGuard + * True when user is authenticated + * UrlTree with redirect to login page when user isn't authenticated + * @method canActivate */ -@Injectable({ providedIn: 'root' }) -export class AuthenticatedGuard { +export const authenticatedGuard: CanActivateFn = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + authService: AuthService = inject(AuthService), + router: Router = inject(Router), + store: Store = inject(Store), +): Observable => { + const url = state.url; + // redirect to sign in page if user is not authenticated + return store.pipe(select(isAuthenticationLoading)).pipe( + find((isLoading: boolean) => isLoading === false), + switchMap(() => store.pipe(select(isAuthenticated))), + map((authenticated) => { + if (authenticated) { + return authenticated; + } else { + authService.setRedirectUrl(url); + authService.removeToken(); + return router.createUrlTree([LOGIN_ROUTE]); + } + }), + ); +}; - /** - * @constructor - */ - constructor(private authService: AuthService, private router: Router, private store: Store) {} - - /** - * True when user is authenticated - * UrlTree with redirect to login page when user isn't authenticated - * @method canActivate - */ - canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - const url = state.url; - return this.handleAuth(url); - } - - /** - * True when user is authenticated - * UrlTree with redirect to login page when user isn't authenticated - * @method canActivateChild - */ - canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - return this.canActivate(route, state); - } - - private handleAuth(url: string): Observable { - // redirect to sign in page if user is not authenticated - return this.store.pipe(select(isAuthenticationLoading)).pipe( - find((isLoading: boolean) => isLoading === false), - switchMap(() => this.store.pipe(select(isAuthenticated))), - map((authenticated) => { - if (authenticated) { - return authenticated; - } else { - this.authService.setRedirectUrl(url); - this.authService.removeToken(); - return this.router.createUrlTree([LOGIN_ROUTE]); - } - }), - ); - } -} +export const AuthenticatedGuardChild: CanActivateChildFn = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, +) => authenticatedGuard(route, state); diff --git a/src/app/core/breadcrumbs/bitstream-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/bitstream-breadcrumb.resolver.ts index f003bd0f85..5628fe6583 100644 --- a/src/app/core/breadcrumbs/bitstream-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/bitstream-breadcrumb.resolver.ts @@ -1,31 +1,36 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; +import { + ActivatedRouteSnapshot, + ResolveFn, + RouterStateSnapshot, +} from '@angular/router'; +import { Observable } from 'rxjs'; import { BITSTREAM_PAGE_LINKS_TO_FOLLOW } from '../../bitstream-page/bitstream-page.resolver'; +import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config.model'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { BitstreamDataService } from '../data/bitstream-data.service'; import { Bitstream } from '../shared/bitstream.model'; +import { DSpaceObject } from '../shared/dspace-object.model'; import { BitstreamBreadcrumbsService } from './bitstream-breadcrumbs.service'; import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver'; /** - * The class that resolves the BreadcrumbConfig object for an Item + * The resolve function that resolves the BreadcrumbConfig object for an Item */ -@Injectable({ - providedIn: 'root', -}) -export class BitstreamBreadcrumbResolver extends DSOBreadcrumbResolver { - constructor( - protected breadcrumbService: BitstreamBreadcrumbsService, protected dataService: BitstreamDataService) { - super(breadcrumbService, dataService); - } +export const bitstreamBreadcrumbResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + breadcrumbService: BitstreamBreadcrumbsService = inject(BitstreamBreadcrumbsService), + dataService: BitstreamDataService = inject(BitstreamDataService), +): Observable> => { + const linksToFollow: FollowLinkConfig[] = BITSTREAM_PAGE_LINKS_TO_FOLLOW as FollowLinkConfig[]; + return DSOBreadcrumbResolver( + route, + state, + breadcrumbService, + dataService, + ...linksToFollow, + ) as Observable>; +}; - /** - * Method that returns the follow links to already resolve - * The self links defined in this list are expected to be requested somewhere in the near future - * Requesting them as embeds will limit the number of requests - */ - get followLinks(): FollowLinkConfig[] { - return BITSTREAM_PAGE_LINKS_TO_FOLLOW; - } - -} diff --git a/src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts index d337da22c7..7df656a961 100644 --- a/src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts @@ -1,29 +1,36 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; +import { + ActivatedRouteSnapshot, + ResolveFn, + RouterStateSnapshot, +} from '@angular/router'; +import { Observable } from 'rxjs'; +import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config.model'; import { COLLECTION_PAGE_LINKS_TO_FOLLOW } from '../../collection-page/collection-page.resolver'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { CollectionDataService } from '../data/collection-data.service'; import { Collection } from '../shared/collection.model'; +import { DSpaceObject } from '../shared/dspace-object.model'; import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver'; import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; /** - * The class that resolves the BreadcrumbConfig object for a Collection + * The resolve function that resolves the BreadcrumbConfig object for a Collection */ -@Injectable({ - providedIn: 'root', -}) -export class CollectionBreadcrumbResolver extends DSOBreadcrumbResolver { - constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: CollectionDataService) { - super(breadcrumbService, dataService); - } +export const collectionBreadcrumbResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + breadcrumbService: DSOBreadcrumbsService = inject(DSOBreadcrumbsService), + dataService: CollectionDataService = inject(CollectionDataService), +): Observable> => { + const linksToFollow: FollowLinkConfig[] = COLLECTION_PAGE_LINKS_TO_FOLLOW as FollowLinkConfig[]; + return DSOBreadcrumbResolver( + route, + state, + breadcrumbService, + dataService, + ...linksToFollow, + ) as Observable>; +}; - /** - * Method that returns the follow links to already resolve - * The self links defined in this list are expected to be requested somewhere in the near future - * Requesting them as embeds will limit the number of requests - */ - get followLinks(): FollowLinkConfig[] { - return COLLECTION_PAGE_LINKS_TO_FOLLOW; - } -} diff --git a/src/app/core/breadcrumbs/community-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/community-breadcrumb.resolver.ts index 4cbffe9a6a..1064a1cc19 100644 --- a/src/app/core/breadcrumbs/community-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/community-breadcrumb.resolver.ts @@ -1,29 +1,35 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; +import { + ActivatedRouteSnapshot, + ResolveFn, + RouterStateSnapshot, +} from '@angular/router'; +import { Observable } from 'rxjs'; +import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config.model'; import { COMMUNITY_PAGE_LINKS_TO_FOLLOW } from '../../community-page/community-page.resolver'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { CommunityDataService } from '../data/community-data.service'; import { Community } from '../shared/community.model'; +import { DSpaceObject } from '../shared/dspace-object.model'; import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver'; import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; /** - * The class that resolves the BreadcrumbConfig object for a Community + * The resolve function that resolves the BreadcrumbConfig object for a Community */ -@Injectable({ - providedIn: 'root', -}) -export class CommunityBreadcrumbResolver extends DSOBreadcrumbResolver { - constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: CommunityDataService) { - super(breadcrumbService, dataService); - } - - /** - * Method that returns the follow links to already resolve - * The self links defined in this list are expected to be requested somewhere in the near future - * Requesting them as embeds will limit the number of requests - */ - get followLinks(): FollowLinkConfig[] { - return COMMUNITY_PAGE_LINKS_TO_FOLLOW; - } -} +export const communityBreadcrumbResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + breadcrumbService: DSOBreadcrumbsService = inject(DSOBreadcrumbsService), + dataService: CommunityDataService = inject(CommunityDataService), +): Observable> => { + const linksToFollow: FollowLinkConfig[] = COMMUNITY_PAGE_LINKS_TO_FOLLOW as FollowLinkConfig[]; + return DSOBreadcrumbResolver( + route, + state, + breadcrumbService, + dataService, + ...linksToFollow, + ) as Observable>; +}; diff --git a/src/app/core/breadcrumbs/dso-breadcrumb.resolver.spec.ts b/src/app/core/breadcrumbs/dso-breadcrumb.resolver.spec.ts index 59fda031b2..ae19128d4e 100644 --- a/src/app/core/breadcrumbs/dso-breadcrumb.resolver.spec.ts +++ b/src/app/core/breadcrumbs/dso-breadcrumb.resolver.spec.ts @@ -2,12 +2,11 @@ import { getTestScheduler } from 'jasmine-marbles'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { Collection } from '../shared/collection.model'; -import { CollectionBreadcrumbResolver } from './collection-breadcrumb.resolver'; -import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver'; +import { collectionBreadcrumbResolver } from './collection-breadcrumb.resolver'; describe('DSOBreadcrumbResolver', () => { describe('resolve', () => { - let resolver: DSOBreadcrumbResolver; + let resolver: any; let collectionService: any; let dsoBreadcrumbService: any; let testCollection: Collection; @@ -17,18 +16,18 @@ describe('DSOBreadcrumbResolver', () => { beforeEach(() => { uuid = '1234-65487-12354-1235'; - breadcrumbUrl = '/collections/' + uuid; - currentUrl = breadcrumbUrl + '/edit'; + breadcrumbUrl = `/collections/${uuid}`; + currentUrl = `${breadcrumbUrl}/edit`; testCollection = Object.assign(new Collection(), { uuid }); dsoBreadcrumbService = {}; collectionService = { - findById: (id: string) => createSuccessfulRemoteDataObject$(testCollection), + findById: () => createSuccessfulRemoteDataObject$(testCollection), }; - resolver = new CollectionBreadcrumbResolver(dsoBreadcrumbService, collectionService); + resolver = collectionBreadcrumbResolver; }); it('should resolve a breadcrumb config for the correct DSO', () => { - const resolvedConfig = resolver.resolve({ params: { id: uuid } } as any, { url: currentUrl } as any); + const resolvedConfig = resolver({ params: { id: uuid } } as any, { url: currentUrl } as any, dsoBreadcrumbService, collectionService); const expectedConfig = { provider: dsoBreadcrumbService, key: testCollection, url: breadcrumbUrl }; getTestScheduler().expectObservable(resolvedConfig).toBe('(a|)', { a: expectedConfig }); }); diff --git a/src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts index b6f72f937a..cb1f96b103 100644 --- a/src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts @@ -1,4 +1,3 @@ -import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, RouterStateSnapshot, @@ -10,7 +9,6 @@ import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config import { hasValue } from '../../shared/empty.util'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { IdentifiableDataService } from '../data/base/identifiable-data.service'; -import { ChildHALResource } from '../shared/child-hal-resource.model'; import { DSpaceObject } from '../shared/dspace-object.model'; import { getFirstCompletedRemoteData, @@ -19,45 +17,33 @@ import { import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; /** - * The class that resolves the BreadcrumbConfig object for a DSpaceObject + * Method for resolving a breadcrumb config object + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @param {DSOBreadcrumbsService} breadcrumbService + * @param {IdentifiableDataService} dataService + * @param linksToFollow + * @returns BreadcrumbConfig object */ -@Injectable({ - providedIn: 'root', -}) -export abstract class DSOBreadcrumbResolver { - protected constructor( - protected breadcrumbService: DSOBreadcrumbsService, - protected dataService: IdentifiableDataService, - ) { - } - - /** - * Method for resolving a breadcrumb config object - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - * @param {RouterStateSnapshot} state The current RouterStateSnapshot - * @returns BreadcrumbConfig object - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - const uuid = route.params.id; - return this.dataService.findById(uuid, true, false, ...this.followLinks).pipe( - getFirstCompletedRemoteData(), - getRemoteDataPayload(), - map((object: T) => { - if (hasValue(object)) { - const fullPath = state.url; - const url = fullPath.substr(0, fullPath.indexOf(uuid)) + uuid; - return { provider: this.breadcrumbService, key: object, url: url }; - } else { - return undefined; - } - }), - ); - } - - /** - * Method that returns the follow links to already resolve - * The self links defined in this list are expected to be requested somewhere in the near future - * Requesting them as embeds will limit the number of requests - */ - abstract get followLinks(): FollowLinkConfig[]; -} +export const DSOBreadcrumbResolver: (route: ActivatedRouteSnapshot, state: RouterStateSnapshot, breadcrumbService: DSOBreadcrumbsService, dataService: IdentifiableDataService, ...linksToFollow: FollowLinkConfig[]) => Observable> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + breadcrumbService: DSOBreadcrumbsService, + dataService: IdentifiableDataService, + ...linksToFollow: FollowLinkConfig[] +): Observable> => { + const uuid = route.params.id; + return dataService.findById(uuid, true, false, ...linksToFollow).pipe( + getFirstCompletedRemoteData(), + getRemoteDataPayload(), + map((object: DSpaceObject) => { + if (hasValue(object)) { + const fullPath = state.url; + const url = (fullPath.substring(0, fullPath.indexOf(uuid))).concat(uuid); + return { provider: breadcrumbService, key: object, url: url }; + } else { + return undefined; + } + }), + ); +}; diff --git a/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.spec.ts b/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.spec.ts index 05ef2969f7..a85338c490 100644 --- a/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.spec.ts +++ b/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.spec.ts @@ -1,9 +1,9 @@ import { URLCombiner } from '../url-combiner/url-combiner'; -import { I18nBreadcrumbResolver } from './i18n-breadcrumb.resolver'; +import { i18nBreadcrumbResolver } from './i18n-breadcrumb.resolver'; -describe('I18nBreadcrumbResolver', () => { +describe('i18nBreadcrumbResolver', () => { describe('resolve', () => { - let resolver: I18nBreadcrumbResolver; + let resolver: any; let i18nBreadcrumbService: any; let i18nKey: string; let route: any; @@ -27,18 +27,18 @@ describe('I18nBreadcrumbResolver', () => { }; expectedPath = new URLCombiner(parentSegment, segment).toString(); i18nBreadcrumbService = {}; - resolver = new I18nBreadcrumbResolver(i18nBreadcrumbService); + resolver = i18nBreadcrumbResolver; }); it('should resolve the breadcrumb config', () => { - const resolvedConfig = resolver.resolve(route, {} as any); + const resolvedConfig = resolver(route, {} as any, i18nBreadcrumbService); const expectedConfig = { provider: i18nBreadcrumbService, key: i18nKey, url: expectedPath }; expect(resolvedConfig).toEqual(expectedConfig); }); it('should resolve throw an error when no breadcrumbKey is defined', () => { expect(() => { - resolver.resolve({ data: {} } as any, undefined); + resolver({ data: {} } as any, undefined, i18nBreadcrumbService); }).toThrow(); }); }); diff --git a/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.ts index 62004a0d7f..5f5c779211 100644 --- a/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.ts @@ -1,6 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; @@ -10,27 +11,21 @@ import { currentPathFromSnapshot } from '../../shared/utils/route.utils'; import { I18nBreadcrumbsService } from './i18n-breadcrumbs.service'; /** - * The class that resolves a BreadcrumbConfig object with an i18n key string for a route + * Method for resolving an I18n breadcrumb configuration object + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @param {I18nBreadcrumbsService} breadcrumbService + * @returns BreadcrumbConfig object */ -@Injectable({ - providedIn: 'root', -}) -export class I18nBreadcrumbResolver { - constructor(protected breadcrumbService: I18nBreadcrumbsService) { +export const i18nBreadcrumbResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + breadcrumbService: I18nBreadcrumbsService = inject(I18nBreadcrumbsService), +): BreadcrumbConfig => { + const key = route.data.breadcrumbKey; + if (hasNoValue(key)) { + throw new Error('You provided an i18nBreadcrumbResolver for url \"' + route.url + '\" but no breadcrumbKey in the route\'s data'); } - - /** - * Method for resolving an I18n breadcrumb configuration object - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - * @param {RouterStateSnapshot} state The current RouterStateSnapshot - * @returns BreadcrumbConfig object - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): BreadcrumbConfig { - const key = route.data.breadcrumbKey; - if (hasNoValue(key)) { - throw new Error('You provided an i18nBreadcrumbResolver for url \"' + route.url + '\" but no breadcrumbKey in the route\'s data'); - } - const fullPath = currentPathFromSnapshot(route); - return { provider: this.breadcrumbService, key: key, url: fullPath }; - } -} + const fullPath = currentPathFromSnapshot(route); + return { provider: breadcrumbService, key: key, url: fullPath }; +}; diff --git a/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts index f609cbf3bc..cb16cedb42 100644 --- a/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts @@ -1,29 +1,35 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; +import { + ActivatedRouteSnapshot, + ResolveFn, + RouterStateSnapshot, +} from '@angular/router'; +import { Observable } from 'rxjs'; +import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config.model'; import { ITEM_PAGE_LINKS_TO_FOLLOW } from '../../item-page/item.resolver'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { ItemDataService } from '../data/item-data.service'; +import { DSpaceObject } from '../shared/dspace-object.model'; import { Item } from '../shared/item.model'; import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver'; import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; /** - * The class that resolves the BreadcrumbConfig object for an Item + * The resolve function that resolves the BreadcrumbConfig object for an Item */ -@Injectable({ - providedIn: 'root', -}) -export class ItemBreadcrumbResolver extends DSOBreadcrumbResolver { - constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: ItemDataService) { - super(breadcrumbService, dataService); - } - - /** - * Method that returns the follow links to already resolve - * The self links defined in this list are expected to be requested somewhere in the near future - * Requesting them as embeds will limit the number of requests - */ - get followLinks(): FollowLinkConfig[] { - return ITEM_PAGE_LINKS_TO_FOLLOW; - } -} +export const itemBreadcrumbResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + breadcrumbService: DSOBreadcrumbsService = inject(DSOBreadcrumbsService), + dataService: ItemDataService = inject(ItemDataService), +): Observable> => { + const linksToFollow: FollowLinkConfig[] = ITEM_PAGE_LINKS_TO_FOLLOW as FollowLinkConfig[]; + return DSOBreadcrumbResolver( + route, + state, + breadcrumbService, + dataService, + ...linksToFollow, + ) as Observable>; +}; diff --git a/src/app/core/breadcrumbs/navigation-breadcrumb.resolver.spec.ts b/src/app/core/breadcrumbs/navigation-breadcrumb.resolver.spec.ts index db89d02f75..a6bbe49ddd 100644 --- a/src/app/core/breadcrumbs/navigation-breadcrumb.resolver.spec.ts +++ b/src/app/core/breadcrumbs/navigation-breadcrumb.resolver.spec.ts @@ -1,8 +1,8 @@ -import { NavigationBreadcrumbResolver } from './navigation-breadcrumb.resolver'; +import { navigationBreadcrumbResolver } from './navigation-breadcrumb.resolver'; -describe('NavigationBreadcrumbResolver', () => { +describe('navigationBreadcrumbResolver', () => { describe('resolve', () => { - let resolver: NavigationBreadcrumbResolver; + let resolver: any; let NavigationBreadcrumbService: any; let i18nKey: string; let relatedI18nKey: string; @@ -40,11 +40,11 @@ describe('NavigationBreadcrumbResolver', () => { }; expectedPath = '/base/example:/base'; NavigationBreadcrumbService = {}; - resolver = new NavigationBreadcrumbResolver(NavigationBreadcrumbService); + resolver = navigationBreadcrumbResolver; }); it('should resolve the breadcrumb config', () => { - const resolvedConfig = resolver.resolve(route, state); + const resolvedConfig = resolver(route, state, NavigationBreadcrumbService); const expectedConfig = { provider: NavigationBreadcrumbService, key: `${i18nKey}:${relatedI18nKey}`, url: expectedPath }; expect(resolvedConfig).toEqual(expectedConfig); }); diff --git a/src/app/core/breadcrumbs/navigation-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/navigation-breadcrumb.resolver.ts index d2e5d9682c..ac306ee3f5 100644 --- a/src/app/core/breadcrumbs/navigation-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/navigation-breadcrumb.resolver.ts @@ -1,6 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; @@ -8,49 +9,44 @@ import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config import { NavigationBreadcrumbsService } from './navigation-breadcrumb.service'; /** - * The class that resolves a BreadcrumbConfig object with an i18n key string for a route and related parents + * Method for resolving an I18n breadcrumb configuration object + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @param {NavigationBreadcrumbsService} breadcrumbService + * @returns BreadcrumbConfig object */ -@Injectable({ - providedIn: 'root', -}) -export class NavigationBreadcrumbResolver { - - private parentRoutes: ActivatedRouteSnapshot[] = []; - constructor(protected breadcrumbService: NavigationBreadcrumbsService) { - } - - /** - * Method to collect all parent routes snapshot from current route snapshot - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - */ - private getParentRoutes(route: ActivatedRouteSnapshot): void { - if (route.parent) { - this.parentRoutes.push(route.parent); - this.getParentRoutes(route.parent); - } - } - /** - * Method for resolving an I18n breadcrumb configuration object - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - * @param {RouterStateSnapshot} state The current RouterStateSnapshot - * @returns BreadcrumbConfig object - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): BreadcrumbConfig { - this.getParentRoutes(route); - const relatedRoutes = route.data.relatedRoutes; - const parentPaths = this.parentRoutes.map(parent => parent.routeConfig?.path); - const relatedParentRoutes = relatedRoutes.filter(relatedRoute => parentPaths.includes(relatedRoute.path)); - const baseUrlSegmentPath = route.parent.url[route.parent.url.length - 1].path; - const baseUrl = state.url.substring(0, state.url.lastIndexOf(baseUrlSegmentPath) + baseUrlSegmentPath.length); +export const navigationBreadcrumbResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + breadcrumbService: NavigationBreadcrumbsService = inject(NavigationBreadcrumbsService), +): BreadcrumbConfig => { + const parentRoutes: ActivatedRouteSnapshot[] = []; + getParentRoutes(route, parentRoutes); + const relatedRoutes = route.data.relatedRoutes; + const parentPaths = parentRoutes.map(parent => parent.routeConfig?.path); + const relatedParentRoutes = relatedRoutes.filter(relatedRoute => parentPaths.includes(relatedRoute.path)); + const baseUrlSegmentPath = route.parent.url[route.parent.url.length - 1].path; + const baseUrl = state.url.substring(0, state.url.lastIndexOf(baseUrlSegmentPath) + baseUrlSegmentPath.length); - const combinedParentBreadcrumbKeys = relatedParentRoutes.reduce((previous, current) => { - return `${previous}:${current.data.breadcrumbKey}`; - }, route.data.breadcrumbKey); - const combinedUrls = relatedParentRoutes.reduce((previous, current) => { - return `${previous}:${baseUrl}${current.path}`; - }, state.url); + const combinedParentBreadcrumbKeys = relatedParentRoutes.reduce((previous, current) => { + return `${previous}:${current.data.breadcrumbKey}`; + }, route.data.breadcrumbKey); + const combinedUrls = relatedParentRoutes.reduce((previous, current) => { + return `${previous}:${baseUrl}${current.path}`; + }, state.url); - return { provider: this.breadcrumbService, key: combinedParentBreadcrumbKeys, url: combinedUrls }; + return { provider: breadcrumbService, key: combinedParentBreadcrumbKeys, url: combinedUrls }; +}; + +/** + * Method to collect all parent routes snapshot from current route snapshot + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {ActivatedRouteSnapshot[]} parentRoutes + */ +function getParentRoutes(route: ActivatedRouteSnapshot, parentRoutes: ActivatedRouteSnapshot[]): void { + if (route.parent) { + parentRoutes.push(route.parent); + getParentRoutes(route.parent, parentRoutes); } } diff --git a/src/app/core/breadcrumbs/publication-claim-breadcrumb.resolver.spec.ts b/src/app/core/breadcrumbs/publication-claim-breadcrumb.resolver.spec.ts index 7a0e9d43ed..7c2c34d479 100644 --- a/src/app/core/breadcrumbs/publication-claim-breadcrumb.resolver.spec.ts +++ b/src/app/core/breadcrumbs/publication-claim-breadcrumb.resolver.spec.ts @@ -1,8 +1,8 @@ -import { PublicationClaimBreadcrumbResolver } from './publication-claim-breadcrumb.resolver'; +import { publicationClaimBreadcrumbResolver } from './publication-claim-breadcrumb.resolver'; -describe('PublicationClaimBreadcrumbResolver', () => { +describe('publicationClaimBreadcrumbResolver', () => { describe('resolve', () => { - let resolver: PublicationClaimBreadcrumbResolver; + let resolver: any; let publicationClaimBreadcrumbService: any; const fullPath = '/test/publication-claim/openaire:6bee076d-4f2a-4555-a475-04a267769b2a'; const expectedKey = '6bee076d-4f2a-4555-a475-04a267769b2a'; @@ -19,11 +19,11 @@ describe('PublicationClaimBreadcrumbResolver', () => { }, }; publicationClaimBreadcrumbService = {}; - resolver = new PublicationClaimBreadcrumbResolver(publicationClaimBreadcrumbService); + resolver = publicationClaimBreadcrumbResolver; }); it('should resolve the breadcrumb config', () => { - const resolvedConfig = resolver.resolve(route as any, { url: fullPath } as any); + const resolvedConfig = resolver(route as any, { url: fullPath } as any, publicationClaimBreadcrumbService); const expectedConfig = { provider: publicationClaimBreadcrumbService, key: expectedKey }; expect(resolvedConfig).toEqual(expectedConfig); }); diff --git a/src/app/core/breadcrumbs/publication-claim-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/publication-claim-breadcrumb.resolver.ts index 6289d2dd0f..a1b52ce333 100644 --- a/src/app/core/breadcrumbs/publication-claim-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/publication-claim-breadcrumb.resolver.ts @@ -1,28 +1,18 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config.model'; import { PublicationClaimBreadcrumbService } from './publication-claim-breadcrumb.service'; -@Injectable({ - providedIn: 'root', -}) -export class PublicationClaimBreadcrumbResolver { - constructor(protected breadcrumbService: PublicationClaimBreadcrumbService) { - } - - /** - * Method that resolve Publication Claim item into a breadcrumb - * The parameter are retrieved by the url since part of the Publication Claim route config - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - * @param {RouterStateSnapshot} state The current RouterStateSnapshot - * @returns BreadcrumbConfig object - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): BreadcrumbConfig { - const targetId = route.paramMap.get('targetId').split(':')[1]; - return { provider: this.breadcrumbService, key: targetId }; - } -} +export const publicationClaimBreadcrumbResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + breadcrumbService: PublicationClaimBreadcrumbService = inject(PublicationClaimBreadcrumbService), +): BreadcrumbConfig => { + const targetId = route.paramMap.get('targetId').split(':')[1]; + return { provider: breadcrumbService, key: targetId }; +}; diff --git a/src/app/core/breadcrumbs/quality-assurance-breadcrumb.resolver.spec.ts b/src/app/core/breadcrumbs/quality-assurance-breadcrumb.resolver.spec.ts index ea6045c85e..fe2fe77e7f 100644 --- a/src/app/core/breadcrumbs/quality-assurance-breadcrumb.resolver.spec.ts +++ b/src/app/core/breadcrumbs/quality-assurance-breadcrumb.resolver.spec.ts @@ -1,8 +1,8 @@ -import { QualityAssuranceBreadcrumbResolver } from './quality-assurance-breadcrumb.resolver'; +import { qualityAssuranceBreadcrumbResolver } from './quality-assurance-breadcrumb.resolver'; -describe('QualityAssuranceBreadcrumbResolver', () => { +describe('qualityAssuranceBreadcrumbResolver', () => { describe('resolve', () => { - let resolver: QualityAssuranceBreadcrumbResolver; + let resolver: any; let qualityAssuranceBreadcrumbService: any; let route: any; const fullPath = '/test/quality-assurance/'; @@ -19,11 +19,11 @@ describe('QualityAssuranceBreadcrumbResolver', () => { }, }; qualityAssuranceBreadcrumbService = {}; - resolver = new QualityAssuranceBreadcrumbResolver(qualityAssuranceBreadcrumbService); + resolver = qualityAssuranceBreadcrumbResolver; }); it('should resolve the breadcrumb config', () => { - const resolvedConfig = resolver.resolve(route as any, { url: fullPath + 'testSourceId' } as any); + const resolvedConfig = resolver(route as any, { url: fullPath + 'testSourceId' } as any, qualityAssuranceBreadcrumbService); const expectedConfig = { provider: qualityAssuranceBreadcrumbService, key: expectedKey, url: fullPath }; expect(resolvedConfig).toEqual(expectedConfig); }); diff --git a/src/app/core/breadcrumbs/quality-assurance-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/quality-assurance-breadcrumb.resolver.ts index 31df029a0b..6507a75de6 100644 --- a/src/app/core/breadcrumbs/quality-assurance-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/quality-assurance-breadcrumb.resolver.ts @@ -1,36 +1,27 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config.model'; import { QualityAssuranceBreadcrumbService } from './quality-assurance-breadcrumb.service'; -@Injectable({ - providedIn: 'root', -}) -export class QualityAssuranceBreadcrumbResolver { - constructor(protected breadcrumbService: QualityAssuranceBreadcrumbService) {} +export const qualityAssuranceBreadcrumbResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + breadcrumbService: QualityAssuranceBreadcrumbService = inject(QualityAssuranceBreadcrumbService), +): BreadcrumbConfig => { + const sourceId = route.paramMap.get('sourceId'); + const topicId = route.paramMap.get('topicId'); + let key = sourceId; - /** - * Method that resolve QA item into a breadcrumb - * The parameter are retrieved by the url since part of the QA route config - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - * @param {RouterStateSnapshot} state The current RouterStateSnapshot - * @returns BreadcrumbConfig object - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): BreadcrumbConfig { - const sourceId = route.paramMap.get('sourceId'); - const topicId = route.paramMap.get('topicId'); - let key = sourceId; - - if (topicId) { - key += `:${topicId}`; - } - const fullPath = state.url; - const url = fullPath.substr(0, fullPath.indexOf(sourceId)); - - return { provider: this.breadcrumbService, key, url }; + if (topicId) { + key += `:${topicId}`; } -} + const fullPath = state.url; + const url = fullPath.substring(0, fullPath.indexOf(sourceId)); + + return { provider: breadcrumbService, key, url }; +}; diff --git a/src/app/core/coar-notify/notify-info/notify-info.guard.spec.ts b/src/app/core/coar-notify/notify-info/notify-info.guard.spec.ts index 7c8cc3f320..706c8f684b 100644 --- a/src/app/core/coar-notify/notify-info/notify-info.guard.spec.ts +++ b/src/app/core/coar-notify/notify-info/notify-info.guard.spec.ts @@ -1,36 +1,27 @@ -import { TestBed } from '@angular/core/testing'; -import { Router } from '@angular/router'; import { of } from 'rxjs'; -import { NotifyInfoGuard } from './notify-info.guard'; -import { NotifyInfoService } from './notify-info.service'; +import { notifyInfoGuard } from './notify-info.guard'; -describe('NotifyInfoGuard', () => { - let guard: NotifyInfoGuard; +describe('notifyInfoGuard', () => { + let guard: any; let notifyInfoServiceSpy: any; let router: any; beforeEach(() => { notifyInfoServiceSpy = jasmine.createSpyObj('NotifyInfoService', ['isCoarConfigEnabled']); router = jasmine.createSpyObj('Router', ['parseUrl']); - TestBed.configureTestingModule({ - providers: [ - NotifyInfoGuard, - { provide: NotifyInfoService, useValue: notifyInfoServiceSpy }, - { provide: Router, useValue: router }, - ], - }); - guard = TestBed.inject(NotifyInfoGuard); + guard = notifyInfoGuard; }); it('should be created', () => { - expect(guard).toBeTruthy(); + notifyInfoServiceSpy.isCoarConfigEnabled.and.returnValue(of(true)); + expect(guard(null, null, notifyInfoServiceSpy, router)).toBeTruthy(); }); it('should return true if COAR config is enabled', (done) => { notifyInfoServiceSpy.isCoarConfigEnabled.and.returnValue(of(true)); - guard.canActivate(null, null).subscribe((result) => { + guard(null, null, notifyInfoServiceSpy, router).subscribe((result) => { expect(result).toBe(true); done(); }); @@ -40,7 +31,7 @@ describe('NotifyInfoGuard', () => { notifyInfoServiceSpy.isCoarConfigEnabled.and.returnValue(of(false)); router.parseUrl.and.returnValue(of('/404')); - guard.canActivate(null, null).subscribe(() => { + guard(null, null, notifyInfoServiceSpy, router).subscribe(() => { expect(router.parseUrl).toHaveBeenCalledWith('/404'); done(); }); diff --git a/src/app/core/coar-notify/notify-info/notify-info.guard.ts b/src/app/core/coar-notify/notify-info/notify-info.guard.ts index 91f3bf6cde..1025e7b62b 100644 --- a/src/app/core/coar-notify/notify-info/notify-info.guard.ts +++ b/src/app/core/coar-notify/notify-info/notify-info.guard.ts @@ -1,7 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, - CanActivate, + CanActivateFn, Router, RouterStateSnapshot, UrlTree, @@ -11,27 +11,13 @@ import { map } from 'rxjs/operators'; import { NotifyInfoService } from './notify-info.service'; -@Injectable({ - providedIn: 'root', -}) -export class NotifyInfoGuard implements CanActivate { - constructor( - private notifyInfoService: NotifyInfoService, - private router: Router, - ) {} - - canActivate( - route: ActivatedRouteSnapshot, - state: RouterStateSnapshot, - ): Observable { - return this.notifyInfoService.isCoarConfigEnabled().pipe( - map(coarLdnEnabled => { - if (coarLdnEnabled) { - return true; - } else { - return this.router.parseUrl('/404'); - } - }), - ); - } -} +export const notifyInfoGuard: CanActivateFn = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + notifyInfoService: NotifyInfoService = inject(NotifyInfoService), + router: Router = inject(Router), +): Observable => { + return notifyInfoService.isCoarConfigEnabled().pipe( + map(isEnabled => isEnabled ? true : router.parseUrl('/404')), + ); +}; diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts index 351bfa8155..b2d5476d21 100644 --- a/src/app/core/data/collection-data.service.ts +++ b/src/app/core/data/collection-data.service.ts @@ -28,7 +28,10 @@ import { Community } from '../shared/community.model'; import { ContentSource } from '../shared/content-source.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { Item } from '../shared/item.model'; -import { getFirstCompletedRemoteData } from '../shared/operators'; +import { + getAllCompletedRemoteData, + getFirstCompletedRemoteData, +} from '../shared/operators'; import { BitstreamDataService } from './bitstream-data.service'; import { ComColDataService } from './comcol-data.service'; import { CommunityDataService } from './community-data.service'; @@ -84,7 +87,8 @@ export class CollectionDataService extends ComColDataService { }); return this.searchBy(searchHref, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow).pipe( - filter((collections: RemoteData>) => !collections.isResponsePending)); + getAllCompletedRemoteData(), + ); } /** @@ -114,7 +118,8 @@ export class CollectionDataService extends ComColDataService { }); return this.searchBy(searchHref, options, true, reRequestOnStale, ...linksToFollow).pipe( - filter((collections: RemoteData>) => !collections.isResponsePending)); + getAllCompletedRemoteData(), + ); } /** @@ -138,7 +143,8 @@ export class CollectionDataService extends ComColDataService { }); return this.searchBy(searchHref, options, reRequestOnStale).pipe( - filter((collections: RemoteData>) => !collections.isResponsePending)); + getAllCompletedRemoteData(), + ); } /** * Get all collections the user is authorized to submit to, by community and has the metadata @@ -169,7 +175,8 @@ export class CollectionDataService extends ComColDataService { }); return this.searchBy(searchHref, options, true, reRequestOnStale, ...linksToFollow).pipe( - filter((collections: RemoteData>) => !collections.isResponsePending)); + getAllCompletedRemoteData(), + ); } /** @@ -184,9 +191,8 @@ export class CollectionDataService extends ComColDataService { options.elementsPerPage = 1; return this.searchBy(searchHref, options).pipe( - filter((collections: RemoteData>) => !collections.isResponsePending), - take(1), - map((collections: RemoteData>) => collections.payload.totalElements > 0), + getFirstCompletedRemoteData(), + map((collections: RemoteData>) => collections?.payload?.totalElements > 0), ); } diff --git a/src/app/core/data/dso-redirect.service.ts b/src/app/core/data/dso-redirect.service.ts index 1a58c874a6..28628a6246 100644 --- a/src/app/core/data/dso-redirect.service.ts +++ b/src/app/core/data/dso-redirect.service.ts @@ -91,7 +91,7 @@ export class DsoRedirectService { /** * Redirect to a DSpaceObject's path using the given identifier type and ID. * This is used to redirect paths like "/handle/[prefix]/[suffix]" to the object's path (e.g. /items/[uuid]). - * See LookupGuard for more examples. + * See lookupGuard for more examples. * * @param id the identifier of the object to retrieve * @param identifierType the type of the given identifier (defaults to UUID) diff --git a/src/app/core/data/entity-type-data.service.ts b/src/app/core/data/entity-type-data.service.ts index b45e9e997a..d47fadce17 100644 --- a/src/app/core/data/entity-type-data.service.ts +++ b/src/app/core/data/entity-type-data.service.ts @@ -1,7 +1,6 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { - filter, map, switchMap, take, @@ -14,6 +13,7 @@ import { HALEndpointService } from '../shared/hal-endpoint.service'; import { ItemType } from '../shared/item-relationships/item-type.model'; import { RelationshipType } from '../shared/item-relationships/relationship-type.model'; import { + getAllCompletedRemoteData, getFirstSucceededRemoteData, getRemoteDataPayload, } from '../shared/operators'; @@ -89,8 +89,7 @@ export class EntityTypeDataService extends BaseDataService implements getAllAuthorizedRelationshipType(options: FindListOptions = {}): Observable>> { const searchHref = 'findAllByAuthorizedCollection'; - return this.searchBy(searchHref, options).pipe( - filter((type: RemoteData>) => !type.isResponsePending)); + return this.searchBy(searchHref, options).pipe(getAllCompletedRemoteData()); } /** @@ -123,8 +122,7 @@ export class EntityTypeDataService extends BaseDataService implements getAllAuthorizedRelationshipTypeImport(options: FindListOptions = {}): Observable>> { const searchHref = 'findAllByAuthorizedExternalSource'; - return this.searchBy(searchHref, options).pipe( - filter((type: RemoteData>) => !type.isResponsePending)); + return this.searchBy(searchHref, options).pipe(getAllCompletedRemoteData()); } /** @@ -136,15 +134,8 @@ export class EntityTypeDataService extends BaseDataService implements currentPage: 1, }; return this.getAllAuthorizedRelationshipTypeImport(findListOptions).pipe( - map((result: RemoteData>) => { - let output: boolean; - if (result.payload) { - output = ( result.payload.page.length > 1 ); - } else { - output = false; - } - return output; - }), + take(1), + map((result: RemoteData>) => result?.payload?.totalElements > 1), ); } diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard.spec.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard.spec.ts index 09eaaacf84..2cd9fefa93 100644 --- a/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard.spec.ts +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard.spec.ts @@ -12,23 +12,30 @@ import { import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; import { AuthService } from '../../../auth/auth.service'; import { DSpaceObject } from '../../../shared/dspace-object.model'; +import { Item } from '../../../shared/item.model'; import { RemoteData } from '../../remote-data'; import { AuthorizationDataService } from '../authorization-data.service'; import { FeatureID } from '../feature-id'; import { DsoPageSingleFeatureGuard } from './dso-page-single-feature.guard'; +const object = { + self: 'test-selflink', +} as DSpaceObject; + +const testResolver: ResolveFn> = () => createSuccessfulRemoteDataObject$(object); + /** * Test implementation of abstract class DsoPageSingleFeatureGuard */ class DsoPageSingleFeatureGuardImpl extends DsoPageSingleFeatureGuard { - constructor(protected resolver: { - resolve: ResolveFn>; -}, - protected authorizationService: AuthorizationDataService, + + protected resolver: ResolveFn> = testResolver; + + constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService, protected featureID: FeatureID) { - super(resolver, authorizationService, router, authService); + super(authorizationService, router, authService); } getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { @@ -41,27 +48,16 @@ describe('DsoPageSingleFeatureGuard', () => { let authorizationService: AuthorizationDataService; let router: Router; let authService: AuthService; - let resolver: { - resolve: ResolveFn>; -}; - let object: DSpaceObject; let route; let parentRoute; function init() { - object = { - self: 'test-selflink', - } as DSpaceObject; - authorizationService = jasmine.createSpyObj('authorizationService', { isAuthorized: observableOf(true), }); router = jasmine.createSpyObj('router', { parseUrl: {}, }); - resolver = jasmine.createSpyObj('resolver', { - resolve: createSuccessfulRemoteDataObject$(object), - }); authService = jasmine.createSpyObj('authService', { isAuthenticated: observableOf(true), }); @@ -75,7 +71,7 @@ describe('DsoPageSingleFeatureGuard', () => { }, parent: parentRoute, }; - guard = new DsoPageSingleFeatureGuardImpl(resolver, authorizationService, router, authService, undefined); + guard = new DsoPageSingleFeatureGuardImpl(authorizationService, router, authService, undefined); } beforeEach(() => { diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard.spec.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard.spec.ts index 4f482d662d..4e741b5b71 100644 --- a/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard.spec.ts +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard.spec.ts @@ -12,23 +12,30 @@ import { import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; import { AuthService } from '../../../auth/auth.service'; import { DSpaceObject } from '../../../shared/dspace-object.model'; +import { Item } from '../../../shared/item.model'; import { RemoteData } from '../../remote-data'; import { AuthorizationDataService } from '../authorization-data.service'; import { FeatureID } from '../feature-id'; import { DsoPageSomeFeatureGuard } from './dso-page-some-feature.guard'; +const object = { + self: 'test-selflink', +} as DSpaceObject; + +const testResolver: ResolveFn> = () => createSuccessfulRemoteDataObject$(object); + /** * Test implementation of abstract class DsoPageSomeFeatureGuard */ class DsoPageSomeFeatureGuardImpl extends DsoPageSomeFeatureGuard { - constructor(protected resolver: { - resolve: ResolveFn>; -}, - protected authorizationService: AuthorizationDataService, + + protected resolver: ResolveFn> = testResolver; + + constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService, protected featureIDs: FeatureID[]) { - super(resolver, authorizationService, router, authService); + super(authorizationService, router, authService); } getFeatureIDs(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { @@ -41,27 +48,17 @@ describe('DsoPageSomeFeatureGuard', () => { let authorizationService: AuthorizationDataService; let router: Router; let authService: AuthService; - let resolver: { - resolve: ResolveFn>; -}; - let object: DSpaceObject; + let route; let parentRoute; function init() { - object = { - self: 'test-selflink', - } as DSpaceObject; - authorizationService = jasmine.createSpyObj('authorizationService', { isAuthorized: observableOf(true), }); router = jasmine.createSpyObj('router', { parseUrl: {}, }); - resolver = jasmine.createSpyObj('resolver', { - resolve: createSuccessfulRemoteDataObject$(object), - }); authService = jasmine.createSpyObj('authService', { isAuthenticated: observableOf(true), }); @@ -75,7 +72,7 @@ describe('DsoPageSomeFeatureGuard', () => { }, parent: parentRoute, }; - guard = new DsoPageSomeFeatureGuardImpl(resolver, authorizationService, router, authService, []); + guard = new DsoPageSomeFeatureGuardImpl(authorizationService, router, authService, []); } beforeEach(() => { diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard.ts index bda9c1c995..c887b8ae2a 100644 --- a/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard.ts +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard.ts @@ -23,10 +23,10 @@ import { SomeFeatureAuthorizationGuard } from './some-feature-authorization.guar * This guard utilizes a resolver to retrieve the relevant object to check authorizations for */ export abstract class DsoPageSomeFeatureGuard extends SomeFeatureAuthorizationGuard { - constructor(protected resolver: { - resolve: ResolveFn>; -}, - protected authorizationService: AuthorizationDataService, + + protected abstract resolver: ResolveFn>; + + constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) { super(authorizationService, router, authService); @@ -37,14 +37,14 @@ export abstract class DsoPageSomeFeatureGuard extends So */ getObjectUrl(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { const routeWithObjectID = this.getRouteWithDSOId(route); - return (this.resolver.resolve(routeWithObjectID, state) as Observable>).pipe( + return (this.resolver(routeWithObjectID, state) as Observable>).pipe( getAllSucceededRemoteDataPayload(), map((dso) => dso.self), ); } /** - * Method to resolve resolve (parent) route that contains the UUID of the DSO + * Method to resolve (parent) route that contains the UUID of the DSO * @param route The current route */ protected getRouteWithDSOId(route: ActivatedRouteSnapshot): ActivatedRouteSnapshot { diff --git a/src/app/core/data/request.service.spec.ts b/src/app/core/data/request.service.spec.ts index dbf1cc552b..e194901679 100644 --- a/src/app/core/data/request.service.spec.ts +++ b/src/app/core/data/request.service.spec.ts @@ -17,6 +17,7 @@ import { getTestScheduler, } from 'jasmine-marbles'; import { + BehaviorSubject, EMPTY, Observable, of as observableOf, @@ -33,6 +34,7 @@ import { ObjectCacheService } from '../cache/object-cache.service'; import { coreReducers } from '../core.reducers'; import { CoreState } from '../core-state.model'; import { UUIDService } from '../shared/uuid.service'; +import { XSRFService } from '../xsrf/xsrf.service'; import { RequestConfigureAction, RequestExecuteAction, @@ -60,6 +62,7 @@ describe('RequestService', () => { let uuidService: UUIDService; let store: Store; let mockStore: MockStore; + let xsrfService: XSRFService; const testUUID = '5f2a0d2a-effa-4d54-bd54-5663b960f9eb'; const testHref = 'https://rest.api/endpoint/selfLink'; @@ -105,10 +108,15 @@ describe('RequestService', () => { store = TestBed.inject(Store); mockStore = store as MockStore; mockStore.setState(initialState); + xsrfService = { + tokenInitialized$: new BehaviorSubject(false), + } as XSRFService; + service = new RequestService( objectCache, uuidService, store, + xsrfService, undefined, ); serviceAsAny = service as any; diff --git a/src/app/core/data/request.service.ts b/src/app/core/data/request.service.ts index 8cd74ccc47..3e2bbfdc9f 100644 --- a/src/app/core/data/request.service.ts +++ b/src/app/core/data/request.service.ts @@ -43,6 +43,7 @@ import { requestIndexSelector, } from '../index/index.selectors'; import { UUIDService } from '../shared/uuid.service'; +import { XSRFService } from '../xsrf/xsrf.service'; import { RequestConfigureAction, RequestExecuteAction, @@ -169,6 +170,7 @@ export class RequestService { constructor(private objectCache: ObjectCacheService, private uuidService: UUIDService, private store: Store, + protected xsrfService: XSRFService, private indexStore: Store) { } @@ -453,7 +455,17 @@ export class RequestService { private dispatchRequest(request: RestRequest) { asapScheduler.schedule(() => { this.store.dispatch(new RequestConfigureAction(request)); - this.store.dispatch(new RequestExecuteAction(request.uuid)); + // If it's a GET request, or we have an XSRF token, dispatch it immediately + if (request.method === RestRequestMethod.GET || this.xsrfService.tokenInitialized$.getValue() === true) { + this.store.dispatch(new RequestExecuteAction(request.uuid)); + } else { + // Otherwise wait for the XSRF token first + this.xsrfService.tokenInitialized$.pipe( + find((hasInitialized: boolean) => hasInitialized === true), + ).subscribe(() => { + this.store.dispatch(new RequestExecuteAction(request.uuid)); + }); + } }); } diff --git a/src/app/core/feedback/feedback.guard.ts b/src/app/core/feedback/feedback.guard.ts index 3170aa4088..eef5205ee4 100644 --- a/src/app/core/feedback/feedback.guard.ts +++ b/src/app/core/feedback/feedback.guard.ts @@ -1,6 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, + CanActivateFn, RouterStateSnapshot, UrlTree, } from '@angular/router'; @@ -10,16 +11,13 @@ import { AuthorizationDataService } from '../data/feature-authorization/authoriz import { FeatureID } from '../data/feature-authorization/feature-id'; /** - * An guard for redirecting users to the feedback page if user is authorized + * A guard for redirecting users to the feedback page if user is authorized */ -@Injectable({ providedIn: 'root' }) -export class FeedbackGuard { +export const feedbackGuard: CanActivateFn = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + authorizationService: AuthorizationDataService = inject(AuthorizationDataService), +): Observable => { + return authorizationService.isAuthorized(FeatureID.CanSendFeedback); +}; - constructor(private authorizationService: AuthorizationDataService) { - } - - canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - return this.authorizationService.isAuthorized(FeatureID.CanSendFeedback); - } - -} diff --git a/src/app/core/reload/reload.guard.spec.ts b/src/app/core/reload/reload.guard.spec.ts index d7b92146af..31ac741800 100644 --- a/src/app/core/reload/reload.guard.spec.ts +++ b/src/app/core/reload/reload.guard.spec.ts @@ -2,17 +2,17 @@ import { Router } from '@angular/router'; import { AppConfig } from '../../../config/app-config.interface'; import { DefaultAppConfig } from '../../../config/default-app-config'; -import { ReloadGuard } from './reload.guard'; +import { reloadGuard } from './reload.guard'; -describe('ReloadGuard', () => { - let guard: ReloadGuard; +describe('reloadGuard', () => { + let guard: any; let router: Router; let appConfig: AppConfig; beforeEach(() => { router = jasmine.createSpyObj('router', ['parseUrl', 'createUrlTree']); appConfig = new DefaultAppConfig(); - guard = new ReloadGuard(router, appConfig); + guard = reloadGuard; }); describe('canActivate', () => { @@ -31,7 +31,7 @@ describe('ReloadGuard', () => { }); it('should create a UrlTree with the redirect URL', () => { - guard.canActivate(route, undefined); + guard(route, undefined, appConfig, router); expect(router.parseUrl).toHaveBeenCalledWith(redirectUrl.substring(1)); }); }); @@ -44,7 +44,7 @@ describe('ReloadGuard', () => { }); it('should create a UrlTree to home', () => { - guard.canActivate(route, undefined); + guard(route, undefined, appConfig, router); expect(router.createUrlTree).toHaveBeenCalledWith(['home']); }); }); diff --git a/src/app/core/reload/reload.guard.ts b/src/app/core/reload/reload.guard.ts index 5aab83df31..4e6b1f21a5 100644 --- a/src/app/core/reload/reload.guard.ts +++ b/src/app/core/reload/reload.guard.ts @@ -1,9 +1,7 @@ -import { - Inject, - Injectable, -} from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, + CanActivateFn, Router, RouterStateSnapshot, UrlTree, @@ -13,33 +11,25 @@ import { APP_CONFIG, AppConfig, } from '../../../config/app-config.interface'; +import { HOME_PAGE_PATH } from '../../app-routing-paths'; import { isNotEmpty } from '../../shared/empty.util'; /** * A guard redirecting the user to the URL provided in the route's query params * When no redirect url is found, the user is redirected to the homepage */ -@Injectable({ providedIn: 'root' }) -export class ReloadGuard { - constructor( - private router: Router, - @Inject(APP_CONFIG) private appConfig: AppConfig, - ) { +export const reloadGuard: CanActivateFn = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + appConfig: AppConfig = inject(APP_CONFIG), + router: Router = inject(Router), +): UrlTree => { + if (isNotEmpty(route.queryParams.redirect)) { + const url = route.queryParams.redirect.startsWith(appConfig.ui.nameSpace) + ? route.queryParams.redirect.substring(appConfig.ui.nameSpace.length) + : route.queryParams.redirect; + return router.parseUrl(url); + } else { + return router.createUrlTree([HOME_PAGE_PATH]); } - - /** - * Get the UrlTree of the URL to redirect to - * @param route - * @param state - */ - canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): UrlTree { - if (isNotEmpty(route.queryParams.redirect)) { - const url = route.queryParams.redirect.startsWith(this.appConfig.ui.nameSpace) - ? route.queryParams.redirect.substring(this.appConfig.ui.nameSpace.length) - : route.queryParams.redirect; - return this.router.parseUrl(url); - } else { - return this.router.createUrlTree(['home']); - } - } -} +}; diff --git a/src/app/core/server-check/server-check.guard.spec.ts b/src/app/core/server-check/server-check.guard.spec.ts index ad2120aaaa..e89b16eedc 100644 --- a/src/app/core/server-check/server-check.guard.spec.ts +++ b/src/app/core/server-check/server-check.guard.spec.ts @@ -16,7 +16,7 @@ import { ServerCheckGuard } from './server-check.guard'; import SpyObj = jasmine.SpyObj; describe('ServerCheckGuard', () => { - let guard: ServerCheckGuard; + let guard: any; let router: Router; let eventSubject: ReplaySubject; let rootDataServiceStub: SpyObj; @@ -39,7 +39,7 @@ describe('ServerCheckGuard', () => { navigateByUrl: jasmine.createSpy('navigateByUrl'), parseUrl: jasmine.createSpy('parseUrl').and.returnValue(redirectUrlTree), } as any; - guard = new ServerCheckGuard(router, rootDataServiceStub); + guard = ServerCheckGuard; }); it('should be created', () => { @@ -53,7 +53,7 @@ describe('ServerCheckGuard', () => { it('should return true', () => { testScheduler.run(({ expectObservable }) => { - const result$ = guard.canActivateChild({} as any, {} as any); + const result$ = guard({} as any, {} as any, rootDataServiceStub, router); expectObservable(result$).toBe('(a|)', { a: true }); }); }); @@ -66,14 +66,14 @@ describe('ServerCheckGuard', () => { it('should return a UrlTree with the route to the 500 error page', () => { testScheduler.run(({ expectObservable }) => { - const result$ = guard.canActivateChild({} as any, {} as any); + const result$ = guard({} as any, {} as any, rootDataServiceStub, router); expectObservable(result$).toBe('(b|)', { b: redirectUrlTree }); }); expect(router.parseUrl).toHaveBeenCalledWith('/500'); }); }); - describe(`listenForRouteChanges`, () => { + xdescribe(`listenForRouteChanges`, () => { it(`should invalidate the root cache, when the method is first called`, () => { testScheduler.run(() => { guard.listenForRouteChanges(); diff --git a/src/app/core/server-check/server-check.guard.ts b/src/app/core/server-check/server-check.guard.ts index f023cc3e75..6d4a81b7b5 100644 --- a/src/app/core/server-check/server-check.guard.ts +++ b/src/app/core/server-check/server-check.guard.ts @@ -1,14 +1,13 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, - NavigationStart, + CanActivateChildFn, Router, RouterStateSnapshot, UrlTree, } from '@angular/router'; import { Observable } from 'rxjs'; import { - filter, map, take, } from 'rxjs/operators'; @@ -16,52 +15,18 @@ import { import { getPageInternalServerErrorRoute } from '../../app-routing-paths'; import { RootDataService } from '../data/root-data.service'; -@Injectable({ - providedIn: 'root', -}) /** * A guard that checks if root api endpoint is reachable. * If not redirect to 500 error page */ -export class ServerCheckGuard { - constructor(private router: Router, private rootDataService: RootDataService) { - } - - /** - * True when root api endpoint is reachable. - */ - canActivateChild( - route: ActivatedRouteSnapshot, - state: RouterStateSnapshot, - ): Observable { - - return this.rootDataService.checkServerAvailability().pipe( - take(1), - map((isAvailable: boolean) => { - if (!isAvailable) { - return this.router.parseUrl(getPageInternalServerErrorRoute()); - } else { - return true; - } - }), - ); - } - - /** - * Listen to all router events. Every time a new navigation starts, invalidate the cache - * for the root endpoint. That way we retrieve it once per routing operation to ensure the - * backend is not down. But if the guard is called multiple times during the same routing - * operation, the cached version is used. - */ - listenForRouteChanges(): void { - // we'll always be too late for the first NavigationStart event with the router subscribe below, - // so this statement is for the very first route operation. - this.rootDataService.invalidateRootCache(); - - this.router.events.pipe( - filter(event => event instanceof NavigationStart), - ).subscribe(() => { - this.rootDataService.invalidateRootCache(); - }); - } -} +export const ServerCheckGuard: CanActivateChildFn = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + rootDataService: RootDataService = inject(RootDataService), + router: Router = inject(Router), +): Observable => { + return rootDataService.checkServerAvailability().pipe( + take(1), + map((isAvailable: boolean) => isAvailable ? true : router.parseUrl(getPageInternalServerErrorRoute())), + ); +}; diff --git a/src/app/core/submission/resolver/submission-object.resolver.ts b/src/app/core/submission/resolver/submission-object.resolver.ts index 2779193fb1..4ddd9dea93 100644 --- a/src/app/core/submission/resolver/submission-object.resolver.ts +++ b/src/app/core/submission/resolver/submission-object.resolver.ts @@ -1,45 +1,37 @@ -import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, RouterStateSnapshot, } from '@angular/router'; -import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; import { switchMap } from 'rxjs/operators'; import { followLink } from '../../../shared/utils/follow-link-config.model'; import { IdentifiableDataService } from '../../data/base/identifiable-data.service'; import { RemoteData } from '../../data/remote-data'; +import { Item } from '../../shared/item.model'; import { getFirstCompletedRemoteData } from '../../shared/operators'; +import { SubmissionObject } from '../models/submission-object.model'; /** - * This class represents a resolver that requests a specific item before the route is activated + * Method for resolving an item based on the parameters in the current route + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @param {IdentifiableDataService } dataService + * @returns Observable<> Emits the found item based on the parameters in the current route, + * or an error if something went wrong */ -@Injectable({ providedIn: 'root' }) -export class SubmissionObjectResolver { - constructor( - protected dataService: IdentifiableDataService, - protected store: Store, - ) { - } - - /** - * Method for resolving an item based on the parameters in the current route - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - * @param {RouterStateSnapshot} state The current RouterStateSnapshot - * @returns Observable<> Emits the found item based on the parameters in the current route, - * or an error if something went wrong - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - const itemRD$ = this.dataService.findById(route.params.id, - true, - false, - followLink('item'), - ).pipe( - getFirstCompletedRemoteData(), - switchMap((wfiRD: RemoteData) => wfiRD.payload.item as Observable>), - getFirstCompletedRemoteData(), - ); - return itemRD$; - } -} +export const SubmissionObjectResolver: (route: ActivatedRouteSnapshot, state: RouterStateSnapshot, dataService: IdentifiableDataService) => Observable> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + dataService: IdentifiableDataService, +): Observable> => { + return dataService.findById(route.params.id, + true, + false, + followLink('item'), + ).pipe( + getFirstCompletedRemoteData(), + switchMap((wfiRD: RemoteData) => wfiRD.payload.item as Observable>), + getFirstCompletedRemoteData(), + ); +}; diff --git a/src/app/core/xsrf/browser-xsrf.service.spec.ts b/src/app/core/xsrf/browser-xsrf.service.spec.ts new file mode 100644 index 0000000000..aba3edd330 --- /dev/null +++ b/src/app/core/xsrf/browser-xsrf.service.spec.ts @@ -0,0 +1,58 @@ +import { HttpClient } from '@angular/common/http'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import { TestBed } from '@angular/core/testing'; + +import { RESTURLCombiner } from '../url-combiner/rest-url-combiner'; +import { BrowserXSRFService } from './browser-xsrf.service'; + +describe(`BrowserXSRFService`, () => { + let service: BrowserXSRFService; + let httpClient: HttpClient; + let httpTestingController: HttpTestingController; + + const endpointURL = new RESTURLCombiner('/security/csrf').toString(); + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ HttpClientTestingModule ], + providers: [ BrowserXSRFService ], + }); + httpClient = TestBed.inject(HttpClient); + httpTestingController = TestBed.inject(HttpTestingController); + service = TestBed.inject(BrowserXSRFService); + }); + + describe(`initXSRFToken`, () => { + it(`should perform a GET to the csrf endpoint`, (done: DoneFn) => { + service.initXSRFToken(httpClient)(); + + const req = httpTestingController.expectOne({ + url: endpointURL, + method: 'GET', + }); + + req.flush({}); + httpTestingController.verify(); + expect().nothing(); + done(); + }); + + describe(`when the GET succeeds`, () => { + it(`should set tokenInitialized$ to true`, (done: DoneFn) => { + service.initXSRFToken(httpClient)(); + + const req = httpTestingController.expectOne(endpointURL); + + req.flush({}); + httpTestingController.verify(); + + expect(service.tokenInitialized$.getValue()).toBeTrue(); + done(); + }); + }); + + }); +}); diff --git a/src/app/core/xsrf/browser-xsrf.service.ts b/src/app/core/xsrf/browser-xsrf.service.ts new file mode 100644 index 0000000000..121defc061 --- /dev/null +++ b/src/app/core/xsrf/browser-xsrf.service.ts @@ -0,0 +1,30 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { take } from 'rxjs/operators'; + +import { RESTURLCombiner } from '../url-combiner/rest-url-combiner'; +import { XSRFService } from './xsrf.service'; + +/** + * Browser (CSR) Service to obtain a new CSRF/XSRF token when needed by our RequestService + * to perform a modify request (e.g. POST/PUT/DELETE). + * NOTE: This is primarily necessary before the *first* modifying request, as the CSRF + * token may not yet be initialized. + */ +@Injectable() +export class BrowserXSRFService extends XSRFService { + initXSRFToken(httpClient: HttpClient): () => Promise { + return () => new Promise((resolve) => { + // Force a new token to be created by calling the CSRF endpoint + httpClient.get(new RESTURLCombiner('/security/csrf').toString(), undefined).pipe( + take(1), + ).subscribe(() => { + // Once token is returned, set tokenInitialized to true. + this.tokenInitialized$.next(true); + }); + + // return immediately, the rest of the app doesn't need to wait for this to finish + resolve(); + }); + } +} diff --git a/src/app/core/xsrf/server-xsrf.service.spec.ts b/src/app/core/xsrf/server-xsrf.service.spec.ts new file mode 100644 index 0000000000..05728edb42 --- /dev/null +++ b/src/app/core/xsrf/server-xsrf.service.spec.ts @@ -0,0 +1,33 @@ +import { HttpClient } from '@angular/common/http'; + +import { ServerXSRFService } from './server-xsrf.service'; + +describe(`ServerXSRFService`, () => { + let service: ServerXSRFService; + let httpClient: HttpClient; + + beforeEach(() => { + httpClient = jasmine.createSpyObj(['post', 'get', 'request']); + service = new ServerXSRFService(); + }); + + describe(`initXSRFToken`, () => { + it(`shouldn't perform any requests`, (done: DoneFn) => { + service.initXSRFToken(httpClient)().then(() => { + for (const prop in httpClient) { + if (httpClient.hasOwnProperty(prop)) { + expect(httpClient[prop]).not.toHaveBeenCalled(); + } + } + done(); + }); + }); + + it(`should leave tokenInitialized$ on false`, (done: DoneFn) => { + service.initXSRFToken(httpClient)().then(() => { + expect(service.tokenInitialized$.getValue()).toBeFalse(); + done(); + }); + }); + }); +}); diff --git a/src/app/core/xsrf/server-xsrf.service.ts b/src/app/core/xsrf/server-xsrf.service.ts new file mode 100644 index 0000000000..f729aa49a7 --- /dev/null +++ b/src/app/core/xsrf/server-xsrf.service.ts @@ -0,0 +1,19 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; + +import { XSRFService } from './xsrf.service'; + +/** + * Server (SSR) Service to obtain a new CSRF/XSRF token. Because SSR only triggers GET + * requests a CSRF token is never needed. + */ +@Injectable() +export class ServerXSRFService extends XSRFService { + initXSRFToken(httpClient: HttpClient): () => Promise { + return () => new Promise((resolve) => { + // return immediately, and keep tokenInitialized$ false. The server side can make only GET + // requests, since it can never get a valid XSRF cookie + resolve(); + }); + } +} diff --git a/src/app/core/xsrf/xsrf.service.spec.ts b/src/app/core/xsrf/xsrf.service.spec.ts new file mode 100644 index 0000000000..56564a294c --- /dev/null +++ b/src/app/core/xsrf/xsrf.service.spec.ts @@ -0,0 +1,21 @@ +import { HttpClient } from '@angular/common/http'; + +import { XSRFService } from './xsrf.service'; + +class XSRFServiceImpl extends XSRFService { + initXSRFToken(httpClient: HttpClient): () => Promise { + return () => null; + } +} + +describe(`XSRFService`, () => { + let service: XSRFService; + + beforeEach(() => { + service = new XSRFServiceImpl(); + }); + + it(`should start with tokenInitialized$.hasValue() === false`, () => { + expect(service.tokenInitialized$.getValue()).toBeFalse(); + }); +}); diff --git a/src/app/core/xsrf/xsrf.service.ts b/src/app/core/xsrf/xsrf.service.ts new file mode 100644 index 0000000000..99b27021b6 --- /dev/null +++ b/src/app/core/xsrf/xsrf.service.ts @@ -0,0 +1,15 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; + +/** + * Abstract CSRF/XSRF Service used to track whether a CSRF token has been received + * from the DSpace REST API. Once it is received, the "tokenInitialized$" flag will + * be set to "true". + */ +@Injectable() +export abstract class XSRFService { + public tokenInitialized$: BehaviorSubject = new BehaviorSubject(false); + + abstract initXSRFToken(httpClient: HttpClient): () => Promise; +} diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.spec.ts b/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.spec.ts index 0172c41bd4..150457d54e 100644 --- a/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.spec.ts +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.spec.ts @@ -35,6 +35,7 @@ import { Bitstream } from '../../../../../core/shared/bitstream.model'; import { HALEndpointService } from '../../../../../core/shared/hal-endpoint.service'; import { Item } from '../../../../../core/shared/item.model'; import { UUIDService } from '../../../../../core/shared/uuid.service'; +import { XSRFService } from '../../../../../core/xsrf/xsrf.service'; import { getMockThemeService } from '../../../../../shared/mocks/theme-service.mock'; import { CollectionElementLinkType } from '../../../../../shared/object-collection/collection-element-link.type'; import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; @@ -138,6 +139,7 @@ describe('PersonSearchResultListElementSubmissionComponent', () => { { provide: Store, useValue: {} }, { provide: ObjectCacheService, useValue: {} }, { provide: UUIDService, useValue: {} }, + { provide: XSRFService, useValue: {} }, { provide: RemoteDataBuildService, useValue: {} }, { provide: CommunityDataService, useValue: {} }, { provide: HALEndpointService, useValue: {} }, diff --git a/src/app/forgot-password/forgot-password-routes.ts b/src/app/forgot-password/forgot-password-routes.ts index 8251d1892a..66acc70794 100644 --- a/src/app/forgot-password/forgot-password-routes.ts +++ b/src/app/forgot-password/forgot-password-routes.ts @@ -1,6 +1,6 @@ import { Route } from '@angular/router'; -import { RegistrationGuard } from '../register-page/registration.guard'; +import { registrationGuard } from '../register-page/registration.guard'; import { ThemedForgotEmailComponent } from './forgot-password-email/themed-forgot-email.component'; import { ThemedForgotPasswordFormComponent } from './forgot-password-form/themed-forgot-password-form.component'; @@ -13,6 +13,6 @@ export const ROUTES: Route[] = [ { path: ':token', component: ThemedForgotPasswordFormComponent, - canActivate: [RegistrationGuard], + canActivate: [registrationGuard], }, ]; diff --git a/src/app/health-page/health-page-routes.ts b/src/app/health-page/health-page-routes.ts index befd4d218c..4c02bc548f 100644 --- a/src/app/health-page/health-page-routes.ts +++ b/src/app/health-page/health-page-routes.ts @@ -1,18 +1,21 @@ -import { Route } from '@angular/router'; +import { + mapToCanActivate, + Route, +} from '@angular/router'; -import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; +import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; import { SiteAdministratorGuard } from '../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard'; import { HealthPageComponent } from './health-page.component'; export const ROUTES: Route[] = [ { path: '', - resolve: { breadcrumb: I18nBreadcrumbResolver }, + resolve: { breadcrumb: i18nBreadcrumbResolver }, data: { breadcrumbKey: 'health', title: 'health-page.title', }, - canActivate: [SiteAdministratorGuard], + canActivate: mapToCanActivate([SiteAdministratorGuard]), component: HealthPageComponent, }, ]; diff --git a/src/app/home-page/home-news/home-news.component.html b/src/app/home-page/home-news/home-news.component.html index 8d3f99b60d..c83d2103c8 100644 --- a/src/app/home-page/home-news/home-news.component.html +++ b/src/app/home-page/home-news/home-news.component.html @@ -2,7 +2,7 @@
-

DSpace 7

+

DSpace 8

DSpace is the world leading open source repository platform that enables organisations to:

diff --git a/src/app/home-page/home-page-routes.ts b/src/app/home-page/home-page-routes.ts index ae1282abc8..6e4a8f353f 100644 --- a/src/app/home-page/home-page-routes.ts +++ b/src/app/home-page/home-page-routes.ts @@ -2,7 +2,7 @@ import { Route } from '@angular/router'; import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model'; import { MenuItemType } from '../shared/menu/menu-item-type.model'; -import { HomePageResolver } from './home-page.resolver'; +import { homePageResolver } from './home-page.resolver'; import { ThemedHomePageComponent } from './themed-home-page.component'; export const ROUTES: Route[] = [ @@ -27,7 +27,7 @@ export const ROUTES: Route[] = [ }, }, resolve: { - site: HomePageResolver, + site: homePageResolver, }, }, ]; diff --git a/src/app/home-page/home-page.resolver.ts b/src/app/home-page/home-page.resolver.ts index eafc4b2846..76f27edbf1 100644 --- a/src/app/home-page/home-page.resolver.ts +++ b/src/app/home-page/home-page.resolver.ts @@ -1,6 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; import { Observable } from 'rxjs'; @@ -9,21 +10,10 @@ import { take } from 'rxjs/operators'; import { SiteDataService } from '../core/data/site-data.service'; import { Site } from '../core/shared/site.model'; -/** - * The class that resolve the Site object for a route - */ -@Injectable({ providedIn: 'root' }) -export class HomePageResolver { - constructor(private siteService: SiteDataService) { - } - - /** - * Method for resolving a site object - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - * @param {RouterStateSnapshot} state The current RouterStateSnapshot - * @returns Observable Emits the found Site object, or an error if something went wrong - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable | Promise | Site { - return this.siteService.find().pipe(take(1)); - } -} +export const homePageResolver: ResolveFn = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + siteService: SiteDataService = inject(SiteDataService), +): Observable => { + return siteService.find().pipe(take(1)); +}; diff --git a/src/app/import-external-page/import-external-page-routes.ts b/src/app/import-external-page/import-external-page-routes.ts index 4c70e9d328..294d4160fa 100644 --- a/src/app/import-external-page/import-external-page-routes.ts +++ b/src/app/import-external-page/import-external-page-routes.ts @@ -1,11 +1,11 @@ import { Route } from '@angular/router'; -import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; +import { authenticatedGuard } from '../core/auth/authenticated.guard'; import { ThemedSubmissionImportExternalComponent } from '../submission/import-external/themed-submission-import-external.component'; export const ROUTES: Route[] = [ { - canActivate: [AuthenticatedGuard], + canActivate: [authenticatedGuard], path: '', component: ThemedSubmissionImportExternalComponent, pathMatch: 'full', diff --git a/src/app/info/info-routes.ts b/src/app/info/info-routes.ts index 8697529619..423a93ea45 100644 --- a/src/app/info/info-routes.ts +++ b/src/app/info/info-routes.ts @@ -1,6 +1,6 @@ import { environment } from '../../environments/environment'; -import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; -import { FeedbackGuard } from '../core/feedback/feedback.guard'; +import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; +import { feedbackGuard } from '../core/feedback/feedback.guard'; import { ThemedEndUserAgreementComponent } from './end-user-agreement/themed-end-user-agreement.component'; import { ThemedFeedbackComponent } from './feedback/themed-feedback.component'; import { @@ -15,20 +15,20 @@ export const ROUTES = [ { path: FEEDBACK_PATH, component: ThemedFeedbackComponent, - resolve: { breadcrumb: I18nBreadcrumbResolver }, + resolve: { breadcrumb: i18nBreadcrumbResolver }, data: { title: 'info.feedback.title', breadcrumbKey: 'info.feedback' }, - canActivate: [FeedbackGuard], + canActivate: [feedbackGuard], }, environment.info.enableEndUserAgreement ? { path: END_USER_AGREEMENT_PATH, component: ThemedEndUserAgreementComponent, - resolve: { breadcrumb: I18nBreadcrumbResolver }, + resolve: { breadcrumb: i18nBreadcrumbResolver }, data: { title: 'info.end-user-agreement.title', breadcrumbKey: 'info.end-user-agreement' }, } : undefined, environment.info.enablePrivacyStatement ? { path: PRIVACY_PATH, component: ThemedPrivacyComponent, - resolve: { breadcrumb: I18nBreadcrumbResolver }, + resolve: { breadcrumb: i18nBreadcrumbResolver }, data: { title: 'info.privacy.title', breadcrumbKey: 'info.privacy' }, } : undefined, ]; diff --git a/src/app/item-page/bitstreams/upload/upload-bitstream.component.ts b/src/app/item-page/bitstreams/upload/upload-bitstream.component.ts index 49b1ffa5fb..e8b216aa52 100644 --- a/src/app/item-page/bitstreams/upload/upload-bitstream.component.ts +++ b/src/app/item-page/bitstreams/upload/upload-bitstream.component.ts @@ -146,7 +146,7 @@ export class UploadBitstreamComponent implements OnInit, OnDestroy { /** * Initialize component properties: - * itemRD$ Fetched from the current route data (populated by BitstreamPageResolver) + * itemRD$ Fetched from the current route data (populated by bitstreamPageResolver) * selectedBundleId Starts off by checking if the route's queryParams contain a "bundle" parameter. If none is found, * the ID of the first bundle in the list is selected. * Calls setUploadUrl after setting the selected bundle diff --git a/src/app/item-page/edit-item-page/edit-item-page-routes.ts b/src/app/item-page/edit-item-page/edit-item-page-routes.ts index 63885fd4e1..1b1e43a883 100644 --- a/src/app/item-page/edit-item-page/edit-item-page-routes.ts +++ b/src/app/item-page/edit-item-page/edit-item-page-routes.ts @@ -1,11 +1,14 @@ -import { Route } from '@angular/router'; +import { + mapToCanActivate, + Route, +} from '@angular/router'; -import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver'; +import { i18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver'; import { ThemedDsoEditMetadataComponent } from '../../dso-shared/dso-edit-metadata/themed-dso-edit-metadata.component'; import { ResourcePolicyCreateComponent } from '../../shared/resource-policies/create/resource-policy-create.component'; import { ResourcePolicyEditComponent } from '../../shared/resource-policies/edit/resource-policy-edit.component'; -import { ResourcePolicyResolver } from '../../shared/resource-policies/resolvers/resource-policy.resolver'; -import { ResourcePolicyTargetResolver } from '../../shared/resource-policies/resolvers/resource-policy-target.resolver'; +import { resourcePolicyResolver } from '../../shared/resource-policies/resolvers/resource-policy.resolver'; +import { resourcePolicyTargetResolver } from '../../shared/resource-policies/resolvers/resource-policy-target.resolver'; import { EditItemPageComponent } from './edit-item-page.component'; import { ITEM_EDIT_AUTHORIZATIONS_PATH, @@ -52,7 +55,7 @@ export const ROUTES: Route[] = [ { path: '', resolve: { - breadcrumb: I18nBreadcrumbResolver, + breadcrumb: i18nBreadcrumbResolver, }, data: { breadcrumbKey: 'item.edit' }, children: [ @@ -69,31 +72,31 @@ export const ROUTES: Route[] = [ path: 'status', component: ThemedItemStatusComponent, data: { title: 'item.edit.tabs.status.title', showBreadcrumbs: true }, - canActivate: [ItemPageStatusGuard], + canActivate: mapToCanActivate([ItemPageStatusGuard]), }, { path: 'bitstreams', component: ItemBitstreamsComponent, data: { title: 'item.edit.tabs.bitstreams.title', showBreadcrumbs: true }, - canActivate: [ItemPageBitstreamsGuard], + canActivate: mapToCanActivate([ItemPageBitstreamsGuard]), }, { path: 'metadata', component: ThemedDsoEditMetadataComponent, data: { title: 'item.edit.tabs.metadata.title', showBreadcrumbs: true }, - canActivate: [ItemPageMetadataGuard], + canActivate: mapToCanActivate([ItemPageMetadataGuard]), }, { path: 'curate', component: ItemCurateComponent, data: { title: 'item.edit.tabs.curate.title', showBreadcrumbs: true }, - canActivate: [ItemPageCurateGuard], + canActivate: mapToCanActivate([ItemPageCurateGuard]), }, { path: 'relationships', component: ItemRelationshipsComponent, data: { title: 'item.edit.tabs.relationships.title', showBreadcrumbs: true }, - canActivate: [ItemPageRelationshipsGuard], + canActivate: mapToCanActivate([ItemPageRelationshipsGuard]), }, /* TODO - uncomment & fix when view page exists { @@ -111,19 +114,19 @@ export const ROUTES: Route[] = [ path: 'versionhistory', component: ItemVersionHistoryComponent, data: { title: 'item.edit.tabs.versionhistory.title', showBreadcrumbs: true }, - canActivate: [ItemPageVersionHistoryGuard], + canActivate: mapToCanActivate([ItemPageVersionHistoryGuard]), }, { path: 'access-control', component: ItemAccessControlComponent, data: { title: 'item.edit.tabs.access-control.title', showBreadcrumbs: true }, - canActivate: [ItemPageAccessControlGuard], + canActivate: mapToCanActivate([ItemPageAccessControlGuard]), }, { path: 'mapper', component: ItemCollectionMapperComponent, data: { title: 'item.edit.tabs.item-mapper.title', showBreadcrumbs: true }, - canActivate: [ItemPageCollectionMapperGuard], + canActivate: mapToCanActivate([ItemPageCollectionMapperGuard]), }, ], }, @@ -134,12 +137,12 @@ export const ROUTES: Route[] = [ { path: ITEM_EDIT_WITHDRAW_PATH, component: ItemWithdrawComponent, - canActivate: [ItemPageWithdrawGuard], + canActivate: mapToCanActivate([ItemPageWithdrawGuard]), }, { path: ITEM_EDIT_REINSTATE_PATH, component: ItemReinstateComponent, - canActivate: [ItemPageReinstateGuard], + canActivate: mapToCanActivate([ItemPageReinstateGuard]), }, { path: ITEM_EDIT_PRIVATE_PATH, @@ -161,7 +164,7 @@ export const ROUTES: Route[] = [ { path: ITEM_EDIT_REGISTER_DOI_PATH, component: ItemRegisterDoiComponent, - canActivate: [ItemPageRegisterDoiGuard], + canActivate: mapToCanActivate([ItemPageRegisterDoiGuard]), data: { title: 'item.edit.register-doi.title' }, }, { @@ -170,7 +173,7 @@ export const ROUTES: Route[] = [ { path: 'create', resolve: { - resourcePolicyTarget: ResourcePolicyTargetResolver, + resourcePolicyTarget: resourcePolicyTargetResolver, }, component: ResourcePolicyCreateComponent, data: { title: 'resource-policies.create.page.title' }, @@ -178,7 +181,7 @@ export const ROUTES: Route[] = [ { path: 'edit', resolve: { - resourcePolicy: ResourcePolicyResolver, + resourcePolicy: resourcePolicyResolver, }, component: ResourcePolicyEditComponent, data: { title: 'resource-policies.edit.page.title' }, diff --git a/src/app/item-page/edit-item-page/edit-item-page.component.spec.ts b/src/app/item-page/edit-item-page/edit-item-page.component.spec.ts index 9746c654d7..da92f53c1d 100644 --- a/src/app/item-page/edit-item-page/edit-item-page.component.spec.ts +++ b/src/app/item-page/edit-item-page/edit-item-page.component.spec.ts @@ -1,4 +1,3 @@ -/* eslint-disable max-classes-per-file */ import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA, @@ -13,9 +12,9 @@ import { By } from '@angular/platform-browser'; import { ActivatedRoute, ActivatedRouteSnapshot, + CanActivateFn, RouterModule, RouterStateSnapshot, - UrlTree, } from '@angular/router'; import { TranslateLoader, @@ -31,29 +30,31 @@ import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock'; import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils'; import { EditItemPageComponent } from './edit-item-page.component'; -describe('ItemPageComponent', () => { +describe('EditItemPageComponent', () => { let comp: EditItemPageComponent; let fixture: ComponentFixture; - class AcceptAllGuard { - canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable | Promise | boolean | UrlTree { - return observableOf(true); - } - } + const AcceptAllGuard: CanActivateFn = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + ): Observable => { + return observableOf(true); + }; - class AcceptNoneGuard { - canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable | Promise | boolean | UrlTree { - return observableOf(false); - } - } + const AcceptNoneGuard: CanActivateFn = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + ): Observable => { + return observableOf(false); + }; - const accesiblePages = ['accessible']; - const inaccesiblePages = ['inaccessible', 'inaccessibleDoubleGuard']; + const accessiblePages = ['accessible']; + const inaccessiblePages = ['inaccessible', 'inaccessibleDoubleGuard']; const mockRoute = { snapshot: { firstChild: { routeConfig: { - path: accesiblePages[0], + path: accessiblePages[0], }, }, routerState: { @@ -63,13 +64,13 @@ describe('ItemPageComponent', () => { routeConfig: { children: [ { - path: accesiblePages[0], + path: accessiblePages[0], canActivate: [AcceptAllGuard], }, { - path: inaccesiblePages[0], + path: inaccessiblePages[0], canActivate: [AcceptNoneGuard], }, { - path: inaccesiblePages[1], + path: inaccessiblePages[1], canActivate: [AcceptAllGuard, AcceptNoneGuard], }, ], @@ -77,13 +78,6 @@ describe('ItemPageComponent', () => { data: observableOf({ dso: createSuccessfulRemoteDataObject(new Item()) }), }; - const mockRouter = { - routerState: { - snapshot: undefined, - }, - events: observableOf(undefined), - }; - beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [ @@ -98,8 +92,6 @@ describe('ItemPageComponent', () => { ], providers: [ { provide: ActivatedRoute, useValue: mockRoute }, - AcceptAllGuard, - AcceptNoneGuard, ], schemas: [NO_ERRORS_SCHEMA], }).overrideComponent(EditItemPageComponent, { @@ -110,19 +102,19 @@ describe('ItemPageComponent', () => { beforeEach(waitForAsync(() => { fixture = TestBed.createComponent(EditItemPageComponent); comp = fixture.componentInstance; - spyOn((comp as any).injector, 'get').and.callFake((a) => new a()); + // spyOn((comp as any).injector, 'get').and.callFake((a) => new a()); fixture.detectChanges(); })); describe('ngOnInit', () => { it('should enable tabs that the user can activate', fakeAsync(() => { const enabledItems = fixture.debugElement.queryAll(By.css('a.nav-link')); - expect(enabledItems.length).toBe(accesiblePages.length); + expect(enabledItems.length).toBe(accessiblePages.length); })); it('should disable tabs that the user can not activate', () => { const disabledItems = fixture.debugElement.queryAll(By.css('button.nav-link.disabled')); - expect(disabledItems.length).toBe(inaccesiblePages.length); + expect(disabledItems.length).toBe(inaccessiblePages.length); }); }); }); diff --git a/src/app/item-page/edit-item-page/edit-item-page.component.ts b/src/app/item-page/edit-item-page/edit-item-page.component.ts index ee71a52a4e..cd3e1ff215 100644 --- a/src/app/item-page/edit-item-page/edit-item-page.component.ts +++ b/src/app/item-page/edit-item-page/edit-item-page.component.ts @@ -9,6 +9,7 @@ import { Component, Injector, OnInit, + runInInjectionContext, } from '@angular/core'; import { ActivatedRoute, @@ -28,7 +29,6 @@ import { import { map } from 'rxjs/operators'; import { RemoteData } from '../../core/data/remote-data'; -import { GenericConstructor } from '../../core/shared/generic-constructor'; import { Item } from '../../core/shared/item.model'; import { fadeIn, @@ -88,15 +88,10 @@ export class EditItemPageComponent implements OnInit { .map((child: Route) => { let enabled = observableOf(true); if (isNotEmpty(child.canActivate)) { - enabled = observableCombineLatest(child.canActivate.map((guardConstructor: GenericConstructor<{ - canActivate: CanActivateFn; -}>) => { - const guard: { - canActivate: CanActivateFn; -} = this.injector.get<{ - canActivate: CanActivateFn; -}>(guardConstructor); - return guard.canActivate(this.route.snapshot, this.router.routerState.snapshot); + enabled = observableCombineLatest(child.canActivate.map((guardFn: CanActivateFn) => { + return runInInjectionContext(this.injector, () => { + return guardFn(this.route.snapshot, this.router.routerState.snapshot); + }); }), ).pipe( map((canActivateOutcomes: any[]) => canActivateOutcomes.every((e) => e === true)), diff --git a/src/app/item-page/edit-item-page/item-page-access-control.guard.ts b/src/app/item-page/edit-item-page/item-page-access-control.guard.ts index 3c2cb4486f..42ee1f3d15 100644 --- a/src/app/item-page/edit-item-page/item-page-access-control.guard.ts +++ b/src/app/item-page/edit-item-page/item-page-access-control.guard.ts @@ -1,6 +1,7 @@ import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, Router, RouterStateSnapshot, } from '@angular/router'; @@ -13,8 +14,9 @@ import { AuthService } from '../../core/auth/auth.service'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; import { DsoPageSingleFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard'; import { FeatureID } from '../../core/data/feature-authorization/feature-id'; +import { RemoteData } from '../../core/data/remote-data'; import { Item } from '../../core/shared/item.model'; -import { ItemPageResolver } from '../item-page.resolver'; +import { itemPageResolver } from '../item-page.resolver'; @Injectable({ providedIn: 'root', @@ -23,11 +25,13 @@ import { ItemPageResolver } from '../item-page.resolver'; * Guard for preventing unauthorized access to certain {@link Item} pages requiring administrator rights */ export class ItemPageAccessControlGuard extends DsoPageSingleFeatureGuard { - constructor(protected resolver: ItemPageResolver, - protected authorizationService: AuthorizationDataService, + + protected resolver: ResolveFn> = itemPageResolver; + + constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) { - super(resolver, authorizationService, router, authService); + super(authorizationService, router, authService); } /** diff --git a/src/app/item-page/edit-item-page/item-page-bitstreams.guard.ts b/src/app/item-page/edit-item-page/item-page-bitstreams.guard.ts index 43eef68fdb..396064b524 100644 --- a/src/app/item-page/edit-item-page/item-page-bitstreams.guard.ts +++ b/src/app/item-page/edit-item-page/item-page-bitstreams.guard.ts @@ -1,6 +1,7 @@ import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, Router, RouterStateSnapshot, } from '@angular/router'; @@ -13,8 +14,9 @@ import { AuthService } from '../../core/auth/auth.service'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; import { DsoPageSingleFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard'; import { FeatureID } from '../../core/data/feature-authorization/feature-id'; +import { RemoteData } from '../../core/data/remote-data'; import { Item } from '../../core/shared/item.model'; -import { ItemPageResolver } from '../item-page.resolver'; +import { itemPageResolver } from '../item-page.resolver'; @Injectable({ providedIn: 'root', @@ -23,11 +25,13 @@ import { ItemPageResolver } from '../item-page.resolver'; * Guard for preventing unauthorized access to certain {@link Item} pages requiring manage bitstreams rights */ export class ItemPageBitstreamsGuard extends DsoPageSingleFeatureGuard { - constructor(protected resolver: ItemPageResolver, - protected authorizationService: AuthorizationDataService, + + protected resolver: ResolveFn> = itemPageResolver; + + constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) { - super(resolver, authorizationService, router, authService); + super( authorizationService, router, authService); } /** diff --git a/src/app/item-page/edit-item-page/item-page-collection-mapper.guard.ts b/src/app/item-page/edit-item-page/item-page-collection-mapper.guard.ts index dcd947c9aa..aee0a60371 100644 --- a/src/app/item-page/edit-item-page/item-page-collection-mapper.guard.ts +++ b/src/app/item-page/edit-item-page/item-page-collection-mapper.guard.ts @@ -1,6 +1,7 @@ import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, Router, RouterStateSnapshot, } from '@angular/router'; @@ -13,8 +14,9 @@ import { AuthService } from '../../core/auth/auth.service'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; import { DsoPageSingleFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard'; import { FeatureID } from '../../core/data/feature-authorization/feature-id'; +import { RemoteData } from '../../core/data/remote-data'; import { Item } from '../../core/shared/item.model'; -import { ItemPageResolver } from '../item-page.resolver'; +import { itemPageResolver } from '../item-page.resolver'; @Injectable({ providedIn: 'root', @@ -23,11 +25,13 @@ import { ItemPageResolver } from '../item-page.resolver'; * Guard for preventing unauthorized access to certain {@link Item} pages requiring manage mappings rights */ export class ItemPageCollectionMapperGuard extends DsoPageSingleFeatureGuard { - constructor(protected resolver: ItemPageResolver, - protected authorizationService: AuthorizationDataService, + + protected resolver: ResolveFn> = itemPageResolver; + + constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) { - super(resolver, authorizationService, router, authService); + super(authorizationService, router, authService); } /** diff --git a/src/app/item-page/edit-item-page/item-page-curate.guard.ts b/src/app/item-page/edit-item-page/item-page-curate.guard.ts index a096caf937..392bc5c523 100644 --- a/src/app/item-page/edit-item-page/item-page-curate.guard.ts +++ b/src/app/item-page/edit-item-page/item-page-curate.guard.ts @@ -1,6 +1,7 @@ import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, Router, RouterStateSnapshot, } from '@angular/router'; @@ -13,8 +14,9 @@ import { AuthService } from '../../core/auth/auth.service'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; import { DsoPageSingleFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard'; import { FeatureID } from '../../core/data/feature-authorization/feature-id'; +import { RemoteData } from '../../core/data/remote-data'; import { Item } from '../../core/shared/item.model'; -import { ItemPageResolver } from '../item-page.resolver'; +import { itemPageResolver } from '../item-page.resolver'; @Injectable({ providedIn: 'root', @@ -23,11 +25,13 @@ import { ItemPageResolver } from '../item-page.resolver'; * Guard for preventing unauthorized access to certain {@link Item} pages requiring administrator rights */ export class ItemPageCurateGuard extends DsoPageSingleFeatureGuard { - constructor(protected resolver: ItemPageResolver, - protected authorizationService: AuthorizationDataService, + + protected resolver: ResolveFn> = itemPageResolver; + + constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) { - super(resolver, authorizationService, router, authService); + super(authorizationService, router, authService); } /** diff --git a/src/app/item-page/edit-item-page/item-page-metadata.guard.ts b/src/app/item-page/edit-item-page/item-page-metadata.guard.ts index e0c49283f3..1d6d75df2a 100644 --- a/src/app/item-page/edit-item-page/item-page-metadata.guard.ts +++ b/src/app/item-page/edit-item-page/item-page-metadata.guard.ts @@ -1,6 +1,7 @@ import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, Router, RouterStateSnapshot, } from '@angular/router'; @@ -13,8 +14,9 @@ import { AuthService } from '../../core/auth/auth.service'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; import { DsoPageSingleFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard'; import { FeatureID } from '../../core/data/feature-authorization/feature-id'; +import { RemoteData } from '../../core/data/remote-data'; import { Item } from '../../core/shared/item.model'; -import { ItemPageResolver } from '../item-page.resolver'; +import { itemPageResolver } from '../item-page.resolver'; @Injectable({ providedIn: 'root', @@ -23,11 +25,13 @@ import { ItemPageResolver } from '../item-page.resolver'; * Guard for preventing unauthorized access to certain {@link Item} pages requiring edit metadata rights */ export class ItemPageMetadataGuard extends DsoPageSingleFeatureGuard { - constructor(protected resolver: ItemPageResolver, - protected authorizationService: AuthorizationDataService, + + protected resolver: ResolveFn> = itemPageResolver; + + constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) { - super(resolver, authorizationService, router, authService); + super(authorizationService, router, authService); } /** diff --git a/src/app/item-page/edit-item-page/item-page-register-doi.guard.ts b/src/app/item-page/edit-item-page/item-page-register-doi.guard.ts index 7eac0b0499..18c7939852 100644 --- a/src/app/item-page/edit-item-page/item-page-register-doi.guard.ts +++ b/src/app/item-page/edit-item-page/item-page-register-doi.guard.ts @@ -1,6 +1,7 @@ import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, Router, RouterStateSnapshot, } from '@angular/router'; @@ -13,8 +14,9 @@ import { AuthService } from '../../core/auth/auth.service'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; import { DsoPageSingleFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard'; import { FeatureID } from '../../core/data/feature-authorization/feature-id'; +import { RemoteData } from '../../core/data/remote-data'; import { Item } from '../../core/shared/item.model'; -import { ItemPageResolver } from '../item-page.resolver'; +import { itemPageResolver } from '../item-page.resolver'; @Injectable({ providedIn: 'root', @@ -23,11 +25,13 @@ import { ItemPageResolver } from '../item-page.resolver'; * Guard for preventing unauthorized access to certain {@link Item} pages requiring DOI registration rights */ export class ItemPageRegisterDoiGuard extends DsoPageSingleFeatureGuard { - constructor(protected resolver: ItemPageResolver, - protected authorizationService: AuthorizationDataService, + + protected resolver: ResolveFn> = itemPageResolver; + + constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) { - super(resolver, authorizationService, router, authService); + super(authorizationService, router, authService); } /** diff --git a/src/app/item-page/edit-item-page/item-page-reinstate.guard.ts b/src/app/item-page/edit-item-page/item-page-reinstate.guard.ts index a160301e44..aacf2f3e7d 100644 --- a/src/app/item-page/edit-item-page/item-page-reinstate.guard.ts +++ b/src/app/item-page/edit-item-page/item-page-reinstate.guard.ts @@ -1,6 +1,7 @@ import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, Router, RouterStateSnapshot, } from '@angular/router'; @@ -13,8 +14,9 @@ import { AuthService } from '../../core/auth/auth.service'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; import { DsoPageSingleFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard'; import { FeatureID } from '../../core/data/feature-authorization/feature-id'; +import { RemoteData } from '../../core/data/remote-data'; import { Item } from '../../core/shared/item.model'; -import { ItemPageResolver } from '../item-page.resolver'; +import { itemPageResolver } from '../item-page.resolver'; @Injectable({ providedIn: 'root', @@ -23,11 +25,13 @@ import { ItemPageResolver } from '../item-page.resolver'; * Guard for preventing unauthorized access to certain {@link Item} pages requiring reinstate rights */ export class ItemPageReinstateGuard extends DsoPageSingleFeatureGuard { - constructor(protected resolver: ItemPageResolver, - protected authorizationService: AuthorizationDataService, + + protected resolver: ResolveFn> = itemPageResolver; + + constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) { - super(resolver, authorizationService, router, authService); + super(authorizationService, router, authService); } /** diff --git a/src/app/item-page/edit-item-page/item-page-relationships.guard.ts b/src/app/item-page/edit-item-page/item-page-relationships.guard.ts index c6cc86a198..5d16ae8751 100644 --- a/src/app/item-page/edit-item-page/item-page-relationships.guard.ts +++ b/src/app/item-page/edit-item-page/item-page-relationships.guard.ts @@ -1,6 +1,7 @@ import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, Router, RouterStateSnapshot, } from '@angular/router'; @@ -13,8 +14,9 @@ import { AuthService } from '../../core/auth/auth.service'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; import { DsoPageSingleFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard'; import { FeatureID } from '../../core/data/feature-authorization/feature-id'; +import { RemoteData } from '../../core/data/remote-data'; import { Item } from '../../core/shared/item.model'; -import { ItemPageResolver } from '../item-page.resolver'; +import { itemPageResolver } from '../item-page.resolver'; @Injectable({ providedIn: 'root', @@ -23,11 +25,13 @@ import { ItemPageResolver } from '../item-page.resolver'; * Guard for preventing unauthorized access to certain {@link Item} pages requiring manage relationships rights */ export class ItemPageRelationshipsGuard extends DsoPageSingleFeatureGuard { - constructor(protected resolver: ItemPageResolver, - protected authorizationService: AuthorizationDataService, + + protected resolver: ResolveFn> = itemPageResolver; + + constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) { - super(resolver, authorizationService, router, authService); + super(authorizationService, router, authService); } /** diff --git a/src/app/item-page/edit-item-page/item-page-status.guard.ts b/src/app/item-page/edit-item-page/item-page-status.guard.ts index e9d3640bda..372b072e19 100644 --- a/src/app/item-page/edit-item-page/item-page-status.guard.ts +++ b/src/app/item-page/edit-item-page/item-page-status.guard.ts @@ -1,6 +1,7 @@ import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, Router, RouterStateSnapshot, } from '@angular/router'; @@ -13,8 +14,9 @@ import { AuthService } from '../../core/auth/auth.service'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; import { DsoPageSomeFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard'; import { FeatureID } from '../../core/data/feature-authorization/feature-id'; +import { RemoteData } from '../../core/data/remote-data'; import { Item } from '../../core/shared/item.model'; -import { ItemPageResolver } from '../item-page.resolver'; +import { itemPageResolver } from '../item-page.resolver'; @Injectable({ providedIn: 'root', @@ -24,11 +26,13 @@ import { ItemPageResolver } from '../item-page.resolver'; * the status page */ export class ItemPageStatusGuard extends DsoPageSomeFeatureGuard { - constructor(protected resolver: ItemPageResolver, - protected authorizationService: AuthorizationDataService, + + protected resolver: ResolveFn> = itemPageResolver; + + constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) { - super(resolver, authorizationService, router, authService); + super(authorizationService, router, authService); } /** diff --git a/src/app/item-page/edit-item-page/item-page-version-history.guard.ts b/src/app/item-page/edit-item-page/item-page-version-history.guard.ts index 864cca9c5a..dc512527ad 100644 --- a/src/app/item-page/edit-item-page/item-page-version-history.guard.ts +++ b/src/app/item-page/edit-item-page/item-page-version-history.guard.ts @@ -1,6 +1,7 @@ import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, Router, RouterStateSnapshot, } from '@angular/router'; @@ -13,8 +14,9 @@ import { AuthService } from '../../core/auth/auth.service'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; import { DsoPageSingleFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard'; import { FeatureID } from '../../core/data/feature-authorization/feature-id'; +import { RemoteData } from '../../core/data/remote-data'; import { Item } from '../../core/shared/item.model'; -import { ItemPageResolver } from '../item-page.resolver'; +import { itemPageResolver } from '../item-page.resolver'; @Injectable({ providedIn: 'root', @@ -23,11 +25,13 @@ import { ItemPageResolver } from '../item-page.resolver'; * Guard for preventing unauthorized access to certain {@link Item} pages requiring manage versions rights */ export class ItemPageVersionHistoryGuard extends DsoPageSingleFeatureGuard { - constructor(protected resolver: ItemPageResolver, - protected authorizationService: AuthorizationDataService, + + protected resolver: ResolveFn> = itemPageResolver; + + constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) { - super(resolver, authorizationService, router, authService); + super(authorizationService, router, authService); } /** diff --git a/src/app/item-page/edit-item-page/item-page-withdraw.guard.ts b/src/app/item-page/edit-item-page/item-page-withdraw.guard.ts index 8370cbed16..464386edfc 100644 --- a/src/app/item-page/edit-item-page/item-page-withdraw.guard.ts +++ b/src/app/item-page/edit-item-page/item-page-withdraw.guard.ts @@ -1,6 +1,7 @@ import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, Router, RouterStateSnapshot, } from '@angular/router'; @@ -13,8 +14,9 @@ import { AuthService } from '../../core/auth/auth.service'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; import { DsoPageSingleFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard'; import { FeatureID } from '../../core/data/feature-authorization/feature-id'; +import { RemoteData } from '../../core/data/remote-data'; import { Item } from '../../core/shared/item.model'; -import { ItemPageResolver } from '../item-page.resolver'; +import { itemPageResolver } from '../item-page.resolver'; @Injectable({ providedIn: 'root', @@ -23,11 +25,13 @@ import { ItemPageResolver } from '../item-page.resolver'; * Guard for preventing unauthorized access to certain {@link Item} pages requiring withdraw rights */ export class ItemPageWithdrawGuard extends DsoPageSingleFeatureGuard { - constructor(protected resolver: ItemPageResolver, - protected authorizationService: AuthorizationDataService, + + protected resolver: ResolveFn> = itemPageResolver; + + constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) { - super(resolver, authorizationService, router, authService); + super(authorizationService, router, authService); } /** diff --git a/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts b/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts index 394eb4999b..f5a5846e5a 100644 --- a/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts +++ b/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts @@ -38,6 +38,7 @@ import { ItemType } from '../../../../core/shared/item-relationships/item-type.m import { Relationship } from '../../../../core/shared/item-relationships/relationship.model'; import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model'; import { SearchConfigurationService } from '../../../../core/shared/search/search-configuration.service'; +import { XSRFService } from '../../../../core/xsrf/xsrf.service'; import { HostWindowService } from '../../../../shared/host-window.service'; import { RouterMock } from '../../../../shared/mocks/router.mock'; import { SelectableListService } from '../../../../shared/object-list/selectable-list/selectable-list.service'; @@ -257,6 +258,7 @@ describe('EditRelationshipListComponent', () => { { provide: ActivatedRoute, useValue: new ActivatedRouteStub() }, { provide: AuthRequestService, useValue: new AuthRequestServiceStub() }, { provide: HardRedirectService, useValue: hardRedirectService }, + { provide: XSRFService, useValue: {} }, { provide: APP_CONFIG, useValue: environmentUseThumbs }, { provide: REQUEST, useValue: {} }, CookieService, diff --git a/src/app/item-page/item-page-administrator.guard.ts b/src/app/item-page/item-page-administrator.guard.ts index 905516195d..117ebf5899 100644 --- a/src/app/item-page/item-page-administrator.guard.ts +++ b/src/app/item-page/item-page-administrator.guard.ts @@ -1,6 +1,7 @@ import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, Router, RouterStateSnapshot, } from '@angular/router'; @@ -13,8 +14,9 @@ import { AuthService } from '../core/auth/auth.service'; import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service'; import { DsoPageSingleFeatureGuard } from '../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard'; import { FeatureID } from '../core/data/feature-authorization/feature-id'; +import { RemoteData } from '../core/data/remote-data'; import { Item } from '../core/shared/item.model'; -import { ItemPageResolver } from './item-page.resolver'; +import { itemPageResolver } from './item-page.resolver'; @Injectable({ providedIn: 'root', @@ -23,11 +25,13 @@ import { ItemPageResolver } from './item-page.resolver'; * Guard for preventing unauthorized access to certain {@link Item} pages requiring administrator rights */ export class ItemPageAdministratorGuard extends DsoPageSingleFeatureGuard { - constructor(protected resolver: ItemPageResolver, - protected authorizationService: AuthorizationDataService, + + protected resolver: ResolveFn> = itemPageResolver; + + constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) { - super(resolver, authorizationService, router, authService); + super(authorizationService, router, authService); } /** diff --git a/src/app/item-page/item-page-routes.ts b/src/app/item-page/item-page-routes.ts index ead9d40a08..57aa70336d 100644 --- a/src/app/item-page/item-page-routes.ts +++ b/src/app/item-page/item-page-routes.ts @@ -1,15 +1,18 @@ -import { Route } from '@angular/router'; +import { + mapToCanActivate, + Route, +} from '@angular/router'; import { REQUEST_COPY_MODULE_PATH } from '../app-routing-paths'; -import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; -import { ItemBreadcrumbResolver } from '../core/breadcrumbs/item-breadcrumb.resolver'; -import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver'; +import { authenticatedGuard } from '../core/auth/authenticated.guard'; +import { itemBreadcrumbResolver } from '../core/breadcrumbs/item-breadcrumb.resolver'; +import { dsoEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver'; import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model'; import { MenuItemType } from '../shared/menu/menu-item-type.model'; import { BitstreamRequestACopyPageComponent } from './bitstreams/request-a-copy/bitstream-request-a-copy-page.component'; import { UploadBitstreamComponent } from './bitstreams/upload/upload-bitstream.component'; import { ThemedFullItemPageComponent } from './full/themed-full-item-page.component'; -import { ItemPageResolver } from './item-page.resolver'; +import { itemPageResolver } from './item-page.resolver'; import { ITEM_EDIT_PATH, ORCID_PATH, @@ -18,16 +21,16 @@ import { import { OrcidPageComponent } from './orcid-page/orcid-page.component'; import { OrcidPageGuard } from './orcid-page/orcid-page.guard'; import { ThemedItemPageComponent } from './simple/themed-item-page.component'; -import { VersionResolver } from './version-page/version.resolver'; +import { versionResolver } from './version-page/version.resolver'; import { VersionPageComponent } from './version-page/version-page/version-page.component'; export const ROUTES: Route[] = [ { path: ':id', resolve: { - dso: ItemPageResolver, - breadcrumb: ItemBreadcrumbResolver, - menu: DSOEditMenuResolver, + dso: itemPageResolver, + breadcrumb: itemBreadcrumbResolver, + menu: dsoEditMenuResolver, }, runGuardsAndResolvers: 'always', children: [ @@ -48,7 +51,7 @@ export const ROUTES: Route[] = [ { path: UPLOAD_BITSTREAM_PATH, component: UploadBitstreamComponent, - canActivate: [AuthenticatedGuard], + canActivate: [authenticatedGuard], }, { path: REQUEST_COPY_MODULE_PATH, @@ -57,7 +60,7 @@ export const ROUTES: Route[] = [ { path: ORCID_PATH, component: OrcidPageComponent, - canActivate: [AuthenticatedGuard, OrcidPageGuard], + canActivate: [authenticatedGuard, ...mapToCanActivate([OrcidPageGuard])], }, ], data: { @@ -83,7 +86,7 @@ export const ROUTES: Route[] = [ path: ':id', component: VersionPageComponent, resolve: { - dso: VersionResolver, + dso: versionResolver, }, }, ], diff --git a/src/app/item-page/item-page.resolver.spec.ts b/src/app/item-page/item-page.resolver.spec.ts index 76b7dae774..43b111211b 100644 --- a/src/app/item-page/item-page.resolver.spec.ts +++ b/src/app/item-page/item-page.resolver.spec.ts @@ -6,9 +6,9 @@ import { first } from 'rxjs/operators'; import { DSpaceObject } from '../core/shared/dspace-object.model'; import { MetadataValueFilter } from '../core/shared/metadata.models'; import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; -import { ItemPageResolver } from './item-page.resolver'; +import { itemPageResolver } from './item-page.resolver'; -describe('ItemPageResolver', () => { +describe('itemPageResolver', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [RouterTestingModule.withRoutes([{ @@ -19,7 +19,7 @@ describe('ItemPageResolver', () => { }); describe('resolve', () => { - let resolver: ItemPageResolver; + let resolver: any; let itemService: any; let store: any; let router: any; @@ -42,15 +42,19 @@ describe('ItemPageResolver', () => { store = jasmine.createSpyObj('store', { dispatch: {}, }); - resolver = new ItemPageResolver(itemService, store, router); + resolver = itemPageResolver; }); it('should redirect to the correct route for the entity type', (done) => { spyOn(item, 'firstMetadataValue').and.returnValue(entityType); spyOn(router, 'navigateByUrl').and.callThrough(); - resolver.resolve({ params: { id: uuid } } as any, { url: router.parseUrl(`/items/${uuid}`).toString() } as any) - .pipe(first()) + resolver({ params: { id: uuid } } as any, + { url: router.parseUrl(`/items/${uuid}`).toString() } as any, + router, + itemService, + store, + ).pipe(first()) .subscribe( () => { expect(router.navigateByUrl).toHaveBeenCalledWith(router.parseUrl(`/entities/${entityType}/${uuid}`).toString()); @@ -63,8 +67,13 @@ describe('ItemPageResolver', () => { spyOn(item, 'firstMetadataValue').and.returnValue(entityType); spyOn(router, 'navigateByUrl').and.callThrough(); - resolver.resolve({ params: { id: uuid } } as any, { url: router.parseUrl(`/entities/${entityType}/${uuid}`).toString() } as any) - .pipe(first()) + resolver( + { params: { id: uuid } } as any, + { url: router.parseUrl(`/entities/${entityType}/${uuid}`).toString() } as any, + router, + itemService, + store, + ).pipe(first()) .subscribe( () => { expect(router.navigateByUrl).not.toHaveBeenCalled(); diff --git a/src/app/item-page/item-page.resolver.ts b/src/app/item-page/item-page.resolver.ts index 5e5bc3cc55..b4556886cc 100644 --- a/src/app/item-page/item-page.resolver.ts +++ b/src/app/item-page/item-page.resolver.ts @@ -1,6 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, Router, RouterStateSnapshot, } from '@angular/router'; @@ -8,54 +9,64 @@ import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; +import { AppState } from '../app.reducer'; import { ItemDataService } from '../core/data/item-data.service'; import { RemoteData } from '../core/data/remote-data'; +import { ResolvedAction } from '../core/resolving/resolver.actions'; import { Item } from '../core/shared/item.model'; +import { getFirstCompletedRemoteData } from '../core/shared/operators'; import { hasValue } from '../shared/empty.util'; -import { ItemResolver } from './item.resolver'; +import { ITEM_PAGE_LINKS_TO_FOLLOW } from './item.resolver'; import { getItemPageRoute } from './item-page-routing-paths'; /** - * This class represents a resolver that requests a specific item before the route is activated and will redirect to the - * entity page + * Method for resolving an item based on the parameters in the current route + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @param {Router} router + * @param {ItemDataService} itemService + * @param {Store} store + * @returns Observable<> Emits the found item based on the parameters in the current route, + * or an error if something went wrong */ -@Injectable({ providedIn: 'root' }) -export class ItemPageResolver extends ItemResolver { - constructor( - protected itemService: ItemDataService, - protected store: Store, - protected router: Router, - ) { - super(itemService, store, router); - } +export const itemPageResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + router: Router = inject(Router), + itemService: ItemDataService = inject(ItemDataService), + store: Store = inject(Store), +): Observable> => { + const itemRD$ = itemService.findById( + route.params.id, + true, + false, + ...ITEM_PAGE_LINKS_TO_FOLLOW, + ).pipe( + getFirstCompletedRemoteData(), + ); - /** - * Method for resolving an item based on the parameters in the current route - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - * @param {RouterStateSnapshot} state The current RouterStateSnapshot - * @returns Observable<> Emits the found item based on the parameters in the current route, - * or an error if something went wrong - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - return super.resolve(route, state).pipe( - map((rd: RemoteData) => { - if (rd.hasSucceeded && hasValue(rd.payload)) { - const thisRoute = state.url; + itemRD$.subscribe((itemRD: RemoteData) => { + store.dispatch(new ResolvedAction(state.url, itemRD.payload)); + }); - // Angular uses a custom function for encodeURIComponent, (e.g. it doesn't encode commas - // or semicolons) and thisRoute has been encoded with that function. If we want to compare - // it with itemRoute, we have to run itemRoute through Angular's version as well to ensure - // the same characters are encoded the same way. - const itemRoute = this.router.parseUrl(getItemPageRoute(rd.payload)).toString(); + return itemRD$.pipe( + map((rd: RemoteData) => { + if (rd.hasSucceeded && hasValue(rd.payload)) { + const thisRoute = state.url; - if (!thisRoute.startsWith(itemRoute)) { - const itemId = rd.payload.uuid; - const subRoute = thisRoute.substring(thisRoute.indexOf(itemId) + itemId.length, thisRoute.length); - this.router.navigateByUrl(itemRoute + subRoute); - } + // Angular uses a custom function for encodeURIComponent, (e.g. it doesn't encode commas + // or semicolons) and thisRoute has been encoded with that function. If we want to compare + // it with itemRoute, we have to run itemRoute through Angular's version as well to ensure + // the same characters are encoded the same way. + const itemRoute = router.parseUrl(getItemPageRoute(rd.payload)).toString(); + + if (!thisRoute.startsWith(itemRoute)) { + const itemId = rd.payload.uuid; + const subRoute = thisRoute.substring(thisRoute.indexOf(itemId) + itemId.length, thisRoute.length); + router.navigateByUrl(itemRoute + subRoute); } - return rd; - }), - ); - } -} + } + return rd; + }), + ); +}; diff --git a/src/app/item-page/item.resolver.ts b/src/app/item-page/item.resolver.ts index 385ba81dc4..343e0d1983 100644 --- a/src/app/item-page/item.resolver.ts +++ b/src/app/item-page/item.resolver.ts @@ -1,12 +1,13 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, - Router, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; +import { AppState } from '../app.reducer'; import { ItemDataService } from '../core/data/item-data.service'; import { RemoteData } from '../core/data/remote-data'; import { ResolvedAction } from '../core/resolving/resolver.actions'; @@ -31,38 +32,24 @@ export const ITEM_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig[] = [ followLink('thumbnail'), ]; -/** - * This class represents a resolver that requests a specific item before the route is activated - */ -@Injectable({ providedIn: 'root' }) -export class ItemResolver { - constructor( - protected itemService: ItemDataService, - protected store: Store, - protected router: Router, - ) { - } +export const itemResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + itemService: ItemDataService = inject(ItemDataService), + store: Store = inject(Store), +): Observable> => { + const itemRD$ = itemService.findById( + route.params.id, + true, + false, + ...ITEM_PAGE_LINKS_TO_FOLLOW, + ).pipe( + getFirstCompletedRemoteData(), + ); - /** - * Method for resolving an item based on the parameters in the current route - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - * @param {RouterStateSnapshot} state The current RouterStateSnapshot - * @returns Observable<> Emits the found item based on the parameters in the current route, - * or an error if something went wrong - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - const itemRD$ = this.itemService.findById(route.params.id, - true, - false, - ...ITEM_PAGE_LINKS_TO_FOLLOW, - ).pipe( - getFirstCompletedRemoteData(), - ); + itemRD$.subscribe((itemRD: RemoteData) => { + store.dispatch(new ResolvedAction(state.url, itemRD.payload)); + }); - itemRD$.subscribe((itemRD: RemoteData) => { - this.store.dispatch(new ResolvedAction(state.url, itemRD.payload)); - }); - - return itemRD$; - } -} + return itemRD$; +}; diff --git a/src/app/item-page/orcid-page/orcid-page.guard.ts b/src/app/item-page/orcid-page/orcid-page.guard.ts index ee08ac918e..44c74c684e 100644 --- a/src/app/item-page/orcid-page/orcid-page.guard.ts +++ b/src/app/item-page/orcid-page/orcid-page.guard.ts @@ -1,6 +1,7 @@ import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, Router, RouterStateSnapshot, } from '@angular/router'; @@ -13,8 +14,9 @@ import { AuthService } from '../../core/auth/auth.service'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; import { DsoPageSingleFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard'; import { FeatureID } from '../../core/data/feature-authorization/feature-id'; +import { RemoteData } from '../../core/data/remote-data'; import { Item } from '../../core/shared/item.model'; -import { ItemPageResolver } from '../item-page.resolver'; +import { itemPageResolver } from '../item-page.resolver'; @Injectable({ providedIn: 'root', @@ -23,11 +25,13 @@ import { ItemPageResolver } from '../item-page.resolver'; * Guard for preventing unauthorized access to certain {@link Item} pages requiring administrator rights */ export class OrcidPageGuard extends DsoPageSingleFeatureGuard { - constructor(protected resolver: ItemPageResolver, - protected authorizationService: AuthorizationDataService, + + protected resolver: ResolveFn> = itemPageResolver; + + constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) { - super(resolver, authorizationService, router, authService); + super(authorizationService, router, authService); } /** diff --git a/src/app/item-page/simple/field-components/file-section/file-section.component.spec.ts b/src/app/item-page/simple/field-components/file-section/file-section.component.spec.ts index 91eb255afd..b2fb2bf29f 100644 --- a/src/app/item-page/simple/field-components/file-section/file-section.component.spec.ts +++ b/src/app/item-page/simple/field-components/file-section/file-section.component.spec.ts @@ -22,6 +22,7 @@ import { import { BitstreamDataService } from '../../../../core/data/bitstream-data.service'; import { Bitstream } from '../../../../core/shared/bitstream.model'; import { PageInfo } from '../../../../core/shared/page-info.model'; +import { XSRFService } from '../../../../core/xsrf/xsrf.service'; import { MetadataFieldWrapperComponent } from '../../../../shared/metadata-field-wrapper/metadata-field-wrapper.component'; import { MockBitstreamFormat1 } from '../../../../shared/mocks/item.mock'; import { getMockThemeService } from '../../../../shared/mocks/theme-service.mock'; @@ -83,6 +84,7 @@ describe('FileSectionComponent', () => { }), BrowserAnimationsModule, FileSectionComponent, VarDirective, FileSizePipe], providers: [ { provide: APP_DATA_SERVICES_MAP, useValue: {} }, + { provide: XSRFService, useValue: {} }, { provide: BitstreamDataService, useValue: bitstreamDataService }, { provide: NotificationsService, useValue: new NotificationsServiceStub() }, { provide: APP_CONFIG, useValue: environment }, diff --git a/src/app/item-page/version-page/version.resolver.ts b/src/app/item-page/version-page/version.resolver.ts index 1f37f2ae67..0364d4ac0d 100644 --- a/src/app/item-page/version-page/version.resolver.ts +++ b/src/app/item-page/version-page/version.resolver.ts @@ -1,12 +1,13 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, - Router, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; +import { AppState } from '../../app.reducer'; import { RemoteData } from '../../core/data/remote-data'; import { VersionDataService } from '../../core/data/version-data.service'; import { ResolvedAction } from '../../core/resolving/resolver.actions'; @@ -26,37 +27,31 @@ export const VERSION_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig[] = [ ]; /** - * This class represents a resolver that requests a specific version before the route is activated + * Method for resolving a version based on the parameters in the current route + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @param {VersionDataService} versionService + * @param {Store} store + * @returns Observable<> Emits the found item based on the parameters in the current route, + * or an error if something went wrong */ -@Injectable({ providedIn: 'root' }) -export class VersionResolver { - constructor( - protected versionService: VersionDataService, - protected store: Store, - protected router: Router, - ) { - } +export const versionResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + versionService: VersionDataService = inject(VersionDataService), + store: Store = inject(Store), +): Observable> => { + const versionRD$ = versionService.findById(route.params.id, + true, + false, + ...VERSION_PAGE_LINKS_TO_FOLLOW, + ).pipe( + getFirstCompletedRemoteData(), + ); - /** - * Method for resolving a version based on the parameters in the current route - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - * @param {RouterStateSnapshot} state The current RouterStateSnapshot - * @returns Observable<> Emits the found item based on the parameters in the current route, - * or an error if something went wrong - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - const versionRD$ = this.versionService.findById(route.params.id, - true, - false, - ...VERSION_PAGE_LINKS_TO_FOLLOW, - ).pipe( - getFirstCompletedRemoteData(), - ); + versionRD$.subscribe((versionRD: RemoteData) => { + store.dispatch(new ResolvedAction(state.url, versionRD.payload)); + }); - versionRD$.subscribe((versionRD: RemoteData) => { - this.store.dispatch(new ResolvedAction(state.url, versionRD.payload)); - }); - - return versionRD$; - } -} + return versionRD$; +}; diff --git a/src/app/login-page/login-page-routes.ts b/src/app/login-page/login-page-routes.ts index 4833a80076..661c9f9858 100644 --- a/src/app/login-page/login-page-routes.ts +++ b/src/app/login-page/login-page-routes.ts @@ -1,6 +1,6 @@ import { Route } from '@angular/router'; -import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; +import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; import { ThemedLoginPageComponent } from './themed-login-page.component'; export const ROUTES: Route[] = [ @@ -8,7 +8,7 @@ export const ROUTES: Route[] = [ path: '', pathMatch: 'full', component: ThemedLoginPageComponent, - resolve: { breadcrumb: I18nBreadcrumbResolver }, + resolve: { breadcrumb: i18nBreadcrumbResolver }, data: { breadcrumbKey: 'login', title: 'login.title' }, }, ]; diff --git a/src/app/login-page/login-page.component.spec.ts b/src/app/login-page/login-page.component.spec.ts index 74aeddfe0c..6cb4098c4d 100644 --- a/src/app/login-page/login-page.component.spec.ts +++ b/src/app/login-page/login-page.component.spec.ts @@ -12,6 +12,7 @@ import { of as observableOf } from 'rxjs'; import { APP_DATA_SERVICES_MAP } from '../../config/app-config.interface'; import { AuthService } from '../core/auth/auth.service'; +import { XSRFService } from '../core/xsrf/xsrf.service'; import { AuthServiceMock } from '../shared/mocks/auth.service.mock'; import { ActivatedRouteStub } from '../shared/testing/active-router.stub'; import { LoginPageComponent } from './login-page.component'; @@ -39,6 +40,7 @@ describe('LoginPageComponent', () => { providers: [ { provide: ActivatedRoute, useValue: activatedRouteStub }, { provide: AuthService, useValue: new AuthServiceMock() }, + { provide: XSRFService, useValue: {} }, { provide: APP_DATA_SERVICES_MAP, useValue: {} }, provideMockStore({}), ], diff --git a/src/app/logout-page/logout-page-routes.ts b/src/app/logout-page/logout-page-routes.ts index 5a697f52a0..ad6478d642 100644 --- a/src/app/logout-page/logout-page-routes.ts +++ b/src/app/logout-page/logout-page-routes.ts @@ -1,11 +1,11 @@ import { Route } from '@angular/router'; -import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; +import { authenticatedGuard } from '../core/auth/authenticated.guard'; import { ThemedLogoutPageComponent } from './themed-logout-page.component'; export const ROUTES: Route[] = [ { - canActivate: [AuthenticatedGuard], + canActivate: [authenticatedGuard], path: '', component: ThemedLogoutPageComponent, data: { title: 'logout.title' }, diff --git a/src/app/lookup-by-id/lookup-by-id-routes.ts b/src/app/lookup-by-id/lookup-by-id-routes.ts index 8d77b3aa07..b19780d828 100644 --- a/src/app/lookup-by-id/lookup-by-id-routes.ts +++ b/src/app/lookup-by-id/lookup-by-id-routes.ts @@ -4,13 +4,13 @@ import { } from '@angular/router'; import { isNotEmpty } from '../shared/empty.util'; -import { LookupGuard } from './lookup-guard'; +import { lookupGuard } from './lookup-guard'; import { ThemedObjectNotFoundComponent } from './objectnotfound/themed-objectnotfound.component'; export const ROUTES: Route[] = [ { matcher: urlMatcher, - canActivate: [LookupGuard], + canActivate: [lookupGuard], component: ThemedObjectNotFoundComponent, }, ]; diff --git a/src/app/lookup-by-id/lookup-guard.spec.ts b/src/app/lookup-by-id/lookup-guard.spec.ts index 1380f0c689..90a7e62738 100644 --- a/src/app/lookup-by-id/lookup-guard.spec.ts +++ b/src/app/lookup-by-id/lookup-guard.spec.ts @@ -1,9 +1,9 @@ import { of as observableOf } from 'rxjs'; import { IdentifierType } from '../core/data/request.models'; -import { LookupGuard } from './lookup-guard'; +import { lookupGuard } from './lookup-guard'; -describe('LookupGuard', () => { +describe('lookupGuard', () => { let dsoService: any; let guard: any; @@ -12,7 +12,7 @@ describe('LookupGuard', () => { findByIdAndIDType: jasmine.createSpy('findByIdAndIDType').and.returnValue(observableOf({ hasFailed: false, hasSucceeded: true })), }; - guard = new LookupGuard(dsoService); + guard = lookupGuard; }); it('should call findByIdAndIDType with handle params', () => { @@ -22,7 +22,7 @@ describe('LookupGuard', () => { idType: '123456789', }, }; - guard.canActivate(scopedRoute as any, undefined); + guard(scopedRoute as any, undefined, dsoService); expect(dsoService.findByIdAndIDType).toHaveBeenCalledWith('hdl:123456789/1234', IdentifierType.HANDLE); }); @@ -33,7 +33,7 @@ describe('LookupGuard', () => { idType: 'handle', }, }; - guard.canActivate(scopedRoute as any, undefined); + guard(scopedRoute as any, undefined, dsoService); expect(dsoService.findByIdAndIDType).toHaveBeenCalledWith('hdl:123456789%2F1234', IdentifierType.HANDLE); }); @@ -44,7 +44,7 @@ describe('LookupGuard', () => { idType: 'uuid', }, }; - guard.canActivate(scopedRoute as any, undefined); + guard(scopedRoute as any, undefined, dsoService); expect(dsoService.findByIdAndIDType).toHaveBeenCalledWith('34cfed7c-f597-49ef-9cbe-ea351f0023c2', IdentifierType.UUID); }); diff --git a/src/app/lookup-by-id/lookup-guard.ts b/src/app/lookup-by-id/lookup-guard.ts index b1bb92e633..25813d28ff 100644 --- a/src/app/lookup-by-id/lookup-guard.ts +++ b/src/app/lookup-by-id/lookup-guard.ts @@ -1,6 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, + CanActivateFn, RouterStateSnapshot, } from '@angular/router'; import { Observable } from 'rxjs'; @@ -16,44 +17,39 @@ interface LookupParams { id: string; } -@Injectable({ - providedIn: 'root', -}) -export class LookupGuard { +export const lookupGuard: CanActivateFn = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + dsoService: DsoRedirectService = inject(DsoRedirectService), +): Observable => { + const params = getLookupParams(route); + return dsoService.findByIdAndIDType(params.id, params.type).pipe( + map((response: RemoteData) => response.hasFailed), + ); +}; - constructor(private dsoService: DsoRedirectService) { - } - - canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - const params = this.getLookupParams(route); - return this.dsoService.findByIdAndIDType(params.id, params.type).pipe( - map((response: RemoteData) => response.hasFailed), - ); - } - - private getLookupParams(route: ActivatedRouteSnapshot): LookupParams { - let type; - let id; - const idType = route.params.idType; - - // If the idType is not recognized, assume a legacy handle request (handle/prefix/id) - if (idType !== IdentifierType.HANDLE && idType !== IdentifierType.UUID) { - type = IdentifierType.HANDLE; - const prefix = route.params.idType; - const handleId = route.params.id; - id = `hdl:${prefix}/${handleId}`; - - } else if (route.params.idType === IdentifierType.HANDLE) { - type = IdentifierType.HANDLE; - id = 'hdl:' + route.params.id; - - } else { - type = IdentifierType.UUID; - id = route.params.id; - } - return { - type: type, - id: id, - }; +function getLookupParams(route: ActivatedRouteSnapshot): LookupParams { + let type; + let id; + const idType = route.params.idType; + + // If the idType is not recognized, assume a legacy handle request (handle/prefix/id) + if (idType !== IdentifierType.HANDLE && idType !== IdentifierType.UUID) { + type = IdentifierType.HANDLE; + const prefix = route.params.idType; + const handleId = route.params.id; + id = `hdl:${prefix}/${handleId}`; + + } else if (route.params.idType === IdentifierType.HANDLE) { + type = IdentifierType.HANDLE; + id = 'hdl:' + route.params.id; + + } else { + type = IdentifierType.UUID; + id = route.params.id; } + return { + type: type, + id: id, + }; } diff --git a/src/app/menu.resolver.ts b/src/app/menu-resolver.service.ts similarity index 99% rename from src/app/menu.resolver.ts rename to src/app/menu-resolver.service.ts index 2be5c3d3d5..144cad4547 100644 --- a/src/app/menu.resolver.ts +++ b/src/app/menu-resolver.service.ts @@ -54,7 +54,7 @@ import { MenuState } from './shared/menu/menu-state.model'; @Injectable({ providedIn: 'root', }) -export class MenuResolver { +export class MenuResolverService { constructor( protected menuService: MenuService, protected browseService: BrowseService, diff --git a/src/app/menu.resolver.spec.ts b/src/app/menu.resolver.spec.ts index 0a1a050637..386bbb0cae 100644 --- a/src/app/menu.resolver.spec.ts +++ b/src/app/menu.resolver.spec.ts @@ -19,7 +19,6 @@ import { ConfigurationDataService } from './core/data/configuration-data.service import { AuthorizationDataService } from './core/data/feature-authorization/authorization-data.service'; import { FeatureID } from './core/data/feature-authorization/feature-id'; import { ScriptDataService } from './core/data/processes/script-data.service'; -import { MenuResolver } from './menu.resolver'; import { MenuService } from './shared/menu/menu.service'; import { MenuID } from './shared/menu/menu-id.model'; import { createSuccessfulRemoteDataObject$ } from './shared/remote-data.utils'; @@ -27,6 +26,7 @@ import { ConfigurationDataServiceStub } from './shared/testing/configuration-dat import { MenuServiceStub } from './shared/testing/menu-service.stub'; import { createPaginatedList } from './shared/testing/utils.test'; import createSpy = jasmine.createSpy; +import { MenuResolverService } from './menu-resolver.service'; const BOOLEAN = { t: true, f: false }; const MENU_STATE = { @@ -38,8 +38,8 @@ const BROWSE_DEFINITIONS = [ { id: 'definition3' }, ]; -describe('MenuResolver', () => { - let resolver: MenuResolver; +describe('menuResolver', () => { + let resolver: MenuResolverService; let menuService; let browseService; @@ -79,12 +79,12 @@ describe('MenuResolver', () => { { provide: AuthorizationDataService, useValue: authorizationService }, { provide: ScriptDataService, useValue: scriptService }, { provide: ConfigurationDataService, useValue: configurationDataService }, - { - provide: NgbModal, useValue: mockNgbModal }, + { provide: NgbModal, useValue: mockNgbModal }, + MenuResolverService, ], schemas: [NO_ERRORS_SCHEMA], }); - resolver = TestBed.inject(MenuResolver); + resolver = TestBed.inject(MenuResolverService); })); it('should be created', () => { diff --git a/src/app/menuResolver.ts b/src/app/menuResolver.ts new file mode 100644 index 0000000000..68ad4494dd --- /dev/null +++ b/src/app/menuResolver.ts @@ -0,0 +1,21 @@ +import { inject } from '@angular/core'; +import { + ActivatedRouteSnapshot, + ResolveFn, + RouterStateSnapshot, +} from '@angular/router'; +import { Observable } from 'rxjs'; + +import { MenuResolverService } from './menu-resolver.service'; + + +/** + * Initialize all menus + */ +export const menuResolver: ResolveFn = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + menuResolverService: MenuResolverService = inject(MenuResolverService), +): Observable => { + return menuResolverService.resolve(route, state); +}; diff --git a/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-external-dropdown/my-dspace-new-external-dropdown.component.ts b/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-external-dropdown/my-dspace-new-external-dropdown.component.ts index 54830f4198..86960725a5 100644 --- a/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-external-dropdown/my-dspace-new-external-dropdown.component.ts +++ b/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-external-dropdown/my-dspace-new-external-dropdown.component.ts @@ -11,6 +11,7 @@ import { Router } from '@angular/router'; import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; import { TranslateModule } from '@ngx-translate/core'; import { + BehaviorSubject, Observable, of as observableOf, Subscription, @@ -67,7 +68,7 @@ export class MyDSpaceNewExternalDropdownComponent implements OnInit, OnDestroy { /** * TRUE if the page is initialized */ - public initialized$: Observable; + public initialized$: BehaviorSubject = new BehaviorSubject(false); /** * Array to track all subscriptions and unsubscribe them onDestroy @@ -88,7 +89,6 @@ export class MyDSpaceNewExternalDropdownComponent implements OnInit, OnDestroy { * Initialize entity type list */ ngOnInit() { - this.initialized$ = observableOf(false); this.moreThanOne$ = this.entityTypeService.hasMoreThanOneAuthorizedImport(); this.singleEntity$ = this.moreThanOne$.pipe( mergeMap((response: boolean) => { @@ -98,21 +98,23 @@ export class MyDSpaceNewExternalDropdownComponent implements OnInit, OnDestroy { currentPage: 1, }; return this.entityTypeService.getAllAuthorizedRelationshipTypeImport(findListOptions).pipe( - map((entities: RemoteData>) => { - this.initialized$ = observableOf(true); - return entities.payload.page[0]; - }), take(1), + map((entities: RemoteData>) => { + this.initialized$.next(true); + return entities?.payload?.page[0]; + }), ); } else { - this.initialized$ = observableOf(true); + this.initialized$.next(true); return observableOf(null); } }), take(1), ); this.subs.push( - this.singleEntity$.subscribe((result) => this.singleEntity = result ), + this.singleEntity$.subscribe((result) => { + this.singleEntity = result; + } ), ); } diff --git a/src/app/my-dspace-page/my-dspace-page-routes.ts b/src/app/my-dspace-page/my-dspace-page-routes.ts index 706385563f..1e92bd7bb3 100644 --- a/src/app/my-dspace-page/my-dspace-page-routes.ts +++ b/src/app/my-dspace-page/my-dspace-page-routes.ts @@ -1,7 +1,7 @@ import { Route } from '@angular/router'; -import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; -import { MyDSpaceGuard } from './my-dspace.guard'; +import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; +import { myDSpaceGuard } from './my-dspace.guard'; import { ThemedMyDSpacePageComponent } from './themed-my-dspace-page.component'; export const ROUTES: Route[] = [ @@ -9,11 +9,11 @@ export const ROUTES: Route[] = [ path: '', component: ThemedMyDSpacePageComponent, resolve: { - breadcrumb: I18nBreadcrumbResolver, + breadcrumb: i18nBreadcrumbResolver, }, data: { title: 'mydspace.title', breadcrumbKey: 'mydspace' }, canActivate: [ - MyDSpaceGuard, + myDSpaceGuard, ], }, ]; diff --git a/src/app/my-dspace-page/my-dspace.guard.ts b/src/app/my-dspace-page/my-dspace.guard.ts index 8362bdd7e1..c174c9594e 100644 --- a/src/app/my-dspace-page/my-dspace.guard.ts +++ b/src/app/my-dspace-page/my-dspace.guard.ts @@ -1,6 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, + CanActivateFn, NavigationExtras, Router, RouterStateSnapshot, @@ -18,48 +19,40 @@ import { MYDSPACE_ROUTE } from './my-dspace-page.component'; /** * Prevent unauthorized activating and loading of mydspace configuration - * @class MyDSpaceGuard */ -@Injectable({ providedIn: 'root' }) -export class MyDSpaceGuard { +export const myDSpaceGuard: CanActivateFn = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + configurationService: MyDSpaceConfigurationService = inject(MyDSpaceConfigurationService), + router: Router = inject(Router), +): Observable => { + return configurationService.getAvailableConfigurationTypes().pipe( + first(), + map((configurationList) => validateConfigurationParam(router, route.queryParamMap.get('configuration'), configurationList))); +}; - /** - * @constructor - */ - constructor(private configurationService: MyDSpaceConfigurationService, private router: Router) { - } +/** + * Check if the given configuration is present in the list of those available + * + * @param router + * the service router + * @param configuration + * the configuration to validate + * @param configurationList + * the list of available configuration + * + */ +function validateConfigurationParam(router: Router, configuration: string, configurationList: MyDSpaceConfigurationValueType[]): boolean { + const configurationDefault: string = configurationList[0]; + if (isEmpty(configuration) || !configurationList.includes(configuration as MyDSpaceConfigurationValueType)) { + // If configuration param is empty or is not included in available configurations redirect to a default configuration value + const navigationExtras: NavigationExtras = { + queryParams: { configuration: configurationDefault }, + }; - /** - * True when configuration is valid - * @method canActivate - */ - canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - return this.configurationService.getAvailableConfigurationTypes().pipe( - first(), - map((configurationList) => this.validateConfigurationParam(route.queryParamMap.get('configuration'), configurationList))); - } - - /** - * Check if the given configuration is present in the list of those available - * - * @param configuration - * the configuration to validate - * @param configurationList - * the list of available configuration - * - */ - private validateConfigurationParam(configuration: string, configurationList: MyDSpaceConfigurationValueType[]): boolean { - const configurationDefault: string = configurationList[0]; - if (isEmpty(configuration) || !configurationList.includes(configuration as MyDSpaceConfigurationValueType)) { - // If configuration param is empty or is not included in available configurations redirect to a default configuration value - const navigationExtras: NavigationExtras = { - queryParams: { configuration: configurationDefault }, - }; - - this.router.navigate([MYDSPACE_ROUTE], navigationExtras); - return false; - } else { - return true; - } + router.navigate([MYDSPACE_ROUTE], navigationExtras); + return false; + } else { + return true; } } diff --git a/src/app/process-page/process-breadcrumb.resolver.spec.ts b/src/app/process-page/process-breadcrumb.resolver.spec.ts index e4bdcba537..ce579e715f 100644 --- a/src/app/process-page/process-breadcrumb.resolver.spec.ts +++ b/src/app/process-page/process-breadcrumb.resolver.spec.ts @@ -1,11 +1,11 @@ import { ProcessDataService } from '../core/data/processes/process-data.service'; import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; -import { ProcessBreadcrumbResolver } from './process-breadcrumb.resolver'; +import { processBreadcrumbResolver } from './process-breadcrumb.resolver'; import { Process } from './processes/process.model'; -describe('ProcessBreadcrumbResolver', () => { +describe('processBreadcrumbResolver', () => { describe('resolve', () => { - let resolver: ProcessBreadcrumbResolver; + let resolver: any; let processDataService: ProcessDataService; let processBreadcrumbService: any; let process: Process; @@ -19,11 +19,16 @@ describe('ProcessBreadcrumbResolver', () => { processDataService = { findById: () => createSuccessfulRemoteDataObject$(process), } as any; - resolver = new ProcessBreadcrumbResolver(processBreadcrumbService, processDataService); + resolver = processBreadcrumbResolver; }); it('should resolve the breadcrumb config', (done) => { - const resolvedConfig = resolver.resolve({ data: { breadcrumbKey: process }, params: { id: id } } as any, { url: path } as any); + const resolvedConfig = resolver( + { data: { breadcrumbKey: process }, params: { id: id } } as any, + { url: path } as any, + processBreadcrumbService, + processDataService, + ); const expectedConfig = { provider: processBreadcrumbService, key: process, url: path }; resolvedConfig.subscribe((config) => { expect(config).toEqual(expectedConfig); @@ -33,7 +38,7 @@ describe('ProcessBreadcrumbResolver', () => { it('should resolve throw an error when no breadcrumbKey is defined', () => { expect(() => { - resolver.resolve({ data: {} } as any, undefined); + resolver({ data: {} } as any, undefined, processBreadcrumbService, processDataService); }).toThrow(); }); }); diff --git a/src/app/process-page/process-breadcrumb.resolver.ts b/src/app/process-page/process-breadcrumb.resolver.ts index 3e1043241f..95596cfe2f 100644 --- a/src/app/process-page/process-breadcrumb.resolver.ts +++ b/src/app/process-page/process-breadcrumb.resolver.ts @@ -1,6 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; import { Observable } from 'rxjs'; @@ -15,30 +16,28 @@ import { ProcessBreadcrumbsService } from './process-breadcrumbs.service'; import { Process } from './processes/process.model'; /** - * This class represents a resolver that requests a specific process before the route is activated + * Method for resolving a process based on the parameters in the current route + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @param breadcrumbService + * @param processService + * @returns Observable<> Emits the found process based on the parameters in the current route, + * or an error if something went wrong */ -@Injectable({ providedIn: 'root' }) -export class ProcessBreadcrumbResolver { - constructor(protected breadcrumbService: ProcessBreadcrumbsService, private processService: ProcessDataService) { - } +export const processBreadcrumbResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + breadcrumbService: ProcessBreadcrumbsService = inject(ProcessBreadcrumbsService), + processService: ProcessDataService = inject(ProcessDataService), +): Observable> => { + const id = route.params.id; - /** - * Method for resolving a process based on the parameters in the current route - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - * @param {RouterStateSnapshot} state The current RouterStateSnapshot - * @returns Observable<> Emits the found process based on the parameters in the current route, - * or an error if something went wrong - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - const id = route.params.id; - - return this.processService.findById(route.params.id, true, false, followLink('script')).pipe( - getFirstCompletedRemoteData(), - map((object: RemoteData) => { - const fullPath = state.url; - const url = fullPath.substr(0, fullPath.indexOf(id)) + id; - return { provider: this.breadcrumbService, key: object.payload, url: url }; - }), - ); - } -} + return processService.findById(route.params.id, true, false, followLink('script')).pipe( + getFirstCompletedRemoteData(), + map((object: RemoteData) => { + const fullPath = state.url; + const url = fullPath.substring(0, fullPath.indexOf(id)).concat(id); + return { provider: breadcrumbService, key: object.payload, url: url }; + }), + ); +}; diff --git a/src/app/process-page/process-page-routes.ts b/src/app/process-page/process-page-routes.ts index 55893090b7..3d41376a44 100644 --- a/src/app/process-page/process-page-routes.ts +++ b/src/app/process-page/process-page-routes.ts @@ -1,19 +1,19 @@ import { Route } from '@angular/router'; -import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; -import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; +import { authenticatedGuard } from '../core/auth/authenticated.guard'; +import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; import { ProcessDetailComponent } from './detail/process-detail.component'; import { NewProcessComponent } from './new/new-process.component'; import { ProcessOverviewComponent } from './overview/process-overview.component'; -import { ProcessBreadcrumbResolver } from './process-breadcrumb.resolver'; -import { ProcessPageResolver } from './process-page.resolver'; +import { processBreadcrumbResolver } from './process-breadcrumb.resolver'; +import { processPageResolver } from './process-page.resolver'; export const ROUTES: Route[] = [ { path: '', - resolve: { breadcrumb: I18nBreadcrumbResolver }, + resolve: { breadcrumb: i18nBreadcrumbResolver }, data: { breadcrumbKey: 'process.overview' }, - canActivate: [AuthenticatedGuard], + canActivate: [authenticatedGuard], children: [ { path: '', @@ -23,15 +23,15 @@ export const ROUTES: Route[] = [ { path: 'new', component: NewProcessComponent, - resolve: { breadcrumb: I18nBreadcrumbResolver }, + resolve: { breadcrumb: i18nBreadcrumbResolver }, data: { title: 'process.new.title', breadcrumbKey: 'process.new' }, }, { path: ':id', component: ProcessDetailComponent, resolve: { - process: ProcessPageResolver, - breadcrumb: ProcessBreadcrumbResolver, + process: processPageResolver, + breadcrumb: processBreadcrumbResolver, }, }, ], diff --git a/src/app/process-page/process-page.resolver.ts b/src/app/process-page/process-page.resolver.ts index b5b0ccb28e..3a074ce225 100644 --- a/src/app/process-page/process-page.resolver.ts +++ b/src/app/process-page/process-page.resolver.ts @@ -1,6 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; import { Observable } from 'rxjs'; @@ -16,23 +17,19 @@ export const PROCESS_PAGE_FOLLOW_LINKS = [ ]; /** - * This class represents a resolver that requests a specific process before the route is activated + * Method for resolving a process based on the parameters in the current route + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @param {ProcessDataService} processService + * @returns Observable<> Emits the found process based on the parameters in the current route, + * or an error if something went wrong */ -@Injectable({ providedIn: 'root' }) -export class ProcessPageResolver { - constructor(private processService: ProcessDataService) { - } - - /** - * Method for resolving a process based on the parameters in the current route - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - * @param {RouterStateSnapshot} state The current RouterStateSnapshot - * @returns Observable<> Emits the found process based on the parameters in the current route, - * or an error if something went wrong - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - return this.processService.findById(route.params.id, false, true, ...PROCESS_PAGE_FOLLOW_LINKS).pipe( - getFirstCompletedRemoteData(), - ); - } -} +export const processPageResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + processService: ProcessDataService = inject(ProcessDataService), +): Observable> => { + return processService.findById(route.params.id, false, true, ...PROCESS_PAGE_FOLLOW_LINKS).pipe( + getFirstCompletedRemoteData(), + ); +}; diff --git a/src/app/profile-page/profile-page-routes.ts b/src/app/profile-page/profile-page-routes.ts index baeabedc0c..a4e8d6925f 100644 --- a/src/app/profile-page/profile-page-routes.ts +++ b/src/app/profile-page/profile-page-routes.ts @@ -1,6 +1,6 @@ import { Route } from '@angular/router'; -import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; +import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; import { ThemedProfilePageComponent } from './themed-profile-page.component'; export const ROUTES: Route[] = [ @@ -8,7 +8,7 @@ export const ROUTES: Route[] = [ path: '', pathMatch: 'full', component: ThemedProfilePageComponent, - resolve: { breadcrumb: I18nBreadcrumbResolver }, + resolve: { breadcrumb: i18nBreadcrumbResolver }, data: { breadcrumbKey: 'profile', title: 'profile.title' }, }, ]; diff --git a/src/app/quality-assurance-notifications-pages/notifications-pages-routes.ts b/src/app/quality-assurance-notifications-pages/notifications-pages-routes.ts index 661fe02f24..98f94692f9 100644 --- a/src/app/quality-assurance-notifications-pages/notifications-pages-routes.ts +++ b/src/app/quality-assurance-notifications-pages/notifications-pages-routes.ts @@ -1,8 +1,8 @@ import { Route } from '@angular/router'; -import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; -import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; -import { QualityAssuranceBreadcrumbResolver } from '../core/breadcrumbs/quality-assurance-breadcrumb.resolver'; +import { authenticatedGuard } from '../core/auth/authenticated.guard'; +import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; +import { qualityAssuranceBreadcrumbResolver } from '../core/breadcrumbs/quality-assurance-breadcrumb.resolver'; import { NOTIFICATIONS_RECITER_SUGGESTION_PATH, QUALITY_ASSURANCE_EDIT_PATH, @@ -10,8 +10,8 @@ import { import { NotificationsSuggestionTargetsPageComponent } from './notifications-suggestion-targets-page/notifications-suggestion-targets-page.component'; import { AdminNotificationsPublicationClaimPageResolver } from './notifications-suggestion-targets-page/notifications-suggestion-targets-page-resolver.service'; import { QualityAssuranceEventsPageComponent } from './quality-assurance-events-page/quality-assurance-events-page.component'; -import { QualityAssuranceEventsPageResolver } from './quality-assurance-events-page/quality-assurance-events-page.resolver'; -import { SourceDataResolver } from './quality-assurance-source-page-component/quality-assurance-source-data.resolver'; +import { qualityAssuranceEventsPageResolver } from './quality-assurance-events-page/quality-assurance-events-page.resolver'; +import { qualityAssuranceSourceDataResolver } from './quality-assurance-source-page-component/quality-assurance-source-data.resolver'; import { QualityAssuranceSourcePageComponent } from './quality-assurance-source-page-component/quality-assurance-source-page.component'; import { QualityAssuranceSourcePageResolver } from './quality-assurance-source-page-component/quality-assurance-source-page-resolver.service'; import { QualityAssuranceTopicsPageComponent } from './quality-assurance-topics-page/quality-assurance-topics-page.component'; @@ -19,12 +19,12 @@ import { QualityAssuranceTopicsPageResolver } from './quality-assurance-topics-p export const ROUTES: Route[] = [ { - canActivate: [AuthenticatedGuard], + canActivate: [authenticatedGuard], path: `${NOTIFICATIONS_RECITER_SUGGESTION_PATH}`, component: NotificationsSuggestionTargetsPageComponent, pathMatch: 'full', resolve: { - breadcrumb: I18nBreadcrumbResolver, + breadcrumb: i18nBreadcrumbResolver, reciterSuggestionTargetParams: AdminNotificationsPublicationClaimPageResolver, }, data: { @@ -34,12 +34,12 @@ export const ROUTES: Route[] = [ }, }, { - canActivate: [AuthenticatedGuard], + canActivate: [authenticatedGuard], path: `${QUALITY_ASSURANCE_EDIT_PATH}/:sourceId`, component: QualityAssuranceTopicsPageComponent, pathMatch: 'full', resolve: { - breadcrumb: QualityAssuranceBreadcrumbResolver, + breadcrumb: qualityAssuranceBreadcrumbResolver, openaireQualityAssuranceTopicsParams: QualityAssuranceTopicsPageResolver, }, data: { @@ -49,12 +49,12 @@ export const ROUTES: Route[] = [ }, }, { - canActivate: [AuthenticatedGuard], + canActivate: [authenticatedGuard], path: `${QUALITY_ASSURANCE_EDIT_PATH}/:sourceId/target/:targetId`, component: QualityAssuranceTopicsPageComponent, pathMatch: 'full', resolve: { - breadcrumb: I18nBreadcrumbResolver, + breadcrumb: i18nBreadcrumbResolver, openaireQualityAssuranceTopicsParams: QualityAssuranceTopicsPageResolver, }, data: { @@ -64,14 +64,14 @@ export const ROUTES: Route[] = [ }, }, { - canActivate: [AuthenticatedGuard], + canActivate: [authenticatedGuard], path: `${QUALITY_ASSURANCE_EDIT_PATH}`, component: QualityAssuranceSourcePageComponent, pathMatch: 'full', resolve: { - breadcrumb: I18nBreadcrumbResolver, + breadcrumb: i18nBreadcrumbResolver, openaireQualityAssuranceSourceParams: QualityAssuranceSourcePageResolver, - sourceData: SourceDataResolver, + sourceData: qualityAssuranceSourceDataResolver, }, data: { title: 'admin.notifications.source.breadcrumbs', @@ -80,13 +80,13 @@ export const ROUTES: Route[] = [ }, }, { - canActivate: [AuthenticatedGuard], + canActivate: [authenticatedGuard], path: `${QUALITY_ASSURANCE_EDIT_PATH}/:sourceId/:topicId`, component: QualityAssuranceEventsPageComponent, pathMatch: 'full', resolve: { - breadcrumb: QualityAssuranceBreadcrumbResolver, - openaireQualityAssuranceEventsParams: QualityAssuranceEventsPageResolver, + breadcrumb: qualityAssuranceBreadcrumbResolver, + openaireQualityAssuranceEventsParams: qualityAssuranceEventsPageResolver, }, data: { title: 'admin.notifications.event.page.title', diff --git a/src/app/quality-assurance-notifications-pages/quality-assurance-events-page/quality-assurance-events-page.resolver.ts b/src/app/quality-assurance-notifications-pages/quality-assurance-events-page/quality-assurance-events-page.resolver.ts index 3374fd619b..b0c3bdc978 100644 --- a/src/app/quality-assurance-notifications-pages/quality-assurance-events-page/quality-assurance-events-page.resolver.ts +++ b/src/app/quality-assurance-notifications-pages/quality-assurance-events-page/quality-assurance-events-page.resolver.ts @@ -1,6 +1,6 @@ -import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; @@ -14,22 +14,18 @@ export interface AssuranceEventsPageParams { } /** - * This class represents a resolver that retrieve the route data before the route is activated. + * Method for resolving the parameters in the current route. + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @returns AdminQualityAssuranceEventsPageParams Emits the route parameters */ -@Injectable({ providedIn: 'root' }) -export class QualityAssuranceEventsPageResolver { - - /** - * Method for resolving the parameters in the current route. - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - * @param {RouterStateSnapshot} state The current RouterStateSnapshot - * @returns AdminQualityAssuranceEventsPageParams Emits the route parameters - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): AssuranceEventsPageParams { - return { - pageId: route.queryParams.pageId, - pageSize: parseInt(route.queryParams.pageSize, 10), - currentPage: parseInt(route.queryParams.page, 10), - }; - } -} +export const qualityAssuranceEventsPageResolver: ResolveFn = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, +): AssuranceEventsPageParams => { + return { + pageId: route.queryParams.pageId, + pageSize: parseInt(route.queryParams.pageSize, 10), + currentPage: parseInt(route.queryParams.page, 10), + }; +}; diff --git a/src/app/quality-assurance-notifications-pages/quality-assurance-source-page-component/quality-assurance-source-data.resolver.ts b/src/app/quality-assurance-notifications-pages/quality-assurance-source-page-component/quality-assurance-source-data.resolver.ts index 8019f57f90..42ff19299e 100644 --- a/src/app/quality-assurance-notifications-pages/quality-assurance-source-page-component/quality-assurance-source-data.resolver.ts +++ b/src/app/quality-assurance-notifications-pages/quality-assurance-source-page-component/quality-assurance-source-data.resolver.ts @@ -1,53 +1,53 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, Router, RouterStateSnapshot, } from '@angular/router'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { environment } from '../../../environments/environment'; +import { + APP_CONFIG, + AppConfig, +} from '../../../config/app-config.interface'; import { PaginatedList } from '../../core/data/paginated-list.model'; import { QualityAssuranceSourceObject } from '../../core/notifications/qa/models/quality-assurance-source.model'; import { QualityAssuranceSourceService } from '../../notifications/qa/source/quality-assurance-source.service'; /** - * This class represents a resolver that retrieve the route data before the route is activated. + * Method for resolving the parameters in the current route. + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @param router + * @param qualityAssuranceSourceService + * @param appConfig + * @returns Observable */ -@Injectable({ providedIn: 'root' }) -export class SourceDataResolver { - private pageSize = environment.qualityAssuranceConfig.pageSize; - /** - * Initialize the effect class variables. - * @param {QualityAssuranceSourceService} qualityAssuranceSourceService - */ - constructor( - private qualityAssuranceSourceService: QualityAssuranceSourceService, - private router: Router, - ) { } - /** - * Method for resolving the parameters in the current route. - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - * @param {RouterStateSnapshot} state The current RouterStateSnapshot - * @returns Observable - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - return this.qualityAssuranceSourceService.getSources(this.pageSize, 0).pipe( - map((sources: PaginatedList) => { - if (sources.page.length === 1) { - this.router.navigate([this.getResolvedUrl(route) + '/' + sources.page[0].id]); - } - return sources.page; - })); - } +export const qualityAssuranceSourceDataResolver: ResolveFn = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + router: Router = inject(Router), + qualityAssuranceSourceService: QualityAssuranceSourceService = inject(QualityAssuranceSourceService), + appConfig: AppConfig = inject(APP_CONFIG), +): Observable => { + const pageSize = appConfig.qualityAssuranceConfig.pageSize; - /** - * - * @param route url path - * @returns url path - */ - getResolvedUrl(route: ActivatedRouteSnapshot): string { - return route.pathFromRoot.map(v => v.url.map(segment => segment.toString()).join('/')).join('/'); - } + return qualityAssuranceSourceService.getSources(pageSize, 0).pipe( + map((sources: PaginatedList) => { + if (sources.page.length === 1) { + router.navigate([getResolvedUrl(route) + '/' + sources.page[0].id]); + } + return sources.page; + })); +}; + +/** + * + * @param route url path + * @returns url path + */ +function getResolvedUrl(route: ActivatedRouteSnapshot): string { + return route.pathFromRoot.map(v => v.url.map(segment => segment.toString()).join('/')).join('/'); } diff --git a/src/app/register-email-form/registration.resolver.spec.ts b/src/app/register-email-form/registration.resolver.spec.ts index e286a548de..f3b6b0e404 100644 --- a/src/app/register-email-form/registration.resolver.spec.ts +++ b/src/app/register-email-form/registration.resolver.spec.ts @@ -3,10 +3,10 @@ import { first } from 'rxjs/operators'; import { EpersonRegistrationService } from '../core/data/eperson-registration.service'; import { Registration } from '../core/shared/registration.model'; import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; -import { RegistrationResolver } from './registration.resolver'; +import { registrationResolver } from './registration.resolver'; -describe('RegistrationResolver', () => { - let resolver: RegistrationResolver; +describe('registrationResolver', () => { + let resolver: any; let epersonRegistrationService: EpersonRegistrationService; const token = 'test-token'; @@ -16,11 +16,11 @@ describe('RegistrationResolver', () => { epersonRegistrationService = jasmine.createSpyObj('epersonRegistrationService', { searchByToken: createSuccessfulRemoteDataObject$(registration), }); - resolver = new RegistrationResolver(epersonRegistrationService); + resolver = registrationResolver; }); describe('resolve', () => { it('should resolve a registration based on the token', (done) => { - resolver.resolve({ params: { token: token } } as any, undefined) + resolver({ params: { token: token } } as any, undefined, epersonRegistrationService) .pipe(first()) .subscribe( (resolved) => { diff --git a/src/app/register-email-form/registration.resolver.ts b/src/app/register-email-form/registration.resolver.ts index e301b9a4a0..b87f70bf4e 100644 --- a/src/app/register-email-form/registration.resolver.ts +++ b/src/app/register-email-form/registration.resolver.ts @@ -1,6 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; import { Observable } from 'rxjs'; @@ -10,19 +11,13 @@ import { RemoteData } from '../core/data/remote-data'; import { getFirstCompletedRemoteData } from '../core/shared/operators'; import { Registration } from '../core/shared/registration.model'; -@Injectable({ providedIn: 'root' }) -/** - * Resolver to resolve a Registration object based on the provided token - */ -export class RegistrationResolver { - - constructor(private epersonRegistrationService: EpersonRegistrationService) { - } - - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - const token = route.params.token; - return this.epersonRegistrationService.searchByToken(token).pipe( - getFirstCompletedRemoteData(), - ); - } -} +export const registrationResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + epersonRegistrationService: EpersonRegistrationService = inject(EpersonRegistrationService), +): Observable> => { + const token = route.params.token; + return epersonRegistrationService.searchByToken(token).pipe( + getFirstCompletedRemoteData(), + ); +}; diff --git a/src/app/register-page/register-page-routes.ts b/src/app/register-page/register-page-routes.ts index acc737099a..109ed24c6d 100644 --- a/src/app/register-page/register-page-routes.ts +++ b/src/app/register-page/register-page-routes.ts @@ -1,9 +1,12 @@ -import { Route } from '@angular/router'; +import { + mapToCanActivate, + Route, +} from '@angular/router'; import { EndUserAgreementCookieGuard } from '../core/end-user-agreement/end-user-agreement-cookie.guard'; import { ThemedCreateProfileComponent } from './create-profile/themed-create-profile.component'; import { ThemedRegisterEmailComponent } from './register-email/themed-register-email.component'; -import { RegistrationGuard } from './registration.guard'; +import { registrationGuard } from './registration.guard'; export const ROUTES: Route[] = [ @@ -16,8 +19,8 @@ export const ROUTES: Route[] = [ path: ':token', component: ThemedCreateProfileComponent, canActivate: [ - RegistrationGuard, - EndUserAgreementCookieGuard, + registrationGuard, + ...mapToCanActivate([EndUserAgreementCookieGuard]), ], }, ]; diff --git a/src/app/register-page/registration.guard.spec.ts b/src/app/register-page/registration.guard.spec.ts index 7ac67895b6..31bc751993 100644 --- a/src/app/register-page/registration.guard.spec.ts +++ b/src/app/register-page/registration.guard.spec.ts @@ -13,10 +13,10 @@ import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject, } from '../shared/remote-data.utils'; -import { RegistrationGuard } from './registration.guard'; +import { registrationGuard } from './registration.guard'; -describe('RegistrationGuard', () => { - let guard: RegistrationGuard; +describe('registrationGuard', () => { + let guard: any; let epersonRegistrationService: EpersonRegistrationService; let router: Router; @@ -65,7 +65,7 @@ describe('RegistrationGuard', () => { setRedirectUrl: {}, }); - guard = new RegistrationGuard(epersonRegistrationService, router, authService); + guard = registrationGuard; }); describe('canActivate', () => { @@ -75,21 +75,21 @@ describe('RegistrationGuard', () => { }); it('should return true', (done) => { - guard.canActivate(route, state).subscribe((result) => { + guard(route, state, authService, epersonRegistrationService, router).subscribe((result) => { expect(result).toEqual(true); done(); }); }); it('should add the response to the route\'s data', (done) => { - guard.canActivate(route, state).subscribe(() => { + guard(route, state, authService, epersonRegistrationService, router).subscribe(() => { expect(route.data).toEqual({ ...startingRouteData, registration: registrationRD }); done(); }); }); it('should not redirect', (done) => { - guard.canActivate(route, state).subscribe(() => { + guard(route, state, authService, epersonRegistrationService, router).subscribe(() => { expect(router.navigateByUrl).not.toHaveBeenCalled(); done(); }); @@ -102,7 +102,7 @@ describe('RegistrationGuard', () => { }); it('should redirect', () => { - guard.canActivate(route, state).subscribe(); + guard(route, state, authService, epersonRegistrationService, router).subscribe(); expect(router.navigateByUrl).toHaveBeenCalled(); }); }); diff --git a/src/app/register-page/registration.guard.ts b/src/app/register-page/registration.guard.ts index 54e81b8beb..49614208e2 100644 --- a/src/app/register-page/registration.guard.ts +++ b/src/app/register-page/registration.guard.ts @@ -1,6 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, + CanActivateFn, Router, RouterStateSnapshot, } from '@angular/router'; @@ -12,37 +13,25 @@ import { EpersonRegistrationService } from '../core/data/eperson-registration.se import { redirectOn4xx } from '../core/shared/authorized.operators'; import { getFirstCompletedRemoteData } from '../core/shared/operators'; -@Injectable({ - providedIn: 'root', -}) /** * A guard responsible for redirecting to 4xx pages upon retrieving a Registration object * The guard also adds the resulting RemoteData object to the route's data for further usage in components * The reason this is a guard and not a resolver, is because it has to run before the EndUserAgreementCookieGuard */ -export class RegistrationGuard { - constructor(private epersonRegistrationService: EpersonRegistrationService, - private router: Router, - private authService: AuthService) { - } - - /** - * Can the user activate the route? Returns true if the provided token resolves to an existing Registration, false if - * not. Redirects to 4xx page on 4xx error. Adds the resulting RemoteData object to the route's - * data.registration property - * @param route - * @param state - */ - canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - const token = route.params.token; - return this.epersonRegistrationService.searchByToken(token).pipe( - getFirstCompletedRemoteData(), - redirectOn4xx(this.router, this.authService), - map((rd) => { - route.data = { ...route.data, registration: rd }; - return rd.hasSucceeded; - }), - ); - } - -} +export const registrationGuard: CanActivateFn = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + authService: AuthService = inject(AuthService), + epersonRegistrationService: EpersonRegistrationService = inject(EpersonRegistrationService), + router: Router = inject(Router), +): Observable => { + const token = route.params.token; + return epersonRegistrationService.searchByToken(token).pipe( + getFirstCompletedRemoteData(), + redirectOn4xx(router, authService), + map((rd) => { + route.data = { ...route.data, registration: rd }; + return rd.hasSucceeded; + }), + ); +}; diff --git a/src/app/request-copy/request-copy-routes.ts b/src/app/request-copy/request-copy-routes.ts index 17a2d39c1b..9eacb4b643 100644 --- a/src/app/request-copy/request-copy-routes.ts +++ b/src/app/request-copy/request-copy-routes.ts @@ -3,7 +3,7 @@ import { Route } from '@angular/router'; import { ThemedDenyRequestCopyComponent } from './deny-request-copy/themed-deny-request-copy.component'; import { GrantDenyRequestCopyComponent } from './grant-deny-request-copy/grant-deny-request-copy.component'; import { ThemedGrantRequestCopyComponent } from './grant-request-copy/themed-grant-request-copy.component'; -import { RequestCopyResolver } from './request-copy.resolver'; +import { requestCopyResolver } from './request-copy.resolver'; import { REQUEST_COPY_DENY_PATH, REQUEST_COPY_GRANT_PATH, @@ -13,7 +13,7 @@ export const ROUTES: Route[] = [ { path: ':token', resolve: { - request: RequestCopyResolver, + request: requestCopyResolver, }, children: [ { diff --git a/src/app/request-copy/request-copy.resolver.ts b/src/app/request-copy/request-copy.resolver.ts index 2661ed4dfb..b91e1b7d8c 100644 --- a/src/app/request-copy/request-copy.resolver.ts +++ b/src/app/request-copy/request-copy.resolver.ts @@ -1,6 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; import { Observable } from 'rxjs'; @@ -10,21 +11,12 @@ import { RemoteData } from '../core/data/remote-data'; import { ItemRequest } from '../core/shared/item-request.model'; import { getFirstCompletedRemoteData } from '../core/shared/operators'; -/** - * Resolves an {@link ItemRequest} from the token found in the route's parameters - */ -@Injectable({ providedIn: 'root' }) -export class RequestCopyResolver { - - constructor( - private itemRequestDataService: ItemRequestDataService, - ) { - } - - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> | Promise> | RemoteData { - return this.itemRequestDataService.findById(route.params.token).pipe( - getFirstCompletedRemoteData(), - ); - } - -} +export const requestCopyResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + itemRequestDataService: ItemRequestDataService = inject(ItemRequestDataService), +): Observable> => { + return itemRequestDataService.findById(route.params.token).pipe( + getFirstCompletedRemoteData(), + ); +}; diff --git a/src/app/search-page/configuration-search-page.guard.ts b/src/app/search-page/configuration-search-page.guard.ts index 7b86c2a498..ec032bd04a 100644 --- a/src/app/search-page/configuration-search-page.guard.ts +++ b/src/app/search-page/configuration-search-page.guard.ts @@ -1,25 +1,22 @@ -import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, + CanActivateFn, RouterStateSnapshot, } from '@angular/router'; -import { Observable } from 'rxjs'; -@Injectable() /** * Assemble the correct i18n key for the configuration search page's title depending on the current route's configuration parameter. * The format of the key will be "{configuration}.search.title" with: * - configuration: The current configuration stored in route.params */ -export class ConfigurationSearchPageGuard { - canActivate( - route: ActivatedRouteSnapshot, - state: RouterStateSnapshot): Observable | Promise | boolean { - const configuration = route.params.configuration; +export const configurationSearchPageGuard: CanActivateFn = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, +): boolean => { + const configuration = route.params.configuration; - const newTitle = configuration + '.search.title'; + const newTitle = `${configuration}.search.title`; - route.data = { title: newTitle }; - return true; - } -} + route.data = { title: newTitle }; + return true; +}; diff --git a/src/app/search-page/search-page-routes.ts b/src/app/search-page/search-page-routes.ts index f9214459b3..329c09d185 100644 --- a/src/app/search-page/search-page-routes.ts +++ b/src/app/search-page/search-page-routes.ts @@ -1,19 +1,19 @@ import { Route } from '@angular/router'; -import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; -import { ConfigurationSearchPageGuard } from './configuration-search-page.guard'; +import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; +import { configurationSearchPageGuard } from './configuration-search-page.guard'; import { ThemedConfigurationSearchPageComponent } from './themed-configuration-search-page.component'; import { ThemedSearchPageComponent } from './themed-search-page.component'; export const ROUTES: Route[] = [{ path: '', - resolve: { breadcrumb: I18nBreadcrumbResolver }, data: { title: 'search.title', breadcrumbKey: 'search' }, + resolve: { breadcrumb: i18nBreadcrumbResolver }, data: { title: 'search.title', breadcrumbKey: 'search' }, children: [ { path: '', component: ThemedSearchPageComponent }, { path: ':configuration', component: ThemedConfigurationSearchPageComponent, - canActivate: [ConfigurationSearchPageGuard], + canActivate: [configurationSearchPageGuard], }, ], }]; diff --git a/src/app/shared/auth-nav-menu/auth-nav-menu.component.spec.ts b/src/app/shared/auth-nav-menu/auth-nav-menu.component.spec.ts index 224620c6e1..a969b6f49b 100644 --- a/src/app/shared/auth-nav-menu/auth-nav-menu.component.spec.ts +++ b/src/app/shared/auth-nav-menu/auth-nav-menu.component.spec.ts @@ -27,6 +27,7 @@ import { } from '../../core/auth/auth.reducer'; import { AuthService } from '../../core/auth/auth.service'; import { AuthTokenInfo } from '../../core/auth/models/auth-token-info.model'; +import { XSRFService } from '../../core/xsrf/xsrf.service'; import { HostWindowService } from '../host-window.service'; import { ActivatedRouteStub } from '../testing/active-router.stub'; import { BrowserOnlyMockPipe } from '../testing/browser-only-mock.pipe'; @@ -102,6 +103,7 @@ describe('AuthNavMenuComponent', () => { { provide: HostWindowService, useValue: window }, { provide: AuthService, useValue: authService }, { provide: ActivatedRoute, useValue: new ActivatedRouteStub() }, + { provide: XSRFService, useValue: {} }, ], schemas: [ CUSTOM_ELEMENTS_SCHEMA, diff --git a/src/app/shared/auth-nav-menu/user-menu/user-menu.component.spec.ts b/src/app/shared/auth-nav-menu/user-menu/user-menu.component.spec.ts index d57db27684..c7ddf2ac34 100644 --- a/src/app/shared/auth-nav-menu/user-menu/user-menu.component.spec.ts +++ b/src/app/shared/auth-nav-menu/user-menu/user-menu.component.spec.ts @@ -29,6 +29,7 @@ import { } from '../../../core/auth/auth.reducer'; import { AuthService } from '../../../core/auth/auth.service'; import { AuthTokenInfo } from '../../../core/auth/models/auth-token-info.model'; +import { XSRFService } from '../../../core/xsrf/xsrf.service'; import { TranslateLoaderMock } from '../../mocks/translate-loader.mock'; import { ActivatedRouteStub } from '../../testing/active-router.stub'; import { EPersonMock } from '../../testing/eperson.mock'; @@ -91,6 +92,7 @@ describe('UserMenuComponent', () => { providers: [ { provide: AuthService, useValue: authService }, { provide: ActivatedRoute, useValue: new ActivatedRouteStub() }, + { provide: XSRFService, useValue: {} }, { provide: APP_DATA_SERVICES_MAP, useValue: {} }, ], schemas: [ diff --git a/src/app/shared/dso-page/dso-edit-menu-resolver.service.ts b/src/app/shared/dso-page/dso-edit-menu-resolver.service.ts new file mode 100644 index 0000000000..e6bbeac619 --- /dev/null +++ b/src/app/shared/dso-page/dso-edit-menu-resolver.service.ts @@ -0,0 +1,324 @@ +import { Injectable } from '@angular/core'; +import { + ActivatedRouteSnapshot, + RouterStateSnapshot, +} from '@angular/router'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateService } from '@ngx-translate/core'; +import { + combineLatest, + Observable, + of as observableOf, +} from 'rxjs'; +import { + map, + switchMap, +} from 'rxjs/operators'; + +import { getDSORoute } from '../../app-routing-paths'; +import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; +import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; +import { FeatureID } from '../../core/data/feature-authorization/feature-id'; +import { ResearcherProfileDataService } from '../../core/profile/researcher-profile-data.service'; +import { Collection } from '../../core/shared/collection.model'; +import { Community } from '../../core/shared/community.model'; +import { Item } from '../../core/shared/item.model'; +import { + getFirstCompletedRemoteData, + getRemoteDataPayload, +} from '../../core/shared/operators'; +import { CorrectionTypeDataService } from '../../core/submission/correctiontype-data.service'; +import { URLCombiner } from '../../core/url-combiner/url-combiner'; +import { + hasNoValue, + hasValue, + isNotEmpty, +} from '../empty.util'; +import { MenuService } from '../menu/menu.service'; +import { MenuID } from '../menu/menu-id.model'; +import { LinkMenuItemModel } from '../menu/menu-item/models/link.model'; +import { OnClickMenuItemModel } from '../menu/menu-item/models/onclick.model'; +import { MenuItemType } from '../menu/menu-item-type.model'; +import { MenuSection } from '../menu/menu-section.model'; +import { NotificationsService } from '../notifications/notifications.service'; +import { SubscriptionModalComponent } from '../subscriptions/subscription-modal/subscription-modal.component'; +import { DsoVersioningModalService } from './dso-versioning-modal-service/dso-versioning-modal.service'; +import { + DsoWithdrawnReinstateModalService, + REQUEST_REINSTATE, + REQUEST_WITHDRAWN, +} from './dso-withdrawn-reinstate-service/dso-withdrawn-reinstate-modal.service'; + +/** + * Creates the menus for the dspace object pages + */ +@Injectable({ + providedIn: 'root', +}) +export class DSOEditMenuResolverService { + + constructor( + protected dSpaceObjectDataService: DSpaceObjectDataService, + protected menuService: MenuService, + protected authorizationService: AuthorizationDataService, + protected modalService: NgbModal, + protected dsoVersioningModalService: DsoVersioningModalService, + protected researcherProfileService: ResearcherProfileDataService, + protected notificationsService: NotificationsService, + protected translate: TranslateService, + protected dsoWithdrawnReinstateModalService: DsoWithdrawnReinstateModalService, + private correctionTypeDataService: CorrectionTypeDataService, + ) { + } + + /** + * Initialise all dspace object related menus + */ + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<{ [key: string]: MenuSection[] }> { + let id = route.params.id; + if (hasNoValue(id) && hasValue(route.queryParams.scope)) { + id = route.queryParams.scope; + } + if (hasNoValue(id)) { + // If there's no ID, we're not on a DSO homepage, so pass on any pre-existing menu route data + return observableOf({ ...route.data?.menu }); + } else { + return this.dSpaceObjectDataService.findById(id, true, false).pipe( + getFirstCompletedRemoteData(), + switchMap((dsoRD) => { + if (dsoRD.hasSucceeded) { + const dso = dsoRD.payload; + return combineLatest(this.getDsoMenus(dso, route, state)).pipe( + // Menu sections are retrieved as an array of arrays and flattened into a single array + map((combinedMenus) => [].concat.apply([], combinedMenus)), + map((menus) => this.addDsoUuidToMenuIDs(menus, dso)), + map((menus) => { + return { + ...route.data?.menu, + [MenuID.DSO_EDIT]: menus, + }; + }), + ); + } else { + return observableOf({ ...route.data?.menu }); + } + }), + ); + } + } + + /** + * Return all the menus for a dso based on the route and state + */ + getDsoMenus(dso, route, state): Observable[] { + return [ + this.getItemMenu(dso), + this.getComColMenu(dso), + this.getCommonMenu(dso, state), + ]; + } + + /** + * Get the common menus between all dspace objects + */ + protected getCommonMenu(dso, state): Observable { + return combineLatest([ + this.authorizationService.isAuthorized(FeatureID.CanEditMetadata, dso.self), + ]).pipe( + map(([canEditItem]) => { + return [ + { + id: 'edit-dso', + active: false, + visible: canEditItem, + model: { + type: MenuItemType.LINK, + text: this.getDsoType(dso) + '.page.edit', + link: new URLCombiner(getDSORoute(dso), 'edit', 'metadata').toString(), + } as LinkMenuItemModel, + icon: 'pencil-alt', + index: 2, + }, + ]; + }), + ); + } + + /** + * Get item specific menus + */ + protected getItemMenu(dso): Observable { + if (dso instanceof Item) { + return combineLatest([ + this.authorizationService.isAuthorized(FeatureID.CanCreateVersion, dso.self), + this.dsoVersioningModalService.isNewVersionButtonDisabled(dso), + this.dsoVersioningModalService.getVersioningTooltipMessage(dso, 'item.page.version.hasDraft', 'item.page.version.create'), + this.authorizationService.isAuthorized(FeatureID.CanSynchronizeWithORCID, dso.self), + this.authorizationService.isAuthorized(FeatureID.CanClaimItem, dso.self), + this.correctionTypeDataService.findByItem(dso.uuid, false).pipe( + getFirstCompletedRemoteData(), + getRemoteDataPayload()), + ]).pipe( + map(([canCreateVersion, disableVersioning, versionTooltip, canSynchronizeWithOrcid, canClaimItem, correction]) => { + const isPerson = this.getDsoType(dso) === 'person'; + return [ + { + id: 'orcid-dso', + active: false, + visible: isPerson && canSynchronizeWithOrcid, + model: { + type: MenuItemType.LINK, + text: 'item.page.orcid.tooltip', + link: new URLCombiner(getDSORoute(dso), 'orcid').toString(), + } as LinkMenuItemModel, + icon: 'orcid fab fa-lg', + index: 0, + }, + { + id: 'version-dso', + active: false, + visible: canCreateVersion, + model: { + type: MenuItemType.ONCLICK, + text: versionTooltip, + disabled: disableVersioning, + function: () => { + this.dsoVersioningModalService.openCreateVersionModal(dso); + }, + } as OnClickMenuItemModel, + icon: 'code-branch', + index: 1, + }, + { + id: 'claim-dso', + active: false, + visible: isPerson && canClaimItem, + model: { + type: MenuItemType.ONCLICK, + text: 'item.page.claim.button', + function: () => { + this.claimResearcher(dso); + }, + } as OnClickMenuItemModel, + icon: 'hand-paper', + index: 3, + }, + { + id: 'withdrawn-item', + active: false, + visible: dso.isArchived && correction?.page.some((c) => c.topic === REQUEST_WITHDRAWN), + model: { + type: MenuItemType.ONCLICK, + text:'item.page.withdrawn', + function: () => { + this.dsoWithdrawnReinstateModalService.openCreateWithdrawnReinstateModal(dso, 'request-withdrawn', dso.isArchived); + }, + } as OnClickMenuItemModel, + icon: 'eye-slash', + index: 4, + }, + { + id: 'reinstate-item', + active: false, + visible: dso.isWithdrawn && correction?.page.some((c) => c.topic === REQUEST_REINSTATE), + model: { + type: MenuItemType.ONCLICK, + text:'item.page.reinstate', + function: () => { + this.dsoWithdrawnReinstateModalService.openCreateWithdrawnReinstateModal(dso, 'request-reinstate', dso.isArchived); + }, + } as OnClickMenuItemModel, + icon: 'eye', + index: 5, + }, + ]; + }), + ); + } else { + return observableOf([]); + } + } + + /** + * Get Community/Collection-specific menus + */ + protected getComColMenu(dso): Observable { + if (dso instanceof Community || dso instanceof Collection) { + return combineLatest([ + this.authorizationService.isAuthorized(FeatureID.CanSubscribe, dso.self), + ]).pipe( + map(([canSubscribe]) => { + return [ + { + id: 'subscribe', + active: false, + visible: canSubscribe, + model: { + type: MenuItemType.ONCLICK, + text: 'subscriptions.tooltip', + function: () => { + const modalRef = this.modalService.open(SubscriptionModalComponent); + modalRef.componentInstance.dso = dso; + }, + } as OnClickMenuItemModel, + icon: 'bell', + index: 4, + }, + ]; + }), + ); + } else { + return observableOf([]); + } + } + + /** + * Claim a researcher by creating a profile + * Shows notifications and/or hides the menu section on success/error + */ + protected claimResearcher(dso) { + this.researcherProfileService.createFromExternalSourceAndReturnRelatedItemId(dso.self) + .subscribe((id: string) => { + if (isNotEmpty(id)) { + this.notificationsService.success(this.translate.get('researcherprofile.success.claim.title'), + this.translate.get('researcherprofile.success.claim.body')); + this.authorizationService.invalidateAuthorizationsRequestCache(); + this.menuService.hideMenuSection(MenuID.DSO_EDIT, 'claim-dso-' + dso.uuid); + } else { + this.notificationsService.error( + this.translate.get('researcherprofile.error.claim.title'), + this.translate.get('researcherprofile.error.claim.body')); + } + }); + } + + /** + * Retrieve the dso or entity type for an object to be used in generic messages + */ + protected getDsoType(dso) { + const renderType = dso.getRenderTypes()[0]; + if (typeof renderType === 'string' || renderType instanceof String) { + return renderType.toLowerCase(); + } else { + return dso.type.toString().toLowerCase(); + } + } + + /** + * Add the dso uuid to all provided menu ids and parent ids + */ + protected addDsoUuidToMenuIDs(menus, dso) { + return menus.map((menu) => { + Object.assign(menu, { + id: menu.id + '-' + dso.uuid, + }); + if (hasValue(menu.parentID)) { + Object.assign(menu, { + parentID: menu.parentID + '-' + dso.uuid, + }); + } + return menu; + }); + } + +} diff --git a/src/app/shared/dso-page/dso-edit-menu.resolver.spec.ts b/src/app/shared/dso-page/dso-edit-menu.resolver.spec.ts index 0ff9227b68..461f0e3934 100644 --- a/src/app/shared/dso-page/dso-edit-menu.resolver.spec.ts +++ b/src/app/shared/dso-page/dso-edit-menu.resolver.spec.ts @@ -40,17 +40,17 @@ import { } from '../remote-data.utils'; import { MenuServiceStub } from '../testing/menu-service.stub'; import { createPaginatedList } from '../testing/utils.test'; -import { DSOEditMenuResolver } from './dso-edit-menu.resolver'; +import { DSOEditMenuResolverService } from './dso-edit-menu-resolver.service'; import { DsoVersioningModalService } from './dso-versioning-modal-service/dso-versioning-modal.service'; import { DsoWithdrawnReinstateModalService } from './dso-withdrawn-reinstate-service/dso-withdrawn-reinstate-modal.service'; -describe('DSOEditMenuResolver', () => { +describe('dsoEditMenuResolver', () => { const MENU_STATE = { id: 'some menu', }; - let resolver: DSOEditMenuResolver; + let resolver: DSOEditMenuResolverService; let dSpaceObjectDataService; let menuService; @@ -189,12 +189,12 @@ describe('DSOEditMenuResolver', () => { { provide: NotificationsService, useValue: notificationsService }, { provide: DsoWithdrawnReinstateModalService, useValue: dsoWithdrawnReinstateModalService }, { provide: CorrectionTypeDataService, useValue: correctionsDataService }, - { - provide: NgbModal, useValue: mockNgbModal }, + { provide: NgbModal, useValue: mockNgbModal }, + DSOEditMenuResolverService, ], schemas: [NO_ERRORS_SCHEMA], }); - resolver = TestBed.inject(DSOEditMenuResolver); + resolver = TestBed.inject(DSOEditMenuResolverService); spyOn(menuService, 'addSection'); })); diff --git a/src/app/shared/dso-page/dso-edit-menu.resolver.ts b/src/app/shared/dso-page/dso-edit-menu.resolver.ts index f11c474ea5..2592357b9a 100644 --- a/src/app/shared/dso-page/dso-edit-menu.resolver.ts +++ b/src/app/shared/dso-page/dso-edit-menu.resolver.ts @@ -1,324 +1,21 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { TranslateService } from '@ngx-translate/core'; -import { - combineLatest, - Observable, - of as observableOf, -} from 'rxjs'; -import { - map, - switchMap, -} from 'rxjs/operators'; +import { Observable } from 'rxjs'; -import { getDSORoute } from '../../app-routing-paths'; -import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; -import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; -import { FeatureID } from '../../core/data/feature-authorization/feature-id'; -import { ResearcherProfileDataService } from '../../core/profile/researcher-profile-data.service'; -import { Collection } from '../../core/shared/collection.model'; -import { Community } from '../../core/shared/community.model'; -import { Item } from '../../core/shared/item.model'; -import { - getFirstCompletedRemoteData, - getRemoteDataPayload, -} from '../../core/shared/operators'; -import { CorrectionTypeDataService } from '../../core/submission/correctiontype-data.service'; -import { URLCombiner } from '../../core/url-combiner/url-combiner'; -import { - hasNoValue, - hasValue, - isNotEmpty, -} from '../empty.util'; -import { MenuService } from '../menu/menu.service'; -import { MenuID } from '../menu/menu-id.model'; -import { LinkMenuItemModel } from '../menu/menu-item/models/link.model'; -import { OnClickMenuItemModel } from '../menu/menu-item/models/onclick.model'; -import { MenuItemType } from '../menu/menu-item-type.model'; import { MenuSection } from '../menu/menu-section.model'; -import { NotificationsService } from '../notifications/notifications.service'; -import { SubscriptionModalComponent } from '../subscriptions/subscription-modal/subscription-modal.component'; -import { DsoVersioningModalService } from './dso-versioning-modal-service/dso-versioning-modal.service'; -import { - DsoWithdrawnReinstateModalService, - REQUEST_REINSTATE, - REQUEST_WITHDRAWN, -} from './dso-withdrawn-reinstate-service/dso-withdrawn-reinstate-modal.service'; +import { DSOEditMenuResolverService } from './dso-edit-menu-resolver.service'; /** - * Creates the menus for the dspace object pages + * Initialise all dspace object related menus */ -@Injectable({ - providedIn: 'root', -}) -export class DSOEditMenuResolver { - - constructor( - protected dSpaceObjectDataService: DSpaceObjectDataService, - protected menuService: MenuService, - protected authorizationService: AuthorizationDataService, - protected modalService: NgbModal, - protected dsoVersioningModalService: DsoVersioningModalService, - protected researcherProfileService: ResearcherProfileDataService, - protected notificationsService: NotificationsService, - protected translate: TranslateService, - protected dsoWithdrawnReinstateModalService: DsoWithdrawnReinstateModalService, - private correctionTypeDataService: CorrectionTypeDataService, - ) { - } - - /** - * Initialise all dspace object related menus - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<{ [key: string]: MenuSection[] }> { - let id = route.params.id; - if (hasNoValue(id) && hasValue(route.queryParams.scope)) { - id = route.queryParams.scope; - } - if (hasNoValue(id)) { - // If there's no ID, we're not on a DSO homepage, so pass on any pre-existing menu route data - return observableOf({ ...route.data?.menu }); - } else { - return this.dSpaceObjectDataService.findById(id, true, false).pipe( - getFirstCompletedRemoteData(), - switchMap((dsoRD) => { - if (dsoRD.hasSucceeded) { - const dso = dsoRD.payload; - return combineLatest(this.getDsoMenus(dso, route, state)).pipe( - // Menu sections are retrieved as an array of arrays and flattened into a single array - map((combinedMenus) => [].concat.apply([], combinedMenus)), - map((menus) => this.addDsoUuidToMenuIDs(menus, dso)), - map((menus) => { - return { - ...route.data?.menu, - [MenuID.DSO_EDIT]: menus, - }; - }), - ); - } else { - return observableOf({ ...route.data?.menu }); - } - }), - ); - } - } - - /** - * Return all the menus for a dso based on the route and state - */ - getDsoMenus(dso, route, state): Observable[] { - return [ - this.getItemMenu(dso), - this.getComColMenu(dso), - this.getCommonMenu(dso, state), - ]; - } - - /** - * Get the common menus between all dspace objects - */ - protected getCommonMenu(dso, state): Observable { - return combineLatest([ - this.authorizationService.isAuthorized(FeatureID.CanEditMetadata, dso.self), - ]).pipe( - map(([canEditItem]) => { - return [ - { - id: 'edit-dso', - active: false, - visible: canEditItem, - model: { - type: MenuItemType.LINK, - text: this.getDsoType(dso) + '.page.edit', - link: new URLCombiner(getDSORoute(dso), 'edit', 'metadata').toString(), - } as LinkMenuItemModel, - icon: 'pencil-alt', - index: 2, - }, - ]; - }), - ); - } - - /** - * Get item specific menus - */ - protected getItemMenu(dso): Observable { - if (dso instanceof Item) { - return combineLatest([ - this.authorizationService.isAuthorized(FeatureID.CanCreateVersion, dso.self), - this.dsoVersioningModalService.isNewVersionButtonDisabled(dso), - this.dsoVersioningModalService.getVersioningTooltipMessage(dso, 'item.page.version.hasDraft', 'item.page.version.create'), - this.authorizationService.isAuthorized(FeatureID.CanSynchronizeWithORCID, dso.self), - this.authorizationService.isAuthorized(FeatureID.CanClaimItem, dso.self), - this.correctionTypeDataService.findByItem(dso.uuid, false).pipe( - getFirstCompletedRemoteData(), - getRemoteDataPayload()), - ]).pipe( - map(([canCreateVersion, disableVersioning, versionTooltip, canSynchronizeWithOrcid, canClaimItem, correction]) => { - const isPerson = this.getDsoType(dso) === 'person'; - return [ - { - id: 'orcid-dso', - active: false, - visible: isPerson && canSynchronizeWithOrcid, - model: { - type: MenuItemType.LINK, - text: 'item.page.orcid.tooltip', - link: new URLCombiner(getDSORoute(dso), 'orcid').toString(), - } as LinkMenuItemModel, - icon: 'orcid fab fa-lg', - index: 0, - }, - { - id: 'version-dso', - active: false, - visible: canCreateVersion, - model: { - type: MenuItemType.ONCLICK, - text: versionTooltip, - disabled: disableVersioning, - function: () => { - this.dsoVersioningModalService.openCreateVersionModal(dso); - }, - } as OnClickMenuItemModel, - icon: 'code-branch', - index: 1, - }, - { - id: 'claim-dso', - active: false, - visible: isPerson && canClaimItem, - model: { - type: MenuItemType.ONCLICK, - text: 'item.page.claim.button', - function: () => { - this.claimResearcher(dso); - }, - } as OnClickMenuItemModel, - icon: 'hand-paper', - index: 3, - }, - { - id: 'withdrawn-item', - active: false, - visible: dso.isArchived && correction?.page.some((c) => c.topic === REQUEST_WITHDRAWN), - model: { - type: MenuItemType.ONCLICK, - text:'item.page.withdrawn', - function: () => { - this.dsoWithdrawnReinstateModalService.openCreateWithdrawnReinstateModal(dso, 'request-withdrawn', dso.isArchived); - }, - } as OnClickMenuItemModel, - icon: 'eye-slash', - index: 4, - }, - { - id: 'reinstate-item', - active: false, - visible: dso.isWithdrawn && correction?.page.some((c) => c.topic === REQUEST_REINSTATE), - model: { - type: MenuItemType.ONCLICK, - text:'item.page.reinstate', - function: () => { - this.dsoWithdrawnReinstateModalService.openCreateWithdrawnReinstateModal(dso, 'request-reinstate', dso.isArchived); - }, - } as OnClickMenuItemModel, - icon: 'eye', - index: 5, - }, - ]; - }), - ); - } else { - return observableOf([]); - } - } - - /** - * Get Community/Collection-specific menus - */ - protected getComColMenu(dso): Observable { - if (dso instanceof Community || dso instanceof Collection) { - return combineLatest([ - this.authorizationService.isAuthorized(FeatureID.CanSubscribe, dso.self), - ]).pipe( - map(([canSubscribe]) => { - return [ - { - id: 'subscribe', - active: false, - visible: canSubscribe, - model: { - type: MenuItemType.ONCLICK, - text: 'subscriptions.tooltip', - function: () => { - const modalRef = this.modalService.open(SubscriptionModalComponent); - modalRef.componentInstance.dso = dso; - }, - } as OnClickMenuItemModel, - icon: 'bell', - index: 4, - }, - ]; - }), - ); - } else { - return observableOf([]); - } - } - - /** - * Claim a researcher by creating a profile - * Shows notifications and/or hides the menu section on success/error - */ - protected claimResearcher(dso) { - this.researcherProfileService.createFromExternalSourceAndReturnRelatedItemId(dso.self) - .subscribe((id: string) => { - if (isNotEmpty(id)) { - this.notificationsService.success(this.translate.get('researcherprofile.success.claim.title'), - this.translate.get('researcherprofile.success.claim.body')); - this.authorizationService.invalidateAuthorizationsRequestCache(); - this.menuService.hideMenuSection(MenuID.DSO_EDIT, 'claim-dso-' + dso.uuid); - } else { - this.notificationsService.error( - this.translate.get('researcherprofile.error.claim.title'), - this.translate.get('researcherprofile.error.claim.body')); - } - }); - } - - /** - * Retrieve the dso or entity type for an object to be used in generic messages - */ - protected getDsoType(dso) { - const renderType = dso.getRenderTypes()[0]; - if (typeof renderType === 'string' || renderType instanceof String) { - return renderType.toLowerCase(); - } else { - return dso.type.toString().toLowerCase(); - } - } - - /** - * Add the dso uuid to all provided menu ids and parent ids - */ - protected addDsoUuidToMenuIDs(menus, dso) { - return menus.map((menu) => { - Object.assign(menu, { - id: menu.id + '-' + dso.uuid, - }); - if (hasValue(menu.parentID)) { - Object.assign(menu, { - parentID: menu.parentID + '-' + dso.uuid, - }); - } - return menu; - }); - } - -} +export const dsoEditMenuResolver: ResolveFn<{ [key: string]: MenuSection[] }> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + menuResolverService: DSOEditMenuResolverService = inject(DSOEditMenuResolverService), +): Observable<{ [key: string]: MenuSection[] }> => { + return menuResolverService.resolve(route, state); +}; diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts index 2aa5485371..1ef9a9e350 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts @@ -53,6 +53,7 @@ import { DynamicFormValidationService, DynamicTemplateDirective, } from '@ng-dynamic-forms/core'; +import { DynamicFormControlMapFn } from '@ng-dynamic-forms/core/lib/service/dynamic-form-component.service'; import { Store } from '@ngrx/store'; import { TranslateModule, @@ -74,7 +75,6 @@ import { import { APP_CONFIG, AppConfig, - DynamicFormControlFn, } from '../../../../../config/app-config.interface'; import { AppState } from '../../../../app.reducer'; import { ItemDataService } from '../../../../core/data/item-data.service'; @@ -214,7 +214,7 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo public formBuilderService: FormBuilderService, private submissionService: SubmissionService, @Inject(APP_CONFIG) protected appConfig: AppConfig, - @Inject(DYNAMIC_FORM_CONTROL_MAP_FN) protected dynamicFormControlFn: DynamicFormControlFn, + @Inject(DYNAMIC_FORM_CONTROL_MAP_FN) protected dynamicFormControlFn: DynamicFormControlMapFn, ) { super(ref, componentFactoryResolver, layoutService, validationService, dynamicFormComponentService, relationService); this.fetchThumbnail = this.appConfig.browseBy.showThumbnails; diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.component.spec.ts index 535b88561a..05da3cc583 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.component.spec.ts @@ -43,6 +43,7 @@ import { FormRowModel } from '../../../../../../core/config/models/config-submis import { SubmissionFormsModel } from '../../../../../../core/config/models/config-submission-forms.model'; import { SubmissionObjectDataService } from '../../../../../../core/submission/submission-object-data.service'; import { VocabularyService } from '../../../../../../core/submission/vocabularies/vocabulary.service'; +import { XSRFService } from '../../../../../../core/xsrf/xsrf.service'; import { SubmissionService } from '../../../../../../submission/submission.service'; import { createTestComponent } from '../../../../../testing/utils.test'; import { VocabularyServiceStub } from '../../../../../testing/vocabulary-service.stub'; @@ -180,6 +181,7 @@ describe('DsDynamicRelationGroupComponent test suite', () => { { provide: DsDynamicTypeBindRelationService, useClass: DsDynamicTypeBindRelationService }, { provide: SubmissionObjectDataService, useValue: {} }, { provide: SubmissionService, useValue: {} }, + { provide: XSRFService, useValue: {} }, { provide: APP_CONFIG, useValue: environment }, { provide: APP_DATA_SERVICES_MAP, useValue: {} }, { provide: DYNAMIC_FORM_CONTROL_MAP_FN, useValue: dsDynamicFormControlMapFn }, diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.spec.ts index 4995b36f11..94de9eaaa7 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.spec.ts @@ -33,6 +33,7 @@ import { ExternalSource } from '../../../../../core/shared/external-source.model import { Item } from '../../../../../core/shared/item.model'; import { SearchConfigurationService } from '../../../../../core/shared/search/search-configuration.service'; import { WorkspaceItem } from '../../../../../core/submission/models/workspaceitem.model'; +import { XSRFService } from '../../../../../core/xsrf/xsrf.service'; import { ItemSearchResult } from '../../../../object-collection/shared/item-search-result.model'; import { SelectableListService } from '../../../../object-list/selectable-list/selectable-list.service'; import { createSuccessfulRemoteDataObject$ } from '../../../../remote-data.utils'; @@ -147,6 +148,7 @@ describe('DsDynamicLookupRelationModalComponent', () => { }, }, }, + { provide: XSRFService, useValue: {} }, { provide: NgZone, useValue: new NgZone({}) }, { provide: APP_DATA_SERVICES_MAP, useValue: {} }, NgbActiveModal, diff --git a/src/app/shared/form/chips/chips.component.ts b/src/app/shared/form/chips/chips.component.ts index 1171e17753..ff05e6d2b7 100644 --- a/src/app/shared/form/chips/chips.component.ts +++ b/src/app/shared/form/chips/chips.component.ts @@ -106,8 +106,8 @@ export class ChipsComponent implements OnChanges { } onDrop(event: CdkDragDrop) { - console.log('onDrop', event); moveItemInArray(this.chips.chipsItems.getValue(), event.previousIndex, event.currentIndex); + this.dragged = -1; this.chips.updateOrder(); this.isDragging.next(false); } diff --git a/src/app/shared/form/form.component.spec.ts b/src/app/shared/form/form.component.spec.ts index f88d63f4c2..431ae3387f 100644 --- a/src/app/shared/form/form.component.spec.ts +++ b/src/app/shared/form/form.component.spec.ts @@ -33,6 +33,7 @@ import { BehaviorSubject } from 'rxjs'; import { APP_DATA_SERVICES_MAP } from '../../../config/app-config.interface'; import { storeModuleConfig } from '../../app.reducer'; +import { XSRFService } from '../../core/xsrf/xsrf.service'; import { StoreMock } from '../testing/store.mock'; import { createTestComponent } from '../testing/utils.test'; import { DsDynamicFormComponent } from './builder/ds-dynamic-form-ui/ds-dynamic-form.component'; @@ -176,6 +177,7 @@ describe('FormComponent test suite', () => { FormComponent, FormService, { provide: Store, useClass: StoreMock }, + { provide: XSRFService, useValue: {} }, ], schemas: [CUSTOM_ELEMENTS_SCHEMA], }) diff --git a/src/app/shared/object-collection/shared/listable-object/listable-object-component-loader.component.spec.ts b/src/app/shared/object-collection/shared/listable-object/listable-object-component-loader.component.spec.ts index 449c5b6eb7..e09845a396 100644 --- a/src/app/shared/object-collection/shared/listable-object/listable-object-component-loader.component.spec.ts +++ b/src/app/shared/object-collection/shared/listable-object/listable-object-component-loader.component.spec.ts @@ -18,6 +18,7 @@ import { of } from 'rxjs'; import { AuthRequestService } from 'src/app/core/auth/auth-request.service'; import { CookieService } from 'src/app/core/services/cookie.service'; import { HardRedirectService } from 'src/app/core/services/hard-redirect.service'; +import { XSRFService } from 'src/app/core/xsrf/xsrf.service'; import { CookieServiceMock } from 'src/app/shared/mocks/cookie.service.mock'; import { getMockThemeService } from 'src/app/shared/mocks/theme-service.mock'; import { AuthRequestServiceStub } from 'src/app/shared/testing/auth-request-service.stub'; @@ -70,6 +71,7 @@ describe('ListableObjectComponentLoaderComponent', () => { { provide: HardRedirectService, useValue: jasmine.createSpyObj('hardRedirectService', ['redirect']) }, { provide: AuthRequestService, useValue: new AuthRequestServiceStub() }, { provide: CookieService, useValue: new CookieServiceMock() }, + { provide: XSRFService, useValue: {} }, { provide: REQUEST, useValue: {} }, { provide: ActivatedRoute, diff --git a/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component.spec.ts b/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component.spec.ts index b7eb800764..77225f3ed5 100644 --- a/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component.spec.ts +++ b/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component.spec.ts @@ -32,6 +32,7 @@ import { HALEndpointService } from '../../../../core/shared/hal-endpoint.service import { Item } from '../../../../core/shared/item.model'; import { SearchService } from '../../../../core/shared/search/search.service'; import { UUIDService } from '../../../../core/shared/uuid.service'; +import { XSRFService } from '../../../../core/xsrf/xsrf.service'; import { AuthServiceMock } from '../../../../shared/mocks/auth.service.mock'; import { getMockThemeService } from '../../../../shared/mocks/theme-service.mock'; import { SearchServiceStub } from '../../../../shared/testing/search-service.stub'; @@ -114,6 +115,7 @@ describe('ItemDetailPreviewComponent', () => { { provide: HALEndpointService, useValue: new HALEndpointServiceStub('workspaceitems') }, { provide: ObjectCacheService, useValue: {} }, { provide: UUIDService, useValue: {} }, + { provide: XSRFService, useValue: {} }, { provide: Store, useValue: {} }, { provide: RemoteDataBuildService, useValue: {} }, { provide: CommunityDataService, useValue: {} }, diff --git a/src/app/shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component.spec.ts b/src/app/shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component.spec.ts index f8a40249b0..bfec6ebeac 100644 --- a/src/app/shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component.spec.ts +++ b/src/app/shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component.spec.ts @@ -26,6 +26,7 @@ import { DSOChangeAnalyzer } from '../../../../core/data/dso-change-analyzer.ser import { Collection } from '../../../../core/shared/collection.model'; import { HALEndpointService } from '../../../../core/shared/hal-endpoint.service'; import { UUIDService } from '../../../../core/shared/uuid.service'; +import { XSRFService } from '../../../../core/xsrf/xsrf.service'; import { NotificationsService } from '../../../notifications/notifications.service'; import { CollectionSearchResult } from '../../../object-collection/shared/collection-search-result.model'; import { ActivatedRouteStub } from '../../../testing/active-router.stub'; @@ -95,6 +96,7 @@ describe('CollectionSearchResultGridElementComponent', () => { { provide: DSOChangeAnalyzer, useValue: {} }, { provide: DefaultChangeAnalyzer, useValue: {} }, { provide: BitstreamFormatDataService, useValue: {} }, + { provide: XSRFService, useValue: {} }, { provide: LinkService, useValue: linkService }, provideMockStore({}), ], diff --git a/src/app/shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component.spec.ts b/src/app/shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component.spec.ts index f60d0c5454..86d757a030 100644 --- a/src/app/shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component.spec.ts +++ b/src/app/shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component.spec.ts @@ -26,6 +26,7 @@ import { DSOChangeAnalyzer } from '../../../../core/data/dso-change-analyzer.ser import { Community } from '../../../../core/shared/community.model'; import { HALEndpointService } from '../../../../core/shared/hal-endpoint.service'; import { UUIDService } from '../../../../core/shared/uuid.service'; +import { XSRFService } from '../../../../core/xsrf/xsrf.service'; import { AuthServiceMock } from '../../../../shared/mocks/auth.service.mock'; import { getMockThemeService } from '../../../../shared/mocks/theme-service.mock'; import { StoreMock } from '../../../../shared/testing/store.mock'; @@ -100,6 +101,7 @@ describe('CommunitySearchResultGridElementComponent', () => { { provide: ActivatedRoute, useValue: new ActivatedRouteStub() }, { provide: ThemeService, useValue: getMockThemeService() }, { provide: AuthService, useValue: new AuthServiceMock() }, + { provide: XSRFService, useValue: {} }, ], schemas: [NO_ERRORS_SCHEMA], }).overrideComponent(CommunitySearchResultGridElementComponent, { diff --git a/src/app/shared/object-list/item-list-element/item-types/item/item-list-element.component.spec.ts b/src/app/shared/object-list/item-list-element/item-types/item/item-list-element.component.spec.ts index 597b3cfae1..47a8aa6502 100644 --- a/src/app/shared/object-list/item-list-element/item-types/item/item-list-element.component.spec.ts +++ b/src/app/shared/object-list/item-list-element/item-types/item/item-list-element.component.spec.ts @@ -17,6 +17,7 @@ import { AuthService } from '../../../../../core/auth/auth.service'; import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service'; import { AuthorizationDataService } from '../../../../../core/data/feature-authorization/authorization-data.service'; import { Item } from '../../../../../core/shared/item.model'; +import { XSRFService } from '../../../../../core/xsrf/xsrf.service'; import { AuthServiceMock } from '../../../../../shared/mocks/auth.service.mock'; import { mockTruncatableService } from '../../../../../shared/mocks/mock-trucatable.service'; import { getMockThemeService } from '../../../../../shared/mocks/theme-service.mock'; @@ -78,6 +79,7 @@ describe('ItemListElementComponent', () => { { provide: ActivatedRoute, useValue: new ActivatedRouteStub() }, { provide: AuthService, useValue: new AuthServiceMock() }, { provide: AuthorizationDataService, useValue: {} }, + { provide: XSRFService, useValue: {} }, ], schemas: [NO_ERRORS_SCHEMA], }).overrideComponent(ItemListElementComponent, { diff --git a/src/app/shared/resource-policies/resolvers/resource-policy-target.resolver.ts b/src/app/shared/resource-policies/resolvers/resource-policy-target.resolver.ts index 131824d144..76748efad1 100644 --- a/src/app/shared/resource-policies/resolvers/resource-policy-target.resolver.ts +++ b/src/app/shared/resource-policies/resolvers/resource-policy-target.resolver.ts @@ -1,21 +1,18 @@ import { - Inject, - Injectable, + inject, InjectionToken, Injector, } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, Router, RouterStateSnapshot, } from '@angular/router'; import { Observable } from 'rxjs'; import { switchMap } from 'rxjs/operators'; -import { - APP_DATA_SERVICES_MAP, - LazyDataServicesMap, -} from '../../../../config/app-config.interface'; +import { LazyDataServicesMap } from '../../../../config/app-config.interface'; import { IdentifiableDataService } from '../../../core/data/base/identifiable-data.service'; import { RemoteData } from '../../../core/data/remote-data'; import { lazyService } from '../../../core/lazy-service'; @@ -25,40 +22,36 @@ import { ResourceType } from '../../../core/shared/resource-type'; import { isEmpty } from '../../empty.util'; /** - * This class represents a resolver that requests a specific item before the route is activated + * Method for resolving an item based on the parameters in the current route + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @param dataServiceMap + * @param parentInjector + * @param router + * @returns Observable<> Emits the found item based on the parameters in the current route, + * or an error if something went wrong */ -@Injectable({ providedIn: 'root' }) -export class ResourcePolicyTargetResolver { +export const resourcePolicyTargetResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + dataServiceMap: InjectionToken = inject(InjectionToken), + parentInjector: Injector = inject(Injector), + router: Router = inject(Router), +): Observable> => { + const targetType = route.queryParamMap.get('targetType'); + const policyTargetId = route.queryParamMap.get('policyTargetId'); - constructor( - private parentInjector: Injector, - private router: Router, - @Inject(APP_DATA_SERVICES_MAP) private dataServiceMap: InjectionToken) { + if (isEmpty(targetType) || isEmpty(policyTargetId)) { + router.navigateByUrl('/404', { skipLocationChange: true }); } - /** - * Method for resolving an item based on the parameters in the current route - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - * @param {RouterStateSnapshot} state The current RouterStateSnapshot - * @returns Observable<> Emits the found item based on the parameters in the current route, - * or an error if something went wrong - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - const targetType = route.queryParamMap.get('targetType'); - const policyTargetId = route.queryParamMap.get('policyTargetId'); + const resourceType: ResourceType = new ResourceType(targetType); + const lazyProvider$: Observable> = lazyService(dataServiceMap[resourceType.value], parentInjector); - if (isEmpty(targetType) || isEmpty(policyTargetId)) { - this.router.navigateByUrl('/404', { skipLocationChange: true }); - } - - const resourceType: ResourceType = new ResourceType(targetType); - const lazyProvider$: Observable> = lazyService(this.dataServiceMap[resourceType.value], this.parentInjector); - - return lazyProvider$.pipe( - switchMap((dataService: IdentifiableDataService) => { - return dataService.findById(policyTargetId); - }), - getFirstCompletedRemoteData(), - ); - } -} + return lazyProvider$.pipe( + switchMap((dataService: IdentifiableDataService) => { + return dataService.findById(policyTargetId); + }), + getFirstCompletedRemoteData(), + ); +}; diff --git a/src/app/shared/resource-policies/resolvers/resource-policy.resolver.ts b/src/app/shared/resource-policies/resolvers/resource-policy.resolver.ts index e72b061956..70f4b48dcf 100644 --- a/src/app/shared/resource-policies/resolvers/resource-policy.resolver.ts +++ b/src/app/shared/resource-policies/resolvers/resource-policy.resolver.ts @@ -1,6 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, Router, RouterStateSnapshot, } from '@angular/router'; @@ -14,30 +15,27 @@ import { isEmpty } from '../../empty.util'; import { followLink } from '../../utils/follow-link-config.model'; /** - * This class represents a resolver that requests a specific item before the route is activated + * Method for resolving an item based on the parameters in the current route + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @param {Router} router + * @param {ResourcePolicyDataService} resourcePolicyService + * @returns Observable<> Emits the found item based on the parameters in the current route, + * or an error if something went wrong */ -@Injectable({ providedIn: 'root' }) -export class ResourcePolicyResolver { +export const resourcePolicyResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + router: Router = inject(Router), + resourcePolicyService: ResourcePolicyDataService = inject(ResourcePolicyDataService), +): Observable> => { + const policyId = route.queryParamMap.get('policyId'); - constructor(private resourcePolicyService: ResourcePolicyDataService, private router: Router) { + if (isEmpty(policyId)) { + router.navigateByUrl('/404', { skipLocationChange: true }); } - /** - * Method for resolving an item based on the parameters in the current route - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - * @param {RouterStateSnapshot} state The current RouterStateSnapshot - * @returns Observable<> Emits the found item based on the parameters in the current route, - * or an error if something went wrong - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - const policyId = route.queryParamMap.get('policyId'); - - if (isEmpty(policyId)) { - this.router.navigateByUrl('/404', { skipLocationChange: true }); - } - - return this.resourcePolicyService.findById(policyId, true, false, followLink('eperson'), followLink('group')).pipe( - getFirstCompletedRemoteData(), - ); - } -} + return resourcePolicyService.findById(policyId, true, false, followLink('eperson'), followLink('group')).pipe( + getFirstCompletedRemoteData(), + ); +}; diff --git a/src/app/shared/search/search.component.spec.ts b/src/app/shared/search/search.component.spec.ts index fec23281cd..8c7b5b06ff 100644 --- a/src/app/shared/search/search.component.spec.ts +++ b/src/app/shared/search/search.component.spec.ts @@ -46,6 +46,7 @@ import { SearchConfig, SortConfig, } from '../../core/shared/search/search-filters/search-config.model'; +import { XSRFService } from '../../core/xsrf/xsrf.service'; import { SEARCH_CONFIG_SERVICE } from '../../my-dspace-page/my-dspace-configuration.service'; import { HostWindowService } from '../host-window.service'; import { PaginationComponentOptions } from '../pagination/pagination-component-options.model'; @@ -237,6 +238,7 @@ export function configureSearchComponentTestingModule(compType, additionalDeclar provide: SearchFilterService, useValue: {}, }, + { provide: XSRFService, useValue: {} }, { provide: SEARCH_CONFIG_SERVICE, useValue: searchConfigurationServiceStub, diff --git a/src/app/statistics-page/statistics-page-routes.ts b/src/app/statistics-page/statistics-page-routes.ts index 3373df5e3b..41a91ee3aa 100644 --- a/src/app/statistics-page/statistics-page-routes.ts +++ b/src/app/statistics-page/statistics-page-routes.ts @@ -1,10 +1,13 @@ -import { Route } from '@angular/router'; +import { + mapToCanActivate, + Route, +} from '@angular/router'; -import { CollectionPageResolver } from '../collection-page/collection-page.resolver'; -import { CommunityPageResolver } from '../community-page/community-page.resolver'; -import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; +import { collectionPageResolver } from '../collection-page/collection-page.resolver'; +import { communityPageResolver } from '../community-page/community-page.resolver'; +import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; import { StatisticsAdministratorGuard } from '../core/data/feature-authorization/feature-authorization-guard/statistics-administrator.guard'; -import { ItemResolver } from '../item-page/item.resolver'; +import { itemResolver } from '../item-page/item.resolver'; import { ThemedCollectionStatisticsPageComponent } from './collection-statistics-page/themed-collection-statistics-page.component'; import { ThemedCommunityStatisticsPageComponent } from './community-statistics-page/themed-community-statistics-page.component'; import { ThemedItemStatisticsPageComponent } from './item-statistics-page/themed-item-statistics-page.component'; @@ -14,7 +17,7 @@ export const ROUTES: Route[] = [ { path: '', resolve: { - breadcrumb: I18nBreadcrumbResolver, + breadcrumb: i18nBreadcrumbResolver, }, data: { title: 'statistics.title', @@ -26,45 +29,45 @@ export const ROUTES: Route[] = [ component: ThemedSiteStatisticsPageComponent, }, ], - canActivate: [StatisticsAdministratorGuard], + canActivate: mapToCanActivate([StatisticsAdministratorGuard]), }, { path: `items/:id`, resolve: { - scope: ItemResolver, - breadcrumb: I18nBreadcrumbResolver, + scope: itemResolver, + breadcrumb: i18nBreadcrumbResolver, }, data: { title: 'statistics.title', breadcrumbKey: 'statistics', }, component: ThemedItemStatisticsPageComponent, - canActivate: [StatisticsAdministratorGuard], + canActivate: mapToCanActivate([StatisticsAdministratorGuard]), }, { path: `collections/:id`, resolve: { - scope: CollectionPageResolver, - breadcrumb: I18nBreadcrumbResolver, + scope: collectionPageResolver, + breadcrumb: i18nBreadcrumbResolver, }, data: { title: 'statistics.title', breadcrumbKey: 'statistics', }, component: ThemedCollectionStatisticsPageComponent, - canActivate: [StatisticsAdministratorGuard], + canActivate: mapToCanActivate([StatisticsAdministratorGuard]), }, { path: `communities/:id`, resolve: { - scope: CommunityPageResolver, - breadcrumb: I18nBreadcrumbResolver, + scope: communityPageResolver, + breadcrumb: i18nBreadcrumbResolver, }, data: { title: 'statistics.title', breadcrumbKey: 'statistics', }, component: ThemedCommunityStatisticsPageComponent, - canActivate: [StatisticsAdministratorGuard], + canActivate: mapToCanActivate([StatisticsAdministratorGuard]), }, ]; diff --git a/src/app/submission/edit/submission-edit.component.spec.ts b/src/app/submission/edit/submission-edit.component.spec.ts index 5469078f6f..59f9883f19 100644 --- a/src/app/submission/edit/submission-edit.component.spec.ts +++ b/src/app/submission/edit/submission-edit.component.spec.ts @@ -22,6 +22,7 @@ import { AuthService } from '../../core/auth/auth.service'; import { ItemDataService } from '../../core/data/item-data.service'; import { HALEndpointService } from '../../core/shared/hal-endpoint.service'; import { SubmissionJsonPatchOperationsService } from '../../core/submission/submission-json-patch-operations.service'; +import { XSRFService } from '../../core/xsrf/xsrf.service'; import { mockSubmissionObject } from '../../shared/mocks/submission.mock'; import { getMockThemeService } from '../../shared/mocks/theme-service.mock'; import { NotificationsService } from '../../shared/notifications/notifications.service'; @@ -83,6 +84,7 @@ describe('SubmissionEditComponent Component', () => { { provide: HALEndpointService, useValue: halService }, { provide: SectionsService, useValue: new SectionsServiceStub() }, { provide: ThemeService, useValue: themeService }, + { provide: XSRFService, useValue: {} }, { provide: APP_DATA_SERVICES_MAP, useValue: {} }, provideMockStore(), ], diff --git a/src/app/submission/sections/accesses/section-accesses.component.spec.ts b/src/app/submission/sections/accesses/section-accesses.component.spec.ts index 6828714950..c49bd74e0d 100644 --- a/src/app/submission/sections/accesses/section-accesses.component.spec.ts +++ b/src/app/submission/sections/accesses/section-accesses.component.spec.ts @@ -24,6 +24,7 @@ import { SubmissionAccessesConfigDataService } from '../../../core/config/submis import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder'; import { SubmissionJsonPatchOperationsService } from '../../../core/submission/submission-json-patch-operations.service'; import { SubmissionObjectDataService } from '../../../core/submission/submission-object-data.service'; +import { XSRFService } from '../../../core/xsrf/xsrf.service'; import { dsDynamicFormControlMapFn } from '../../../shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-map-fn'; import { DsDynamicTypeBindRelationService } from '../../../shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service'; import { FormBuilderService } from '../../../shared/form/builder/form-builder.service'; @@ -120,6 +121,7 @@ describe('SubmissionSectionAccessesComponent', () => { { provide: DsDynamicTypeBindRelationService, useValue: getMockDsDynamicTypeBindRelationService() }, { provide: SubmissionObjectDataService, useValue: {} }, { provide: SubmissionService, useValue: {} }, + { provide: XSRFService, useValue: {} }, { provide: APP_CONFIG, useValue: environment }, { provide: APP_DATA_SERVICES_MAP, useValue: {} }, { provide: DYNAMIC_FORM_CONTROL_MAP_FN, useValue: dsDynamicFormControlMapFn }, @@ -216,6 +218,7 @@ describe('SubmissionSectionAccessesComponent', () => { { provide: DsDynamicTypeBindRelationService, useValue: getMockDsDynamicTypeBindRelationService() }, { provide: SubmissionObjectDataService, useValue: {} }, { provide: SubmissionService, useValue: {} }, + { provide: XSRFService, useValue: {} }, { provide: APP_CONFIG, useValue: environment }, { provide: APP_DATA_SERVICES_MAP, useValue: {} }, { provide: DYNAMIC_FORM_CONTROL_MAP_FN, useValue: dsDynamicFormControlMapFn }, diff --git a/src/app/submission/sections/license/section-license.component.spec.ts b/src/app/submission/sections/license/section-license.component.spec.ts index c3cb329a50..95b2e7f50a 100644 --- a/src/app/submission/sections/license/section-license.component.spec.ts +++ b/src/app/submission/sections/license/section-license.component.spec.ts @@ -38,6 +38,7 @@ import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/jso import { Collection } from '../../../core/shared/collection.model'; import { License } from '../../../core/shared/license.model'; import { SubmissionObjectDataService } from '../../../core/submission/submission-object-data.service'; +import { XSRFService } from '../../../core/xsrf/xsrf.service'; import { dsDynamicFormControlMapFn } from '../../../shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-map-fn'; import { FormBuilderService } from '../../../shared/form/builder/form-builder.service'; import { FormFieldMetadataValueObject } from '../../../shared/form/builder/models/form-field-metadata-value.model'; @@ -191,6 +192,7 @@ describe('SubmissionSectionLicenseComponent test suite', () => { findById: () => observableOf(createSuccessfulRemoteDataObject(mockSubmissionObject)), }, }, + { provide: XSRFService, useValue: {} }, SubmissionSectionLicenseComponent, ], schemas: [NO_ERRORS_SCHEMA], diff --git a/src/app/submission/sections/upload/accessConditions/submission-section-upload-access-conditions.component.ts b/src/app/submission/sections/upload/accessConditions/submission-section-upload-access-conditions.component.ts index 95297772a3..84eff58f72 100644 --- a/src/app/submission/sections/upload/accessConditions/submission-section-upload-access-conditions.component.ts +++ b/src/app/submission/sections/upload/accessConditions/submission-section-upload-access-conditions.component.ts @@ -7,13 +7,13 @@ import { Input, OnInit, } from '@angular/core'; -import { find } from 'rxjs/operators'; import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; import { RemoteData } from '../../../../core/data/remote-data'; import { GroupDataService } from '../../../../core/eperson/group-data.service'; import { Group } from '../../../../core/eperson/models/group.model'; import { ResourcePolicy } from '../../../../core/resource-policy/models/resource-policy.model'; +import { getFirstCompletedRemoteData } from '../../../../core/shared/operators'; import { isEmpty } from '../../../../shared/empty.util'; /** @@ -55,13 +55,15 @@ export class SubmissionSectionUploadAccessConditionsComponent implements OnInit this.accessConditions.forEach((accessCondition: ResourcePolicy) => { if (isEmpty(accessCondition.name)) { this.groupService.findByHref(accessCondition._links.group.href).pipe( - find((rd: RemoteData) => !rd.isResponsePending && rd.hasSucceeded)) - .subscribe((rd: RemoteData) => { + getFirstCompletedRemoteData(), + ).subscribe((rd: RemoteData) => { + if (rd.hasSucceeded) { const group: Group = rd.payload; const accessConditionEntry = Object.assign({}, accessCondition); accessConditionEntry.name = this.dsoNameService.getName(group); this.accessConditionsList.push(accessConditionEntry); - }); + } + }); } else { this.accessConditionsList.push(accessCondition); } diff --git a/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.spec.ts b/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.spec.ts index 8543ee4089..015ccd4ae4 100644 --- a/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.spec.ts +++ b/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.spec.ts @@ -38,6 +38,7 @@ import { environment } from '../../../../../../environments/environment.test'; import { JsonPatchOperationPathCombiner } from '../../../../../core/json-patch/builder/json-patch-operation-path-combiner'; import { JsonPatchOperationsBuilder } from '../../../../../core/json-patch/builder/json-patch-operations-builder'; import { SubmissionJsonPatchOperationsService } from '../../../../../core/submission/submission-json-patch-operations.service'; +import { XSRFService } from '../../../../../core/xsrf/xsrf.service'; import { dateToISOFormat } from '../../../../../shared/date.util'; import { DsDynamicTypeBindRelationService } from '../../../../../shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service'; import { DynamicCustomSwitchModel } from '../../../../../shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.model'; @@ -154,6 +155,7 @@ describe('SubmissionSectionUploadFileEditComponent test suite', () => { { provide: DsDynamicTypeBindRelationService, useValue: getMockDsDynamicTypeBindRelationService() }, { provide: APP_CONFIG, useValue: environment }, { provide: APP_DATA_SERVICES_MAP, useValue: {} }, + { provide: XSRFService, useValue: {} }, ], schemas: [NO_ERRORS_SCHEMA], }).compileComponents().then(); diff --git a/src/app/submit-page/submit-page-routes.ts b/src/app/submit-page/submit-page-routes.ts index fc3b7113d1..338a81af3e 100644 --- a/src/app/submit-page/submit-page-routes.ts +++ b/src/app/submit-page/submit-page-routes.ts @@ -1,17 +1,17 @@ import { Route } from '@angular/router'; -import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; -import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; +import { authenticatedGuard } from '../core/auth/authenticated.guard'; +import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; import { ThemedSubmissionSubmitComponent } from '../submission/submit/themed-submission-submit.component'; export const ROUTES: Route[] = [ { - canActivate: [AuthenticatedGuard], + canActivate: [authenticatedGuard], path: '', pathMatch: 'full', component: ThemedSubmissionSubmitComponent, resolve: { - breadcrumb: I18nBreadcrumbResolver, + breadcrumb: i18nBreadcrumbResolver, }, data: { title: 'submission.submit.title', breadcrumbKey: 'submission.submit' }, }, diff --git a/src/app/suggestions-page/suggestions-page-routes.ts b/src/app/suggestions-page/suggestions-page-routes.ts index 392a0e4d98..f270a1ef66 100644 --- a/src/app/suggestions-page/suggestions-page-routes.ts +++ b/src/app/suggestions-page/suggestions-page-routes.ts @@ -1,23 +1,23 @@ import { Route } from '@angular/router'; -import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; -import { PublicationClaimBreadcrumbResolver } from '../core/breadcrumbs/publication-claim-breadcrumb.resolver'; +import { authenticatedGuard } from '../core/auth/authenticated.guard'; +import { publicationClaimBreadcrumbResolver } from '../core/breadcrumbs/publication-claim-breadcrumb.resolver'; import { SuggestionsPageComponent } from './suggestions-page.component'; -import { SuggestionsPageResolver } from './suggestions-page.resolver'; +import { suggestionsPageResolver } from './suggestions-page.resolver'; export const ROUTES: Route[] = [ { path: ':targetId', resolve: { - suggestionTargets: SuggestionsPageResolver, - breadcrumb: PublicationClaimBreadcrumbResolver,//I18nBreadcrumbResolver + suggestionTargets: suggestionsPageResolver, + breadcrumb: publicationClaimBreadcrumbResolver,//i18nBreadcrumbResolver }, data: { title: 'admin.notifications.publicationclaim.page.title', breadcrumbKey: 'admin.notifications.publicationclaim', showBreadcrumbsFluid: false, }, - canActivate: [AuthenticatedGuard], + canActivate: [authenticatedGuard], runGuardsAndResolvers: 'always', component: SuggestionsPageComponent, }, diff --git a/src/app/suggestions-page/suggestions-page.resolver.ts b/src/app/suggestions-page/suggestions-page.resolver.ts index 3d6d0f7b9d..48c5a40f31 100644 --- a/src/app/suggestions-page/suggestions-page.resolver.ts +++ b/src/app/suggestions-page/suggestions-page.resolver.ts @@ -1,6 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; import { Observable } from 'rxjs'; @@ -12,23 +13,19 @@ import { SuggestionTargetDataService } from '../core/notifications/target/sugges import { hasValue } from '../shared/empty.util'; /** - * This class represents a resolver that requests a specific collection before the route is activated + * Method for resolving a suggestion target based on the parameters in the current route + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @param {SuggestionTargetDataService} suggestionsDataService + * @returns Observable<> Emits the found collection based on the parameters in the current route, + * or an error if something went wrong */ -@Injectable({ providedIn: 'root' }) -export class SuggestionsPageResolver { - constructor(private suggestionsDataService: SuggestionTargetDataService) { - } - - /** - * Method for resolving a suggestion target based on the parameters in the current route - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - * @param {RouterStateSnapshot} state The current RouterStateSnapshot - * @returns Observable<> Emits the found collection based on the parameters in the current route, - * or an error if something went wrong - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - return this.suggestionsDataService.getTargetById(route.params.targetId).pipe( - find((RD) => hasValue(RD.hasFailed) || RD.hasSucceeded), - ); - } -} +export const suggestionsPageResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + suggestionsDataService: SuggestionTargetDataService = inject(SuggestionTargetDataService), +): Observable> => { + return suggestionsDataService.getTargetById(route.params.targetId).pipe( + find((RD) => hasValue(RD.hasFailed) || RD.hasSucceeded), + ); +}; diff --git a/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.spec.ts b/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.spec.ts index 5804b469b8..2616b2accf 100644 --- a/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.spec.ts +++ b/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.spec.ts @@ -311,6 +311,14 @@ describe('SystemWideAlertFormComponent', () => { expect(comp.back).not.toHaveBeenCalled(); }); + it('should not create the new alert when the enable button is clicked on an invalid the form', () => { + spyOn(comp as any, 'handleResponse'); + + comp.formMessage.patchValue(''); + comp.save(); + + expect((comp as any).handleResponse).not.toHaveBeenCalled(); + }); }); describe('back', () => { it('should navigate back to the home page', () => { diff --git a/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.ts b/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.ts index 5a0ab9d3e2..b30e864fa1 100644 --- a/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.ts +++ b/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.ts @@ -256,11 +256,13 @@ export class SystemWideAlertFormComponent implements OnInit { } else { alert.countdownTo = null; } - if (hasValue(this.currentAlert)) { - const updatedAlert = Object.assign(new SystemWideAlert(), this.currentAlert, alert); - this.handleResponse(this.systemWideAlertDataService.put(updatedAlert), 'system-wide-alert.form.update', navigateToHomePage); - } else { - this.handleResponse(this.systemWideAlertDataService.create(alert), 'system-wide-alert.form.create', navigateToHomePage); + if (this.alertForm.valid) { + if (hasValue(this.currentAlert)) { + const updatedAlert = Object.assign(new SystemWideAlert(), this.currentAlert, alert); + this.handleResponse(this.systemWideAlertDataService.put(updatedAlert), 'system-wide-alert.form.update', navigateToHomePage); + } else { + this.handleResponse(this.systemWideAlertDataService.create(alert), 'system-wide-alert.form.create', navigateToHomePage); + } } } diff --git a/src/app/system-wide-alert/system-wide-alert-routes.ts b/src/app/system-wide-alert/system-wide-alert-routes.ts index 435ff8c900..2f3596825b 100644 --- a/src/app/system-wide-alert/system-wide-alert-routes.ts +++ b/src/app/system-wide-alert/system-wide-alert-routes.ts @@ -1,4 +1,7 @@ -import { Route } from '@angular/router'; +import { + mapToCanActivate, + Route, +} from '@angular/router'; import { SiteAdministratorGuard } from '../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard'; import { SystemWideAlertFormComponent } from './alert-form/system-wide-alert-form.component'; @@ -6,7 +9,7 @@ import { SystemWideAlertFormComponent } from './alert-form/system-wide-alert-for export const ROUTES: Route[] = [ { path: '', - canActivate: [SiteAdministratorGuard], + canActivate: mapToCanActivate([SiteAdministratorGuard]), component: SystemWideAlertFormComponent, }, diff --git a/src/app/workflowitems-edit-page/item-from-workflow.resolver.spec.ts b/src/app/workflowitems-edit-page/item-from-workflow.resolver.spec.ts index 5bf6aec2ff..9ee8eaed06 100644 --- a/src/app/workflowitems-edit-page/item-from-workflow.resolver.spec.ts +++ b/src/app/workflowitems-edit-page/item-from-workflow.resolver.spec.ts @@ -2,11 +2,11 @@ import { first } from 'rxjs/operators'; import { WorkflowItemDataService } from '../core/submission/workflowitem-data.service'; import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; -import { ItemFromWorkflowResolver } from './item-from-workflow.resolver'; +import { itemFromWorkflowResolver } from './item-from-workflow.resolver'; -describe('ItemFromWorkflowResolver', () => { +describe('itemFromWorkflowResolver', () => { describe('resolve', () => { - let resolver: ItemFromWorkflowResolver; + let resolver: any; let wfiService: WorkflowItemDataService; const uuid = '1234-65487-12354-1235'; const itemUuid = '8888-8888-8888-8888'; @@ -20,11 +20,11 @@ describe('ItemFromWorkflowResolver', () => { wfiService = { findById: (id: string) => createSuccessfulRemoteDataObject$(wfi), } as any; - resolver = new ItemFromWorkflowResolver(wfiService, null); + resolver = itemFromWorkflowResolver; }); it('should resolve a an item from from the workflow item with the correct id', (done) => { - resolver.resolve({ params: { id: uuid } } as any, undefined) + resolver({ params: { id: uuid } } as any, undefined, wfiService) .pipe(first()) .subscribe( (resolved) => { diff --git a/src/app/workflowitems-edit-page/item-from-workflow.resolver.ts b/src/app/workflowitems-edit-page/item-from-workflow.resolver.ts index 79756f0f7a..e76a147f52 100644 --- a/src/app/workflowitems-edit-page/item-from-workflow.resolver.ts +++ b/src/app/workflowitems-edit-page/item-from-workflow.resolver.ts @@ -1,20 +1,21 @@ -import { Injectable } from '@angular/core'; -import { Store } from '@ngrx/store'; +import { inject } from '@angular/core'; +import { + ActivatedRouteSnapshot, + ResolveFn, + RouterStateSnapshot, +} from '@angular/router'; +import { Observable } from 'rxjs'; +import { RemoteData } from '../core/data/remote-data'; import { Item } from '../core/shared/item.model'; import { SubmissionObjectResolver } from '../core/submission/resolver/submission-object.resolver'; import { WorkflowItemDataService } from '../core/submission/workflowitem-data.service'; -/** - * This class represents a resolver that requests a specific item before the route is activated - */ -@Injectable({ providedIn: 'root' }) -export class ItemFromWorkflowResolver extends SubmissionObjectResolver { - constructor( - private workflowItemService: WorkflowItemDataService, - protected store: Store, - ) { - super(workflowItemService, store); - } +export const itemFromWorkflowResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + workflowItemService: WorkflowItemDataService = inject(WorkflowItemDataService), +): Observable> => { + return SubmissionObjectResolver(route, state, workflowItemService); +}; -} diff --git a/src/app/workflowitems-edit-page/workflow-item-page.resolver.spec.ts b/src/app/workflowitems-edit-page/workflow-item-page.resolver.spec.ts index 21f9e6e00a..02754776a9 100644 --- a/src/app/workflowitems-edit-page/workflow-item-page.resolver.spec.ts +++ b/src/app/workflowitems-edit-page/workflow-item-page.resolver.spec.ts @@ -2,11 +2,11 @@ import { first } from 'rxjs/operators'; import { WorkflowItemDataService } from '../core/submission/workflowitem-data.service'; import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; -import { WorkflowItemPageResolver } from './workflow-item-page.resolver'; +import { workflowItemPageResolver } from './workflow-item-page.resolver'; -describe('WorkflowItemPageResolver', () => { +describe('workflowItemPageResolver', () => { describe('resolve', () => { - let resolver: WorkflowItemPageResolver; + let resolver: any; let wfiService: WorkflowItemDataService; const uuid = '1234-65487-12354-1235'; @@ -14,11 +14,11 @@ describe('WorkflowItemPageResolver', () => { wfiService = { findById: (id: string) => createSuccessfulRemoteDataObject$({ id }), } as any; - resolver = new WorkflowItemPageResolver(wfiService); + resolver = workflowItemPageResolver; }); it('should resolve a workflow item with the correct id', (done) => { - resolver.resolve({ params: { id: uuid } } as any, undefined) + resolver({ params: { id: uuid } } as any, undefined, wfiService) .pipe(first()) .subscribe( (resolved) => { diff --git a/src/app/workflowitems-edit-page/workflow-item-page.resolver.ts b/src/app/workflowitems-edit-page/workflow-item-page.resolver.ts index aa654bfaa5..d0f49d5700 100644 --- a/src/app/workflowitems-edit-page/workflow-item-page.resolver.ts +++ b/src/app/workflowitems-edit-page/workflow-item-page.resolver.ts @@ -1,6 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; import { Observable } from 'rxjs'; @@ -11,28 +12,17 @@ import { WorkflowItem } from '../core/submission/models/workflowitem.model'; import { WorkflowItemDataService } from '../core/submission/workflowitem-data.service'; import { followLink } from '../shared/utils/follow-link-config.model'; -/** - * This class represents a resolver that requests a specific workflow item before the route is activated - */ -@Injectable({ providedIn: 'root' }) -export class WorkflowItemPageResolver { - constructor(private workflowItemService: WorkflowItemDataService) { - } - - /** - * Method for resolving a workflow item based on the parameters in the current route - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - * @param {RouterStateSnapshot} state The current RouterStateSnapshot - * @returns Observable<> Emits the found workflow item based on the parameters in the current route, - * or an error if something went wrong - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - return this.workflowItemService.findById(route.params.id, - true, - false, - followLink('item'), - ).pipe( - getFirstCompletedRemoteData(), - ); - } -} +export const workflowItemPageResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + workflowItemService: WorkflowItemDataService = inject(WorkflowItemDataService), +): Observable> => { + return workflowItemService.findById( + route.params.id, + true, + false, + followLink('item'), + ).pipe( + getFirstCompletedRemoteData(), + ); +}; diff --git a/src/app/workflowitems-edit-page/workflowitems-edit-page-routes.ts b/src/app/workflowitems-edit-page/workflowitems-edit-page-routes.ts index f5b8efaad4..95e1c69de8 100644 --- a/src/app/workflowitems-edit-page/workflowitems-edit-page-routes.ts +++ b/src/app/workflowitems-edit-page/workflowitems-edit-page-routes.ts @@ -1,13 +1,13 @@ import { Routes } from '@angular/router'; -import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; -import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; +import { authenticatedGuard } from '../core/auth/authenticated.guard'; +import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; import { ThemedFullItemPageComponent } from '../item-page/full/themed-full-item-page.component'; import { ThemedSubmissionEditComponent } from '../submission/edit/themed-submission-edit.component'; import { AdvancedWorkflowActionPageComponent } from './advanced-workflow-action/advanced-workflow-action-page/advanced-workflow-action-page.component'; -import { ItemFromWorkflowResolver } from './item-from-workflow.resolver'; +import { itemFromWorkflowResolver } from './item-from-workflow.resolver'; import { ThemedWorkflowItemDeleteComponent } from './workflow-item-delete/themed-workflow-item-delete.component'; -import { WorkflowItemPageResolver } from './workflow-item-page.resolver'; +import { workflowItemPageResolver } from './workflow-item-page.resolver'; import { ThemedWorkflowItemSendBackComponent } from './workflow-item-send-back/themed-workflow-item-send-back.component'; import { ADVANCED_WORKFLOW_PATH, @@ -20,14 +20,14 @@ import { export const ROUTES: Routes = [ { path: ':id', - resolve: { wfi: WorkflowItemPageResolver }, + resolve: { wfi: workflowItemPageResolver }, children: [ { - canActivate: [AuthenticatedGuard], + canActivate: [authenticatedGuard], path: WORKFLOW_ITEM_EDIT_PATH, component: ThemedSubmissionEditComponent, resolve: { - breadcrumb: I18nBreadcrumbResolver, + breadcrumb: i18nBreadcrumbResolver, }, data: { title: 'workflow-item.edit.title', @@ -36,39 +36,39 @@ export const ROUTES: Routes = [ }, }, { - canActivate: [AuthenticatedGuard], + canActivate: [authenticatedGuard], path: WORKFLOW_ITEM_VIEW_PATH, component: ThemedFullItemPageComponent, resolve: { - dso: ItemFromWorkflowResolver, - breadcrumb: I18nBreadcrumbResolver, + dso: itemFromWorkflowResolver, + breadcrumb: i18nBreadcrumbResolver, }, data: { title: 'workflow-item.view.title', breadcrumbKey: 'workflow-item.view' }, }, { - canActivate: [AuthenticatedGuard], + canActivate: [authenticatedGuard], path: WORKFLOW_ITEM_DELETE_PATH, component: ThemedWorkflowItemDeleteComponent, resolve: { - breadcrumb: I18nBreadcrumbResolver, + breadcrumb: i18nBreadcrumbResolver, }, data: { title: 'workflow-item.delete.title', breadcrumbKey: 'workflow-item.edit' }, }, { - canActivate: [AuthenticatedGuard], + canActivate: [authenticatedGuard], path: WORKFLOW_ITEM_SEND_BACK_PATH, component: ThemedWorkflowItemSendBackComponent, resolve: { - breadcrumb: I18nBreadcrumbResolver, + breadcrumb: i18nBreadcrumbResolver, }, data: { title: 'workflow-item.send-back.title', breadcrumbKey: 'workflow-item.edit' }, }, { - canActivate: [AuthenticatedGuard], + canActivate: [authenticatedGuard], path: ADVANCED_WORKFLOW_PATH, component: AdvancedWorkflowActionPageComponent, resolve: { - breadcrumb: I18nBreadcrumbResolver, + breadcrumb: i18nBreadcrumbResolver, }, data: { title: 'workflow-item.advanced.title', breadcrumbKey: 'workflow-item.edit' }, }, diff --git a/src/app/workspaceitems-edit-page/item-from-workspace.resolver.spec.ts b/src/app/workspaceitems-edit-page/item-from-workspace.resolver.spec.ts index 77232762c3..0dc9ad343d 100644 --- a/src/app/workspaceitems-edit-page/item-from-workspace.resolver.spec.ts +++ b/src/app/workspaceitems-edit-page/item-from-workspace.resolver.spec.ts @@ -2,11 +2,11 @@ import { first } from 'rxjs/operators'; import { WorkspaceitemDataService } from '../core/submission/workspaceitem-data.service'; import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; -import { ItemFromWorkspaceResolver } from './item-from-workspace.resolver'; +import { itemFromWorkspaceResolver } from './item-from-workspace.resolver'; -describe('ItemFromWorkspaceResolver', () => { +describe('itemFromWorkspaceResolver', () => { describe('resolve', () => { - let resolver: ItemFromWorkspaceResolver; + let resolver: any; let wfiService: WorkspaceitemDataService; const uuid = '1234-65487-12354-1235'; const itemUuid = '8888-8888-8888-8888'; @@ -20,11 +20,11 @@ describe('ItemFromWorkspaceResolver', () => { wfiService = { findById: (id: string) => createSuccessfulRemoteDataObject$(wfi), } as any; - resolver = new ItemFromWorkspaceResolver(wfiService, null); + resolver = itemFromWorkspaceResolver; }); it('should resolve a an item from from the workflow item with the correct id', (done) => { - resolver.resolve({ params: { id: uuid } } as any, undefined) + resolver({ params: { id: uuid } } as any, undefined, wfiService) .pipe(first()) .subscribe( (resolved) => { diff --git a/src/app/workspaceitems-edit-page/item-from-workspace.resolver.ts b/src/app/workspaceitems-edit-page/item-from-workspace.resolver.ts index 0bd5193d19..6e43fc7bea 100644 --- a/src/app/workspaceitems-edit-page/item-from-workspace.resolver.ts +++ b/src/app/workspaceitems-edit-page/item-from-workspace.resolver.ts @@ -1,20 +1,23 @@ -import { Injectable } from '@angular/core'; -import { Store } from '@ngrx/store'; +import { inject } from '@angular/core'; +import { + ActivatedRouteSnapshot, + ResolveFn, + RouterStateSnapshot, +} from '@angular/router'; +import { Observable } from 'rxjs'; +import { RemoteData } from '../core/data/remote-data'; import { Item } from '../core/shared/item.model'; import { SubmissionObjectResolver } from '../core/submission/resolver/submission-object.resolver'; import { WorkspaceitemDataService } from '../core/submission/workspaceitem-data.service'; /** - * This class represents a resolver that requests a specific item before the route is activated + * This method represents a resolver that requests a specific item before the route is activated */ -@Injectable({ providedIn: 'root' }) -export class ItemFromWorkspaceResolver extends SubmissionObjectResolver { - constructor( - private workspaceItemService: WorkspaceitemDataService, - protected store: Store, - ) { - super(workspaceItemService, store); - } - -} +export const itemFromWorkspaceResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + workspaceItemService: WorkspaceitemDataService = inject(WorkspaceitemDataService), +): Observable> => { + return SubmissionObjectResolver(route, state, workspaceItemService); +}; diff --git a/src/app/workspaceitems-edit-page/workspace-item-page.resolver.spec.ts b/src/app/workspaceitems-edit-page/workspace-item-page.resolver.spec.ts index 0502da186b..4bf44de926 100644 --- a/src/app/workspaceitems-edit-page/workspace-item-page.resolver.spec.ts +++ b/src/app/workspaceitems-edit-page/workspace-item-page.resolver.spec.ts @@ -2,11 +2,11 @@ import { first } from 'rxjs/operators'; import { WorkspaceitemDataService } from '../core/submission/workspaceitem-data.service'; import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; -import { WorkspaceItemPageResolver } from './workspace-item-page.resolver'; +import { workspaceItemPageResolver } from './workspace-item-page.resolver'; -describe('WorkflowItemPageResolver', () => { +describe('workspaceItemPageResolver', () => { describe('resolve', () => { - let resolver: WorkspaceItemPageResolver; + let resolver: any; let wsiService: WorkspaceitemDataService; const uuid = '1234-65487-12354-1235'; @@ -14,11 +14,11 @@ describe('WorkflowItemPageResolver', () => { wsiService = { findById: (id: string) => createSuccessfulRemoteDataObject$({ id }), } as any; - resolver = new WorkspaceItemPageResolver(wsiService); + resolver = workspaceItemPageResolver; }); it('should resolve a workspace item with the correct id', (done) => { - resolver.resolve({ params: { id: uuid } } as any, undefined) + resolver({ params: { id: uuid } } as any, undefined, wsiService) .pipe(first()) .subscribe( (resolved) => { diff --git a/src/app/workspaceitems-edit-page/workspace-item-page.resolver.ts b/src/app/workspaceitems-edit-page/workspace-item-page.resolver.ts index cb0772d475..1d3b8e946d 100644 --- a/src/app/workspaceitems-edit-page/workspace-item-page.resolver.ts +++ b/src/app/workspaceitems-edit-page/workspace-item-page.resolver.ts @@ -1,38 +1,35 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; import { Observable } from 'rxjs'; import { RemoteData } from '../core/data/remote-data'; import { getFirstCompletedRemoteData } from '../core/shared/operators'; -import { WorkflowItem } from '../core/submission/models/workflowitem.model'; +import { WorkspaceItem } from '../core/submission/models/workspaceitem.model'; import { WorkspaceitemDataService } from '../core/submission/workspaceitem-data.service'; import { followLink } from '../shared/utils/follow-link-config.model'; /** - * This class represents a resolver that requests a specific workflow item before the route is activated + * Method for resolving a workflow item based on the parameters in the current route + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @param {WorkspaceitemDataService} workspaceItemService + * @returns Observable<> Emits the found workflow item based on the parameters in the current route, + * or an error if something went wrong */ -@Injectable({ providedIn: 'root' }) -export class WorkspaceItemPageResolver { - constructor(private workspaceItemService: WorkspaceitemDataService) { - } - - /** - * Method for resolving a workflow item based on the parameters in the current route - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - * @param {RouterStateSnapshot} state The current RouterStateSnapshot - * @returns Observable<> Emits the found workflow item based on the parameters in the current route, - * or an error if something went wrong - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - return this.workspaceItemService.findById(route.params.id, - true, - false, - followLink('item'), - ).pipe( - getFirstCompletedRemoteData(), - ); - } -} +export const workspaceItemPageResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + workspaceItemService: WorkspaceitemDataService = inject(WorkspaceitemDataService), +): Observable> => { + return workspaceItemService.findById(route.params.id, + true, + false, + followLink('item'), + ).pipe( + getFirstCompletedRemoteData(), + ); +}; diff --git a/src/app/workspaceitems-edit-page/workspaceitems-edit-page-routes.ts b/src/app/workspaceitems-edit-page/workspaceitems-edit-page-routes.ts index 70abb9d194..f648be0209 100644 --- a/src/app/workspaceitems-edit-page/workspaceitems-edit-page-routes.ts +++ b/src/app/workspaceitems-edit-page/workspaceitems-edit-page-routes.ts @@ -1,11 +1,11 @@ import { Route } from '@angular/router'; -import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; -import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; +import { authenticatedGuard } from '../core/auth/authenticated.guard'; +import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; import { ThemedFullItemPageComponent } from '../item-page/full/themed-full-item-page.component'; import { ThemedSubmissionEditComponent } from '../submission/edit/themed-submission-edit.component'; -import { ItemFromWorkspaceResolver } from './item-from-workspace.resolver'; -import { WorkspaceItemPageResolver } from './workspace-item-page.resolver'; +import { itemFromWorkspaceResolver } from './item-from-workspace.resolver'; +import { workspaceItemPageResolver } from './workspace-item-page.resolver'; import { ThemedWorkspaceItemsDeletePageComponent } from './workspaceitems-delete-page/themed-workspaceitems-delete-page.component'; import { WorkspaceItemsDeletePageComponent } from './workspaceitems-delete-page/workspaceitems-delete-page.component'; @@ -16,44 +16,44 @@ export const ROUTES: Route[] = [ }, { path: ':id', - resolve: { wsi: WorkspaceItemPageResolver }, + resolve: { wsi: workspaceItemPageResolver }, children: [ { - canActivate: [AuthenticatedGuard], + canActivate: [authenticatedGuard], path: 'edit', component: ThemedSubmissionEditComponent, resolve: { - breadcrumb: I18nBreadcrumbResolver, + breadcrumb: i18nBreadcrumbResolver, }, data: { title: 'submission.edit.title', breadcrumbKey: 'submission.edit' }, }, { - canActivate: [AuthenticatedGuard], + canActivate: [authenticatedGuard], path: 'view', component: ThemedFullItemPageComponent, resolve: { - dso: ItemFromWorkspaceResolver, - breadcrumb: I18nBreadcrumbResolver, + dso: itemFromWorkspaceResolver, + breadcrumb: i18nBreadcrumbResolver, }, data: { title: 'workspace-item.view.title', breadcrumbKey: 'workspace-item.view' }, }, { - canActivate: [AuthenticatedGuard], + canActivate: [authenticatedGuard], path: 'delete', component: WorkspaceItemsDeletePageComponent, resolve: { - dso: ItemFromWorkspaceResolver, - breadcrumb: I18nBreadcrumbResolver, + dso: itemFromWorkspaceResolver, + breadcrumb: i18nBreadcrumbResolver, }, data: { title: 'workspace-item.delete', breadcrumbKey: 'workspace-item.delete' }, }, { - canActivate: [AuthenticatedGuard], + canActivate: [authenticatedGuard], path: 'delete', component: ThemedWorkspaceItemsDeletePageComponent, resolve: { - dso: ItemFromWorkspaceResolver, - breadcrumb: I18nBreadcrumbResolver, + dso: itemFromWorkspaceResolver, + breadcrumb: i18nBreadcrumbResolver, }, data: { title: 'workspace-item.delete', breadcrumbKey: 'workspace-item.delete' }, }, diff --git a/src/config/app-config.interface.ts b/src/config/app-config.interface.ts index 703da3734c..2de471f20e 100644 --- a/src/config/app-config.interface.ts +++ b/src/config/app-config.interface.ts @@ -3,7 +3,6 @@ import { makeStateKey, Type, } from '@angular/core'; -import { DynamicFormControl } from '@ng-dynamic-forms/core/lib/component/dynamic-form-control-interface'; import { AdminNotifyMetricsRow } from '../app/admin/admin-notify-dashboard/admin-notify-metrics/admin-notify-metrics.model'; import { HALDataService } from '../app/core/data/base/hal-data-service.interface'; @@ -78,11 +77,8 @@ export interface LazyDataServicesMap { [type: string]: () => Promise>> } -export type DynamicFormControlFn = (model: string) => Type; export const APP_DATA_SERVICES_MAP: InjectionToken = new InjectionToken('APP_DATA_SERVICES_MAP'); -export const APP_DYNAMIC_FORM_CONTROL_FN: InjectionToken = new InjectionToken('APP_DYNAMIC_FORM_CONTROL_FN'); - export { APP_CONFIG, APP_CONFIG_STATE, diff --git a/src/config/default-app-config.ts b/src/config/default-app-config.ts index 85042034f6..d148dee148 100644 --- a/src/config/default-app-config.ts +++ b/src/config/default-app-config.ts @@ -476,8 +476,8 @@ export class DefaultAppConfig implements AppConfig { // Whether to enable Markdown (https://commonmark.org/) and MathJax (https://www.mathjax.org/) // display in supported metadata fields. By default, only dc.description.abstract is supported. markdown: MarkdownConfig = { - enabled: true, - mathjax: true, + enabled: false, + mathjax: false, }; // Which vocabularies should be used for which search filters diff --git a/src/modules/app/browser-app.config.ts b/src/modules/app/browser-app.config.ts index aa37804cb1..6121cfc219 100644 --- a/src/modules/app/browser-app.config.ts +++ b/src/modules/app/browser-app.config.ts @@ -5,6 +5,7 @@ import { } from '@angular/common/http'; import { APP_ID, + APP_INITIALIZER, ApplicationConfig, importProvidersFrom, makeStateKey, @@ -50,6 +51,8 @@ import { HardRedirectService } from '../../app/core/services/hard-redirect.servi import { ReferrerService } from '../../app/core/services/referrer.service'; import { ClientMathService } from '../../app/core/shared/client-math.service'; import { MathService } from '../../app/core/shared/math.service'; +import { BrowserXSRFService } from '../../app/core/xsrf/browser-xsrf.service'; +import { XSRFService } from '../../app/core/xsrf/xsrf.service'; import { BrowserKlaroService } from '../../app/shared/cookies/browser-klaro.service'; import { KlaroService } from '../../app/shared/cookies/klaro.service'; import { MissingTranslationHelper } from '../../app/shared/translate/missing-translation.helper'; @@ -96,6 +99,16 @@ export const browserAppConfig: ApplicationConfig = mergeApplicationConfig({ useFactory: getRequest, deps: [TransferState], }, + { + provide: APP_INITIALIZER, + useFactory: (xsrfService: XSRFService, httpClient: HttpClient) => xsrfService.initXSRFToken(httpClient), + deps: [ XSRFService, HttpClient ], + multi: true, + }, + { + provide: XSRFService, + useClass: BrowserXSRFService, + }, { provide: AuthService, useClass: AuthService, diff --git a/src/modules/app/browser-init.service.ts b/src/modules/app/browser-init.service.ts index 4d577907b0..799910efc6 100644 --- a/src/modules/app/browser-init.service.ts +++ b/src/modules/app/browser-init.service.ts @@ -10,10 +10,15 @@ import { Injectable, TransferState, } from '@angular/core'; +import { + NavigationStart, + Router, +} from '@angular/router'; import { Store } from '@ngrx/store'; import { TranslateService } from '@ngx-translate/core'; import { firstValueFrom, + lastValueFrom, Subscription, } from 'rxjs'; import { @@ -30,7 +35,6 @@ import { coreSelector } from '../../app/core/core.selectors'; import { RootDataService } from '../../app/core/data/root-data.service'; import { LocaleService } from '../../app/core/locale/locale.service'; import { MetadataService } from '../../app/core/metadata/metadata.service'; -import { ServerCheckGuard } from '../../app/core/server-check/server-check.guard'; import { CorrelationIdService } from '../../app/correlation-id/correlation-id.service'; import { InitService } from '../../app/init.service'; import { KlaroService } from '../../app/shared/cookies/klaro.service'; @@ -76,7 +80,7 @@ export class BrowserInitService extends InitService { protected themeService: ThemeService, protected menuService: MenuService, private rootDataService: RootDataService, - protected serverCheckGuard: ServerCheckGuard, + protected router: Router, ) { super( store, @@ -121,7 +125,7 @@ export class BrowserInitService extends InitService { this.initKlaro(); - await this.authenticationReady$().toPromise(); + await lastValueFrom(this.authenticationReady$()); return true; }; @@ -138,10 +142,12 @@ export class BrowserInitService extends InitService { const state = this.transferState.get(InitService.NGRX_STATE, null); this.transferState.remove(InitService.NGRX_STATE); this.store.dispatch(new StoreAction(StoreActionTypes.REHYDRATE, state)); - return this.store.select(coreSelector).pipe( - find((core: any) => isNotEmpty(core)), - map(() => true), - ).toPromise(); + return lastValueFrom( + this.store.select(coreSelector).pipe( + find((core: any) => isNotEmpty(core)), + map(() => true), + ), + ); } private trackAuthTokenExpiration(): void { @@ -187,7 +193,7 @@ export class BrowserInitService extends InitService { * @private */ private closeAuthCheckSubscription() { - firstValueFrom(this.authenticationReady$()).then(() => { + void firstValueFrom(this.authenticationReady$()).then(() => { this.sub.unsubscribe(); }); } @@ -198,7 +204,25 @@ export class BrowserInitService extends InitService { */ protected initRouteListeners(): void { super.initRouteListeners(); - this.serverCheckGuard.listenForRouteChanges(); + this.listenForRouteChanges(); + } + + /** + * Listen to all router events. Every time a new navigation starts, invalidate the cache + * for the root endpoint. That way we retrieve it once per routing operation to ensure the + * backend is not down. But if the guard is called multiple times during the same routing + * operation, the cached version is used. + */ + protected listenForRouteChanges(): void { + // we'll always be too late for the first NavigationStart event with the router subscribe below, + // so this statement is for the very first route operation. + this.rootDataService.invalidateRootCache(); + + this.router.events.pipe( + filter(event => event instanceof NavigationStart), + ).subscribe(() => { + this.rootDataService.invalidateRootCache(); + }); } } diff --git a/src/modules/app/server-app.config.ts b/src/modules/app/server-app.config.ts index 053bfded6f..4cf9369ddd 100644 --- a/src/modules/app/server-app.config.ts +++ b/src/modules/app/server-app.config.ts @@ -50,6 +50,8 @@ import { ServerHardRedirectService } from '../../app/core/services/server-hard-r import { ServerXhrService } from '../../app/core/services/server-xhr.service'; import { MathService } from '../../app/core/shared/math.service'; import { ServerMathService } from '../../app/core/shared/server-math.service'; +import { ServerXSRFService } from '../../app/core/xsrf/server-xsrf.service'; +import { XSRFService } from '../../app/core/xsrf/xsrf.service'; import { AngularticsProviderMock } from '../../app/shared/mocks/angulartics-provider.service.mock'; import { Angulartics2Mock } from '../../app/shared/mocks/angulartics2.service.mock'; import { Angulartics2DSpace } from '../../app/statistics/angulartics/dspace-provider'; @@ -112,6 +114,10 @@ export const serverAppConfig: ApplicationConfig = mergeApplicationConfig({ provide: AuthRequestService, useClass: ServerAuthRequestService, }, + { + provide: XSRFService, + useClass: ServerXSRFService, + }, { provide: LocaleService, useClass: ServerLocaleService, diff --git a/src/modules/app/server-init.service.ts b/src/modules/app/server-init.service.ts index 8849e8f00e..6db4f4a48b 100644 --- a/src/modules/app/server-init.service.ts +++ b/src/modules/app/server-init.service.ts @@ -8,10 +8,11 @@ import { Inject, Injectable, + TransferState, } from '@angular/core'; -import { TransferState } from '@angular/platform-browser'; import { Store } from '@ngrx/store'; import { TranslateService } from '@ngx-translate/core'; +import { lastValueFrom } from 'rxjs'; import { take } from 'rxjs/operators'; import { AppState } from '../../app/app.reducer'; @@ -75,7 +76,7 @@ export class ServerInitService extends InitService { this.initRouteListeners(); this.themeService.listenForThemeChanges(false); - await this.authenticationReady$().toPromise(); + await lastValueFrom(this.authenticationReady$()); return true; }; diff --git a/src/themes/dspace/app/home-page/home-news/home-news.component.html b/src/themes/dspace/app/home-page/home-news/home-news.component.html index 3c6b8c9130..ca686a9933 100644 --- a/src/themes/dspace/app/home-page/home-news/home-news.component.html +++ b/src/themes/dspace/app/home-page/home-news/home-news.component.html @@ -3,7 +3,7 @@
-

DSpace 7

+

DSpace 8

DSpace is the world leading open source repository platform that enables organisations to: