diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 52f20470a3..f6ffa5e004 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -33,12 +33,12 @@ 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) matrix: - node-version: [16.x, 18.x] + node-version: [18.x, 20.x] # Do NOT exit immediately if one matrix job fails fail-fast: false # These are the actual CI steps to perform per job @@ -74,7 +74,7 @@ jobs: id: yarn-cache-dir-path run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT - name: Cache Yarn dependencies - uses: actions/cache@v3 + uses: actions/cache@v4 with: # Cache entire Yarn cache directory (see previous step) path: ${{ steps.yarn-cache-dir-path.outputs.dir }} @@ -101,19 +101,19 @@ jobs: # so that it can be shared with the 'codecov' job (see below) # NOTE: Angular CLI only supports code coverage for specs. See https://github.com/angular/angular-cli/issues/6286 - name: Upload code coverage report to Artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: matrix.node-version == '18.x' with: - name: dspace-angular coverage report + name: coverage-report-${{ matrix.node-version }} 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 @@ -135,19 +135,19 @@ jobs: # Cypress always creates a video of all e2e tests (whether they succeeded or failed) # Save those in an Artifact - name: Upload e2e test videos to Artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: always() with: - name: e2e-test-videos + name: e2e-test-videos-${{ matrix.node-version }} path: cypress/videos # If e2e tests fail, Cypress creates a screenshot of what happened # Save those in an Artifact - name: Upload e2e test failure screenshots to Artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: failure() with: - name: e2e-test-screenshots + name: e2e-test-screenshots-${{ matrix.node-version }} path: cypress/screenshots - name: Stop app (in case it stays up after e2e tests) @@ -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. @@ -197,7 +197,7 @@ jobs: # Download artifacts from previous 'tests' job - name: Download coverage artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 # Now attempt upload to Codecov using its action. # NOTE: We use a retry action to retry the Codecov upload if it fails the first time. @@ -207,11 +207,12 @@ jobs: - name: Upload coverage to Codecov.io uses: Wandalen/wretry.action@v1.3.0 with: - action: codecov/codecov-action@v3 + action: codecov/codecov-action@v4 # Ensure codecov-action throws an error when it fails to upload # This allows us to auto-restart the action if an error is thrown with: | fail_ci_if_error: true + token: ${{ secrets.CODECOV_TOKEN }} # Try re-running action 5 times max attempt_limit: 5 # Run again in 30 seconds diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 85a7216113..d0b4cd0939 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -28,7 +28,7 @@ jobs: # Use the reusable-docker-build.yml script from DSpace/DSpace repo to build our Docker image uses: DSpace/DSpace/.github/workflows/reusable-docker-build.yml@main with: - build_id: dspace-angular + build_id: dspace-angular-dev image_name: dspace/dspace-angular dockerfile_path: ./Dockerfile secrets: diff --git a/.github/workflows/issue_opened.yml b/.github/workflows/issue_opened.yml index b4436dca3a..0a35a6a950 100644 --- a/.github/workflows/issue_opened.yml +++ b/.github/workflows/issue_opened.yml @@ -16,7 +16,7 @@ jobs: # Only add to project board if issue is flagged as "needs triage" or has no labels # NOTE: By default we flag new issues as "needs triage" in our issue template if: (contains(github.event.issue.labels.*.name, 'needs triage') || join(github.event.issue.labels.*.name) == '') - uses: actions/add-to-project@v0.5.0 + uses: actions/add-to-project@v1.0.0 # Note, the authentication token below is an ORG level Secret. # It must be created/recreated manually via a personal access token with admin:org, project, public_repo permissions # See: https://docs.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token#permissions-for-the-github_token diff --git a/.github/workflows/pull_request_opened.yml b/.github/workflows/pull_request_opened.yml index f16e81c9fd..bbac52af24 100644 --- a/.github/workflows/pull_request_opened.yml +++ b/.github/workflows/pull_request_opened.yml @@ -21,4 +21,4 @@ jobs: # Assign the PR to whomever created it. This is useful for visualizing assignments on project boards # See https://github.com/toshimaru/auto-author-assign - name: Assign PR to creator - uses: toshimaru/auto-author-assign@v2.0.1 + uses: toshimaru/auto-author-assign@v2.1.0 diff --git a/config/config.example.yml b/config/config.example.yml index 36d6a009d3..82c061dab2 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -17,6 +17,13 @@ ui: # Trust X-FORWARDED-* headers from proxies (default = true) useProxies: true +universal: + # Whether to inline "critical" styles into the server-side rendered HTML. + # Determining which styles are critical is a relatively expensive operation; + # this option can be disabled to boost server performance at the expense of + # loading smoothness. + inlineCriticalCss: true + # The REST API server settings # NOTE: these settings define which (publicly available) REST API to use. They are usually # 'synced' with the 'dspace.server.url' setting in your backend's local.cfg. @@ -400,10 +407,11 @@ mediaViewer: # Whether the end user agreement is required before users use the repository. # If enabled, the user will be required to accept the agreement before they can use the repository. -# And whether the privacy statement should exist or not. +# And whether the privacy statement/COAR notify support page should exist or not. info: enableEndUserAgreement: true enablePrivacyStatement: true + enableCOARNotifySupport: true # 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. diff --git a/package.json b/package.json index c0a3843605..77227ff2d2 100644 --- a/package.json +++ b/package.json @@ -55,28 +55,28 @@ "ts-node": "10.2.1" }, "dependencies": { - "@angular/animations": "^15.2.8", - "@angular/cdk": "^15.2.8", - "@angular/common": "^15.2.8", - "@angular/compiler": "^15.2.8", - "@angular/core": "^15.2.8", - "@angular/forms": "^15.2.8", - "@angular/localize": "15.2.8", - "@angular/platform-browser": "^15.2.8", - "@angular/platform-browser-dynamic": "^15.2.8", - "@angular/platform-server": "^15.2.8", - "@angular/router": "^15.2.8", + "@angular/animations": "^16.2.12", + "@angular/cdk": "^16.2.12", + "@angular/common": "^16.2.12", + "@angular/compiler": "^16.2.12", + "@angular/core": "^16.2.12", + "@angular/forms": "^16.2.12", + "@angular/localize": "16.2.12", + "@angular/platform-browser": "^16.2.12", + "@angular/platform-browser-dynamic": "^16.2.12", + "@angular/platform-server": "^16.2.12", + "@angular/router": "^16.2.12", "@babel/runtime": "7.21.0", "@kolkov/ngx-gallery": "^2.0.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.11.3", "@ng-bootstrap/ng-bootstrap": "^11.0.0", - "@ng-dynamic-forms/core": "^15.0.0", - "@ng-dynamic-forms/ui-ng-bootstrap": "^15.0.0", - "@ngrx/effects": "^15.4.0", - "@ngrx/router-store": "^15.4.0", - "@ngrx/store": "^15.4.0", - "@nguniversal/express-engine": "^15.2.1", + "@ng-dynamic-forms/core": "^16.0.0", + "@ng-dynamic-forms/ui-ng-bootstrap": "^16.0.0", + "@ngrx/effects": "^16.3.0", + "@ngrx/router-store": "^16.3.0", + "@ngrx/store": "^16.3.0", + "@nguniversal/express-engine": "^16.2.0", "@ngx-translate/core": "^14.0.0", "@nicky-lenaers/ngx-scroll-to": "^14.0.0", "@types/grecaptcha": "^3.0.4", @@ -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", @@ -110,17 +110,15 @@ "lodash": "^4.17.21", "lru-cache": "^7.14.1", "markdown-it": "^13.0.1", - "markdown-it-mathjax3": "^4.3.2", "mirador": "^3.3.0", "mirador-dl-plugin": "^0.13.0", "mirador-share-plugin": "^0.11.0", "morgan": "^1.10.0", "ng-mocks": "^14.10.0", - "ng2-file-upload": "1.4.0", + "ng2-file-upload": "5.0.0", "ng2-nouislider": "^2.0.0", - "ngx-infinite-scroll": "^15.0.0", + "ngx-infinite-scroll": "^16.0.0", "ngx-pagination": "6.0.3", - "ngx-sortablejs": "^11.1.0", "ngx-ui-switch": "^14.1.0", "nouislider": "^15.7.1", "pem": "1.14.7", @@ -132,24 +130,24 @@ "sortablejs": "1.15.0", "uuid": "^8.3.2", "webfontloader": "1.6.28", - "zone.js": "~0.11.5" + "zone.js": "~0.13.3" }, "devDependencies": { - "@angular-builders/custom-webpack": "~15.0.0", - "@angular-devkit/build-angular": "^15.2.6", - "@angular-eslint/builder": "15.2.1", - "@angular-eslint/eslint-plugin": "15.2.1", - "@angular-eslint/eslint-plugin-template": "15.2.1", - "@angular-eslint/schematics": "15.2.1", - "@angular-eslint/template-parser": "15.2.1", - "@angular/cli": "^16.0.4", - "@angular/compiler-cli": "^15.2.8", - "@angular/language-service": "^15.2.8", + "@angular-builders/custom-webpack": "~16.0.0", + "@angular-devkit/build-angular": "^16.2.12", + "@angular-eslint/builder": "16.3.1", + "@angular-eslint/eslint-plugin": "16.3.1", + "@angular-eslint/eslint-plugin-template": "16.3.1", + "@angular-eslint/schematics": "16.3.1", + "@angular-eslint/template-parser": "16.3.1", + "@angular/cli": "^16.2.12", + "@angular/compiler-cli": "^16.2.12", + "@angular/language-service": "^16.2.12", "@cypress/schematic": "^1.5.0", "@fortawesome/fontawesome-free": "^6.4.0", - "@ngrx/store-devtools": "^15.4.0", - "@ngtools/webpack": "^15.2.6", - "@nguniversal/builders": "^15.2.1", + "@ngrx/store-devtools": "^16.3.0", + "@ngtools/webpack": "^16.2.12", + "@nguniversal/builders": "^16.2.0", "@types/deep-freeze": "0.1.2", "@types/ejs": "^3.1.2", "@types/express": "^4.17.17", @@ -186,7 +184,7 @@ "karma-jasmine": "~4.0.0", "karma-jasmine-html-reporter": "^1.5.0", "karma-mocha-reporter": "2.2.5", - "ngx-mask": "^13.1.7", + "ngx-mask": "14.2.4", "nodemon": "^2.0.22", "postcss": "^8.4", "postcss-apply": "0.12.0", @@ -202,7 +200,7 @@ "sass-loader": "^12.6.0", "sass-resources-loader": "^2.2.5", "ts-node": "^8.10.2", - "typescript": "~4.8.4", + "typescript": "~4.9.3", "webpack": "5.76.1", "webpack-bundle-analyzer": "^4.8.0", "webpack-cli": "^4.2.0", diff --git a/server.ts b/server.ts index da085f372f..d00529687d 100644 --- a/server.ts +++ b/server.ts @@ -48,7 +48,7 @@ import { hasNoValue, hasValue } from './src/app/shared/empty.util'; import { UIServerConfig } from './src/config/ui-server-config.interface'; -import { ServerAppModule } from './src/main.server'; +import bootstrap from './src/main.server'; import { buildAppConfig } from './src/config/config.server'; import { APP_CONFIG, AppConfig } from './src/config/app-config.interface'; @@ -130,7 +130,8 @@ export function app() { // Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine) server.engine('html', (_, options, callback) => ngExpressEngine({ - bootstrap: ServerAppModule, + bootstrap, + inlineCriticalCss: environment.universal.inlineCriticalCss, providers: [ { provide: REQUEST, @@ -142,10 +143,10 @@ export function app() { }, { provide: APP_CONFIG, - useValue: environment - } - ] - })(_, (options as any), callback) + useValue: environment, + }, + ], + })(_, (options as any), callback), ); server.engine('ejs', ejs.renderFile); @@ -162,7 +163,7 @@ export function app() { server.get('/robots.txt', (req, res) => { res.setHeader('content-type', 'text/plain'); res.render('assets/robots.txt.ejs', { - 'origin': req.protocol + '://' + req.headers.host + 'origin': req.protocol + '://' + req.headers.host, }); }); @@ -177,7 +178,7 @@ export function app() { router.use('/sitemap**', createProxyMiddleware({ target: `${environment.rest.baseUrl}/sitemaps`, pathRewrite: path => path.replace(environment.ui.nameSpace, '/'), - changeOrigin: true + changeOrigin: true, })); /** @@ -186,7 +187,7 @@ export function app() { router.use('/signposting**', createProxyMiddleware({ target: `${environment.rest.baseUrl}`, pathRewrite: path => path.replace(environment.ui.nameSpace, '/'), - changeOrigin: true + changeOrigin: true, })); /** @@ -197,7 +198,7 @@ export function app() { const RateLimit = require('express-rate-limit'); const limiter = new RateLimit({ windowMs: (environment.ui as UIServerConfig).rateLimiter.windowMs, - max: (environment.ui as UIServerConfig).rateLimiter.max + max: (environment.ui as UIServerConfig).rateLimiter.max, }); server.use(limiter); } @@ -325,7 +326,7 @@ function initCache() { botCache = new LRU( { max: environment.cache.serverSide.botCache.max, ttl: environment.cache.serverSide.botCache.timeToLive, - allowStale: environment.cache.serverSide.botCache.allowStale + allowStale: environment.cache.serverSide.botCache.allowStale, }); } @@ -337,7 +338,7 @@ function initCache() { anonymousCache = new LRU( { max: environment.cache.serverSide.anonymousCache.max, ttl: environment.cache.serverSide.anonymousCache.timeToLive, - allowStale: environment.cache.serverSide.anonymousCache.allowStale + allowStale: environment.cache.serverSide.anonymousCache.allowStale, }); } } @@ -415,7 +416,7 @@ function checkCacheForRequest(cacheName: string, cache: LRU, req, r const key = getCacheKey(req); // Check if this page is in our cache - let cachedCopy = cache.get(key); + const cachedCopy = cache.get(key); if (cachedCopy) { if (environment.cache.serverSide.debug) { console.log(`CACHE HIT FOR ${key} in ${cacheName} cache`); } @@ -529,20 +530,20 @@ function serverStarted() { function createHttpsServer(keys) { const listener = createServer({ key: keys.serviceKey, - cert: keys.certificate + cert: keys.certificate, }, app).listen(environment.ui.port, environment.ui.host, () => { serverStarted(); }); // Graceful shutdown when signalled - const terminator = createHttpTerminator({server: listener}); + const terminator = createHttpTerminator({ server: listener }); process.on('SIGINT', () => { - void (async ()=> { - console.debug('Closing HTTPS server on signal'); - await terminator.terminate().catch(e => { console.error(e); }); - console.debug('HTTPS server closed'); - })(); - }); + void (async ()=> { + console.debug('Closing HTTPS server on signal'); + await terminator.terminate().catch(e => { console.error(e); }); + console.debug('HTTPS server closed'); + })(); + }); } /** @@ -559,14 +560,14 @@ function run() { }); // Graceful shutdown when signalled - const terminator = createHttpTerminator({server: listener}); + const terminator = createHttpTerminator({ server: listener }); process.on('SIGINT', () => { - void (async () => { - console.debug('Closing HTTP server on signal'); - await terminator.terminate().catch(e => { console.error(e); }); - console.debug('HTTP server closed.');return undefined; - })(); - }); + void (async () => { + console.debug('Closing HTTP server on signal'); + await terminator.terminate().catch(e => { console.error(e); }); + console.debug('HTTP server closed.');return undefined; + })(); + }); } function start() { @@ -597,7 +598,7 @@ function start() { if (serviceKey && certificate) { createHttpsServer({ serviceKey: serviceKey, - certificate: certificate + certificate: certificate, }); } else { console.warn('Disabling certificate validation and proceeding with a self-signed certificate. If this is a production server, it is recommended that you configure a valid certificate instead.'); @@ -606,7 +607,7 @@ function start() { createCertificate({ days: 1, - selfSigned: true + selfSigned: true, }, (error, keys) => { createHttpsServer(keys); }); @@ -627,7 +628,7 @@ function healthCheck(req, res) { }) .catch((error) => { res.status(error.response.status).send({ - error: error.message + error: error.message, }); }); } 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/epeople-registry/eperson-resolver.service.ts b/src/app/access-control/epeople-registry/eperson-resolver.service.ts index 33233ba7e8..6c9d7347f7 100644 --- a/src/app/access-control/epeople-registry/eperson-resolver.service.ts +++ b/src/app/access-control/epeople-registry/eperson-resolver.service.ts @@ -1,7 +1,6 @@ import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, - Resolve, RouterStateSnapshot, } from '@angular/router'; import { Store } from '@ngrx/store'; @@ -27,7 +26,7 @@ export const EPERSON_EDIT_FOLLOW_LINKS: FollowLinkConfig[] = [ @Injectable({ providedIn: 'root', }) -export class EPersonResolver implements Resolve> { +export class EPersonResolver { constructor( protected ePersonService: EPersonDataService, 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-publication-claim-page/admin-notifications-publication-claim-page-resolver.service.ts b/src/app/admin/admin-notifications/admin-notifications-publication-claim-page/admin-notifications-publication-claim-page-resolver.service.ts index e35f4b817c..15d384a104 100644 --- a/src/app/admin/admin-notifications/admin-notifications-publication-claim-page/admin-notifications-publication-claim-page-resolver.service.ts +++ b/src/app/admin/admin-notifications/admin-notifications-publication-claim-page/admin-notifications-publication-claim-page-resolver.service.ts @@ -1,7 +1,6 @@ import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, - Resolve, RouterStateSnapshot, } from '@angular/router'; @@ -18,7 +17,7 @@ export interface NotificationsSuggestionTargetsPageParams { * This class represents a resolver that retrieve the route data before the route is activated. */ @Injectable({ providedIn: 'root' }) -export class NotificationsSuggestionTargetsPageResolver implements Resolve { +export class NotificationsSuggestionTargetsPageResolver { /** * Method for resolving the parameters in the current route. 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 e79b114253..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,7 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, - Resolve, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; import { Observable } from 'rxjs'; @@ -12,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 implements Resolve> { - 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-search-page/admin-search-page.component.html b/src/app/admin/admin-search-page/admin-search-page.component.html index 69ff132fe3..516799ddf9 100644 --- a/src/app/admin/admin-search-page/admin-search-page.component.html +++ b/src/app/admin/admin-search-page/admin-search-page.component.html @@ -1 +1 @@ - + diff --git a/src/app/admin/admin-search-page/admin-search-page.component.spec.ts b/src/app/admin/admin-search-page/admin-search-page.component.spec.ts index dd502ed112..d3a39f12f4 100644 --- a/src/app/admin/admin-search-page/admin-search-page.component.spec.ts +++ b/src/app/admin/admin-search-page/admin-search-page.component.spec.ts @@ -6,7 +6,7 @@ import { } from '@angular/core/testing'; import { ActivatedRoute } from '@angular/router'; -import { ConfigurationSearchPageComponent } from '../../search-page/configuration-search-page.component'; +import { ThemedConfigurationSearchPageComponent } from '../../search-page/themed-configuration-search-page.component'; import { ActivatedRouteStub } from '../../shared/testing/active-router.stub'; import { AdminSearchPageComponent } from './admin-search-page.component'; @@ -23,7 +23,7 @@ describe('AdminSearchPageComponent', () => { schemas: [NO_ERRORS_SCHEMA], }).overrideComponent(AdminSearchPageComponent, { remove: { - imports: [ConfigurationSearchPageComponent], + imports: [ThemedConfigurationSearchPageComponent], }, }) .compileComponents(); diff --git a/src/app/admin/admin-search-page/admin-search-page.component.ts b/src/app/admin/admin-search-page/admin-search-page.component.ts index d6215be971..99909b8257 100644 --- a/src/app/admin/admin-search-page/admin-search-page.component.ts +++ b/src/app/admin/admin-search-page/admin-search-page.component.ts @@ -1,14 +1,14 @@ import { Component } from '@angular/core'; import { Context } from '../../core/shared/context.model'; -import { ConfigurationSearchPageComponent } from '../../search-page/configuration-search-page.component'; +import { ThemedConfigurationSearchPageComponent } from '../../search-page/themed-configuration-search-page.component'; @Component({ selector: 'ds-admin-search-page', templateUrl: './admin-search-page.component.html', styleUrls: ['./admin-search-page.component.scss'], standalone: true, - imports: [ConfigurationSearchPageComponent], + imports: [ThemedConfigurationSearchPageComponent], }) /** diff --git a/src/app/admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.spec.ts b/src/app/admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.spec.ts index f69c8cf503..fdee7c70e4 100644 --- a/src/app/admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.spec.ts +++ b/src/app/admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.spec.ts @@ -31,12 +31,7 @@ describe('AdminSidebarSectionComponent', () => { { provide: MenuService, useValue: menuService }, { provide: CSSVariableService, useClass: CSSVariableServiceStub }, ], - }).overrideComponent(AdminSidebarSectionComponent, { - set: { - entryComponents: [TestComponent], - }, - }) - .compileComponents(); + }).compileComponents(); })); beforeEach(() => { @@ -70,12 +65,7 @@ describe('AdminSidebarSectionComponent', () => { { provide: MenuService, useValue: menuService }, { provide: CSSVariableService, useClass: CSSVariableServiceStub }, ], - }).overrideComponent(AdminSidebarSectionComponent, { - set: { - entryComponents: [TestComponent], - }, - }) - .compileComponents(); + }).compileComponents(); })); beforeEach(() => { diff --git a/src/app/admin/admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component.spec.ts b/src/app/admin/admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component.spec.ts index 85393ae201..1da92611c0 100644 --- a/src/app/admin/admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component.spec.ts +++ b/src/app/admin/admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component.spec.ts @@ -31,12 +31,7 @@ describe('ExpandableAdminSidebarSectionComponent', () => { { provide: CSSVariableService, useClass: CSSVariableServiceStub }, { provide: Router, useValue: new RouterStub() }, ], - }).overrideComponent(ExpandableAdminSidebarSectionComponent, { - set: { - entryComponents: [TestComponent], - }, - }) - .compileComponents(); + }).compileComponents(); })); beforeEach(() => { diff --git a/src/app/admin/admin-workflow-page/admin-workflow-page.component.html b/src/app/admin/admin-workflow-page/admin-workflow-page.component.html index d12cefb331..c16ed31168 100644 --- a/src/app/admin/admin-workflow-page/admin-workflow-page.component.html +++ b/src/app/admin/admin-workflow-page/admin-workflow-page.component.html @@ -1 +1 @@ - + diff --git a/src/app/admin/admin-workflow-page/admin-workflow-page.component.spec.ts b/src/app/admin/admin-workflow-page/admin-workflow-page.component.spec.ts index ff326fbc27..252a38e771 100644 --- a/src/app/admin/admin-workflow-page/admin-workflow-page.component.spec.ts +++ b/src/app/admin/admin-workflow-page/admin-workflow-page.component.spec.ts @@ -5,7 +5,7 @@ import { waitForAsync, } from '@angular/core/testing'; -import { ConfigurationSearchPageComponent } from '../../search-page/configuration-search-page.component'; +import { ThemedConfigurationSearchPageComponent } from '../../search-page/themed-configuration-search-page.component'; import { AdminWorkflowPageComponent } from './admin-workflow-page.component'; describe('AdminSearchPageComponent', () => { @@ -20,7 +20,7 @@ describe('AdminSearchPageComponent', () => { .overrideComponent(AdminWorkflowPageComponent, { remove: { imports: [ - ConfigurationSearchPageComponent, + ThemedConfigurationSearchPageComponent, ], }, }) diff --git a/src/app/admin/admin-workflow-page/admin-workflow-page.component.ts b/src/app/admin/admin-workflow-page/admin-workflow-page.component.ts index fdc34fe4ab..62a66039af 100644 --- a/src/app/admin/admin-workflow-page/admin-workflow-page.component.ts +++ b/src/app/admin/admin-workflow-page/admin-workflow-page.component.ts @@ -1,14 +1,14 @@ import { Component } from '@angular/core'; import { Context } from '../../core/shared/context.model'; -import { ConfigurationSearchPageComponent } from '../../search-page/configuration-search-page.component'; +import { ThemedConfigurationSearchPageComponent } from '../../search-page/themed-configuration-search-page.component'; @Component({ selector: 'ds-admin-workflow-page', templateUrl: './admin-workflow-page.component.html', styleUrls: ['./admin-workflow-page.component.scss'], standalone: true, - imports: [ConfigurationSearchPageComponent], + imports: [ThemedConfigurationSearchPageComponent], }) /** 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..7d202f16e9 100644 --- a/src/app/app-routing-paths.ts +++ b/src/app/app-routing-paths.ts @@ -34,9 +34,8 @@ 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/app.component.spec.ts b/src/app/app.component.spec.ts index 70694a1fbb..d0bbbbaae8 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -35,6 +35,7 @@ import { NativeWindowRef, NativeWindowService, } from './core/services/window.service'; +import { ThemedRootComponent } from './root/themed-root.component'; import { HostWindowResizeAction } from './shared/host-window.actions'; import { HostWindowService } from './shared/host-window.service'; import { MenuService } from './shared/menu/menu.service'; @@ -84,7 +85,6 @@ describe('App component', () => { }, }), ], - declarations: [AppComponent], // declare the test component providers: [ { provide: NativeWindowService, useValue: new NativeWindowRef() }, { provide: MetadataService, useValue: new MetadataServiceMock() }, @@ -109,7 +109,13 @@ describe('App component', () => { // waitForAsync beforeEach beforeEach(waitForAsync(() => { - return TestBed.configureTestingModule(getDefaultTestBedConf()); + return TestBed.configureTestingModule(getDefaultTestBedConf()).overrideComponent( + AppComponent, { + remove: { + imports: [ ThemedRootComponent ], + }, + }, + ); })); // synchronous beforeEach diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 79a4fc7589..cdf45f50a8 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,4 +1,5 @@ import { + AsyncPipe, DOCUMENT, isPlatformBrowser, } from '@angular/common'; @@ -44,6 +45,7 @@ import { NativeWindowService, } from './core/services/window.service'; import { distinctNext } from './core/shared/distinct-next'; +import { ThemedRootComponent } from './root/themed-root.component'; import { HostWindowResizeAction } from './shared/host-window.actions'; import { IdleModalComponent } from './shared/idle-modal/idle-modal.component'; import { CSSVariableService } from './shared/sass-helper/css-variable.service'; @@ -55,6 +57,11 @@ import { ThemeService } from './shared/theme-support/theme.service'; templateUrl: './app.component.html', styleUrls: ['./app.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [ + ThemedRootComponent, + AsyncPipe, + ], }) export class AppComponent implements OnInit, AfterViewInit { notificationOptions; diff --git a/src/app/app.module.ts b/src/app/app.config.ts similarity index 53% rename from src/app/app.module.ts rename to src/app/app.config.ts index 7876a83c39..61a04c8adb 100644 --- a/src/app/app.module.ts +++ b/src/app/app.config.ts @@ -1,14 +1,12 @@ import { APP_BASE_HREF, - CommonModule, DOCUMENT, } from '@angular/common'; +import { HTTP_INTERCEPTORS } from '@angular/common/http'; import { - HTTP_INTERCEPTORS, - HttpClientModule, -} from '@angular/common/http'; -import { NgModule } from '@angular/core'; -import { BrowserModule } from '@angular/platform-browser'; + ApplicationConfig, + importProvidersFrom, +} from '@angular/core'; import { NoPreloading, provideRouter, @@ -29,7 +27,6 @@ import { StoreModule, USER_PROVIDED_META_REDUCERS, } from '@ngrx/store'; -import { TranslateModule } from '@ngx-translate/core'; import { ScrollToModule } from '@nicky-lenaers/ngx-scroll-to'; import { NgxMaskModule } from 'ngx-mask'; @@ -40,7 +37,6 @@ import { import { StoreDevModules } from '../config/store/devtools'; import { environment } from '../environments/environment'; import { EagerThemesModule } from '../themes/eager-themes.module'; -import { AppComponent } from './app.component'; import { appEffects } from './app.effects'; import { appMetaReducers, @@ -68,7 +64,6 @@ import { ClientCookieService } from './core/services/client-cookie.service'; import { ListableModule } from './core/shared/listable.module'; import { XsrfInterceptor } from './core/xsrf/xsrf.interceptor'; import { RootModule } from './root.module'; -import { ThemedRootComponent } from './root/themed-root.component'; import { AUTH_METHOD_FOR_DECORATOR_MAP } from './shared/log-in/methods/log-in.methods-decorator'; import { METADATA_REPRESENTATION_COMPONENT_DECORATOR_MAP } from './shared/metadata-representation/metadata-representation.decorator'; import { @@ -92,97 +87,79 @@ export function getMetaReducers(appConfig: AppConfig): MetaReducer[] { return appConfig.debug ? [...appMetaReducers, ...debugMetaReducers] : appMetaReducers; } -const IMPORTS = [ - CommonModule, - HttpClientModule, - ScrollToModule.forRoot(), - NgbModule, - TranslateModule.forRoot(), - EffectsModule.forRoot(appEffects), - StoreModule.forRoot(appReducers, storeModuleConfig), - StoreRouterConnectingModule.forRoot(), - StoreDevModules, - EagerThemesModule, - RootModule, - ListableModule.withEntryComponents(), -]; - -const PROVIDERS = [ - provideRouter( - APP_ROUTES, - withRouterConfig(APP_ROUTING_CONF), - withInMemoryScrolling(APP_ROUTING_SCROLL_CONF), - withEnabledBlockingInitialNavigation(), - withPreloading(NoPreloading), - ), - { - provide: APP_BASE_HREF, - useFactory: getBaseHref, - deps: [DOCUMENT, APP_CONFIG], - }, - { - provide: USER_PROVIDED_META_REDUCERS, - useFactory: getMetaReducers, - deps: [APP_CONFIG], - }, - { - provide: RouterStateSerializer, - useClass: DSpaceRouterStateSerializer, - }, - ClientCookieService, - // register AuthInterceptor as HttpInterceptor - { - provide: HTTP_INTERCEPTORS, - useClass: AuthInterceptor, - multi: true, - }, - // register LocaleInterceptor as HttpInterceptor - { - provide: HTTP_INTERCEPTORS, - useClass: LocaleInterceptor, - multi: true, - }, - // register XsrfInterceptor as HttpInterceptor - { - provide: HTTP_INTERCEPTORS, - useClass: XsrfInterceptor, - multi: true, - }, - // register LogInterceptor as HttpInterceptor - { - provide: HTTP_INTERCEPTORS, - useClass: LogInterceptor, - multi: true, - }, - // register the dynamic matcher used by form. MUST be provided by the app module - ...DYNAMIC_MATCHER_PROVIDERS, -]; - - -@NgModule({ - declarations: [ - AppComponent, - ], - imports: [ - BrowserModule.withServerTransition({ appId: 'dspace-angular' }), - ...IMPORTS, - NgxMaskModule.forRoot(), - ThemedRootComponent, - ], +export const commonAppConfig: ApplicationConfig = { providers: [ - ...PROVIDERS, + importProvidersFrom( + ScrollToModule.forRoot(), + NgbModule, + // TranslateModule.forRoot(), + EffectsModule.forRoot(appEffects), + StoreModule.forRoot(appReducers, storeModuleConfig), + StoreRouterConnectingModule.forRoot(), + StoreDevModules, + EagerThemesModule, + RootModule, + ListableModule.withEntryComponents(), + NgxMaskModule.forRoot(), + ), + provideRouter( + APP_ROUTES, + withRouterConfig(APP_ROUTING_CONF), + withInMemoryScrolling(APP_ROUTING_SCROLL_CONF), + withEnabledBlockingInitialNavigation(), + withPreloading(NoPreloading), + ), + { + provide: APP_BASE_HREF, + useFactory: getBaseHref, + deps: [DOCUMENT, APP_CONFIG], + }, + { + provide: USER_PROVIDED_META_REDUCERS, + useFactory: getMetaReducers, + deps: [APP_CONFIG], + }, + { + provide: RouterStateSerializer, + useClass: DSpaceRouterStateSerializer, + }, + ClientCookieService, + // register AuthInterceptor as HttpInterceptor + { + provide: HTTP_INTERCEPTORS, + useClass: AuthInterceptor, + multi: true, + }, + // register LocaleInterceptor as HttpInterceptor + { + provide: HTTP_INTERCEPTORS, + useClass: LocaleInterceptor, + multi: true, + }, + // register XsrfInterceptor as HttpInterceptor + { + provide: HTTP_INTERCEPTORS, + useClass: XsrfInterceptor, + multi: true, + }, + // register LogInterceptor as HttpInterceptor + { + provide: HTTP_INTERCEPTORS, + useClass: LogInterceptor, + multi: true, + }, + // register the dynamic matcher used by form. MUST be provided by the app module + ...DYNAMIC_MATCHER_PROVIDERS, provideCore(), ], - bootstrap: [AppComponent], -}) -export class AppModule { +}; - /* Use models object so all decorators are actually called */ - modelList = models; - workflowTasks = WORKFLOW_TASK_OPTION_DECORATOR_MAP; - advancedWorfklowTasks = ADVANCED_WORKFLOW_TASK_OPTION_DECORATOR_MAP; - metadataRepresentations = METADATA_REPRESENTATION_COMPONENT_DECORATOR_MAP; - startsWithDecoratorMap = STARTS_WITH_DECORATOR_MAP; - browseByDecoratorMap = BROWSE_BY_DECORATOR_MAP; - authMethodForDecoratorMap = AUTH_METHOD_FOR_DECORATOR_MAP; -} + +/* Use models object so all decorators are actually called */ +const modelList = models; +const workflowTasks = WORKFLOW_TASK_OPTION_DECORATOR_MAP; +const advancedWorfklowTasks = ADVANCED_WORKFLOW_TASK_OPTION_DECORATOR_MAP; +const metadataRepresentations = METADATA_REPRESENTATION_COMPONENT_DECORATOR_MAP; +const startsWithDecoratorMap = STARTS_WITH_DECORATOR_MAP; +const browseByDecoratorMap = BROWSE_BY_DECORATOR_MAP; +const authMethodForDecoratorMap = AUTH_METHOD_FOR_DECORATOR_MAP; 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 1fe54a523f..f6a039b1d8 100644 --- a/src/app/bitstream-page/bitstream-page.resolver.ts +++ b/src/app/bitstream-page/bitstream-page.resolver.ts @@ -1,7 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, - Resolve, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; import { Observable } from 'rxjs'; @@ -25,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 implements Resolve> { - 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 36e0cd5532..8b9b1127b1 100644 --- a/src/app/bitstream-page/legacy-bitstream-url.resolver.ts +++ b/src/app/bitstream-page/legacy-bitstream-url.resolver.ts @@ -1,7 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, - Resolve, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; import { Observable } from 'rxjs'; @@ -13,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 implements Resolve> { - 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 af1301027e..62fff2fbc6 100644 --- a/src/app/browse-by/browse-by-guard.ts +++ b/src/app/browse-by/browse-by-guard.ts @@ -1,7 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, - CanActivate, + CanActivateFn, Data, Router, RouterStateSnapshot, @@ -26,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 implements CanActivate { - - 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 89255df286..6befefc152 100644 --- a/src/app/collection-page/collection-page.resolver.ts +++ b/src/app/collection-page/collection-page.resolver.ts @@ -1,12 +1,13 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, - Resolve, + 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'; @@ -29,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 implements Resolve> { - 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 0cdf1ff94c..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,39 +1,23 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, - Resolve, + 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 implements Resolve> { - 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-list-page/community-list/community-list.component.html b/src/app/community-list-page/community-list/community-list.component.html index ea34e02d56..0d678299e1 100644 --- a/src/app/community-list-page/community-list/community-list.component.html +++ b/src/app/community-list-page/community-list/community-list.component.html @@ -7,7 +7,7 @@ -
+
@@ -71,7 +71,7 @@ -
+
{{ dsoNameService.getName(node.payload) }}
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 71780d96e9..b8820629e7 100644 --- a/src/app/community-page/community-page.resolver.ts +++ b/src/app/community-page/community-page.resolver.ts @@ -1,12 +1,13 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, - Resolve, + 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'; @@ -29,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 implements Resolve> { - 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 8f0cdd8bf4..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,7 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, - CanActivate, + CanActivateFn, Router, RouterStateSnapshot, } from '@angular/router'; @@ -24,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 implements CanActivate { - 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 b6c575d789..c76480ec0d 100644 --- a/src/app/core/auth/auth-blocking.guard.ts +++ b/src/app/core/auth/auth-blocking.guard.ts @@ -1,5 +1,9 @@ -import { Injectable } from '@angular/core'; -import { CanActivate } from '@angular/router'; +import { inject } from '@angular/core'; +import { + ActivatedRouteSnapshot, + CanActivateFn, + RouterStateSnapshot, +} from '@angular/router'; import { select, Store, @@ -20,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 implements CanActivate { +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 6269988ced..eba6dc89f9 100644 --- a/src/app/core/auth/authenticated.guard.ts +++ b/src/app/core/auth/authenticated.guard.ts @@ -1,7 +1,8 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, - CanActivate, + CanActivateChildFn, + CanActivateFn, Router, RouterStateSnapshot, UrlTree, @@ -17,7 +18,7 @@ import { switchMap, } from 'rxjs/operators'; -import { CoreState } from '../core-state.model'; +import { AppState } from '../../app.reducer'; import { AuthService, LOGIN_ROUTE, @@ -29,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 implements CanActivate { +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 712f40a3c5..cb1f96b103 100644 --- a/src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts @@ -1,7 +1,5 @@ -import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, - Resolve, RouterStateSnapshot, } from '@angular/router'; import { Observable } from 'rxjs'; @@ -11,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, @@ -20,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 implements Resolve> { - 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 afcd461de8..5f5c779211 100644 --- a/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.ts @@ -1,7 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, - Resolve, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; @@ -11,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 implements Resolve> { - 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 594c1a694f..ac306ee3f5 100644 --- a/src/app/core/breadcrumbs/navigation-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/navigation-breadcrumb.resolver.ts @@ -1,7 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, - Resolve, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; @@ -9,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 implements Resolve> { - - 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 7bcff921e1..a1b52ce333 100644 --- a/src/app/core/breadcrumbs/publication-claim-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/publication-claim-breadcrumb.resolver.ts @@ -1,29 +1,18 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, - Resolve, + 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 implements Resolve> { - 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 832cd5d08c..6507a75de6 100644 --- a/src/app/core/breadcrumbs/quality-assurance-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/quality-assurance-breadcrumb.resolver.ts @@ -1,37 +1,27 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, - Resolve, + 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 implements Resolve> { - 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/cache/builders/link.service.spec.ts b/src/app/core/cache/builders/link.service.spec.ts index 96650f1a55..d987891388 100644 --- a/src/app/core/cache/builders/link.service.spec.ts +++ b/src/app/core/cache/builders/link.service.spec.ts @@ -37,9 +37,9 @@ class TestModel implements HALResource { successor?: TestModel; } -const mockDataServiceMap: any = { - [TEST_MODEL.value]: () => import('../../../shared/testing/test-data-service.mock').then(m => m.TestDataService), -}; +const mockDataServiceMap: any = new Map([ + [TEST_MODEL.value, () => import('../../../shared/testing/test-data-service.mock').then(m => m.TestDataService)], +]); let testDataService: TestDataService; diff --git a/src/app/core/cache/builders/link.service.ts b/src/app/core/cache/builders/link.service.ts index b1ced76bb8..6265e89d53 100644 --- a/src/app/core/cache/builders/link.service.ts +++ b/src/app/core/cache/builders/link.service.ts @@ -1,7 +1,6 @@ import { Inject, Injectable, - InjectionToken, Injector, } from '@angular/core'; import { @@ -25,7 +24,7 @@ import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model import { HALDataService } from '../../data/base/hal-data-service.interface'; import { PaginatedList } from '../../data/paginated-list.model'; import { RemoteData } from '../../data/remote-data'; -import { lazyService } from '../../lazy-service'; +import { lazyDataService } from '../../lazy-data-service'; import { GenericConstructor } from '../../shared/generic-constructor'; import { HALResource } from '../../shared/hal-resource.model'; import { @@ -43,7 +42,7 @@ export class LinkService { constructor( protected injector: Injector, - @Inject(APP_DATA_SERVICES_MAP) private map: InjectionToken, + @Inject(APP_DATA_SERVICES_MAP) private map: LazyDataServicesMap, @Inject(LINK_DEFINITION_FACTORY) private getLinkDefinition: (source: GenericConstructor, linkName: keyof T['_links']) => LinkDefinition, @Inject(LINK_DEFINITION_MAP_FACTORY) private getLinkDefinitions: (source: GenericConstructor) => Map>, ) { @@ -73,7 +72,7 @@ export class LinkService { public resolveLinkWithoutAttaching(model, linkToFollow: FollowLinkConfig): Observable>> { const matchingLinkDef = this.getLinkDefinition(model.constructor, linkToFollow.name); if (hasValue(matchingLinkDef)) { - const lazyProvider$: Observable> = lazyService(this.map[matchingLinkDef.resourceType.value], this.injector); + const lazyProvider$: Observable> = lazyDataService(this.map, matchingLinkDef.resourceType.value, this.injector); return lazyProvider$.pipe( switchMap((provider: HALDataService) => { const link = model._links[matchingLinkDef.linkName]; 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-services-map.ts b/src/app/core/data-services-map.ts index 12fc91b678..8e76eef925 100644 --- a/src/app/core/data-services-map.ts +++ b/src/app/core/data-services-map.ts @@ -69,73 +69,71 @@ import { CLAIMED_TASK } from './tasks/models/claimed-task-object.resource-type'; import { POOL_TASK } from './tasks/models/pool-task-object.resource-type'; import { WORKFLOW_ACTION } from './tasks/models/workflow-action-object.resource-type'; -export const LAZY_DATA_SERVICES: LazyDataServicesMap = { - [AUTHORIZATION.value]: () => import('./data/feature-authorization/authorization-data.service').then(m => m.AuthorizationDataService), - [BROWSE_DEFINITION.value]: () => import('./browse/browse-definition-data.service').then(m => m.BrowseDefinitionDataService), - [BULK_ACCESS_CONDITION_OPTIONS.value]: () => import('./config/bulk-access-config-data.service').then(m => m.BulkAccessConfigDataService), - [METADATA_SCHEMA.value]: () => import('./data/metadata-schema-data.service').then(m => m.MetadataSchemaDataService), - [SUBMISSION_UPLOADS_TYPE.value]: () => import('./config/submission-uploads-config-data.service').then(m => m.SubmissionUploadsConfigDataService), - [BITSTREAM.value]: () => import('./data/bitstream-data.service').then(m => m.BitstreamDataService), - [SUBMISSION_ACCESSES_TYPE.value]: () => import('./config/submission-accesses-config-data.service').then(m => m.SubmissionAccessesConfigDataService), - [SYSTEMWIDEALERT.value]: () => import('./data/system-wide-alert-data.service').then(m => m.SystemWideAlertDataService), - [USAGE_REPORT.value]: () => import('./statistics/usage-report-data.service').then(m => m.UsageReportDataService), - [ACCESS_STATUS.value]: () => import('./data/access-status-data.service').then(m => m.AccessStatusDataService), - [COLLECTION.value]: () => import('./data/collection-data.service').then(m => m.CollectionDataService), - [CLAIMED_TASK.value]: () => import('./tasks/claimed-task-data.service').then(m => m.ClaimedTaskDataService), - [VOCABULARY_ENTRY.value]: () => import('./data/href-only-data.service').then(m => m.HrefOnlyDataService), - [ITEM_TYPE.value]: () => import('./data/href-only-data.service').then(m => m.HrefOnlyDataService), - [LICENSE.value]: () => import('./data/href-only-data.service').then(m => m.HrefOnlyDataService), - [SUBSCRIPTION.value]: () => import('../shared/subscriptions/subscriptions-data.service').then(m => m.SubscriptionsDataService), - [COMMUNITY.value]: () => import('./data/community-data.service').then(m => m.CommunityDataService), - [VOCABULARY.value]: () => import('./submission/vocabularies/vocabulary.data.service').then(m => m.VocabularyDataService), - [BUNDLE.value]: () => import('./data/bundle-data.service').then(m => m.BundleDataService), - [CONFIG_PROPERTY.value]: () => import('./data/configuration-data.service').then(m => m.ConfigurationDataService), - [POOL_TASK.value]: () => import('./tasks/pool-task-data.service').then(m => m.PoolTaskDataService), - [CLAIMED_TASK.value]: () => import('./tasks/claimed-task-data.service').then(m => m.ClaimedTaskDataService), - [SUPERVISION_ORDER.value]: () => import('./supervision-order/supervision-order-data.service').then(m => m.SupervisionOrderDataService), - [WORKSPACEITEM.value]: () => import('./submission/workspaceitem-data.service').then(m => m.WorkspaceitemDataService), - [WORKFLOWITEM.value]: () => import('./submission/workflowitem-data.service').then(m => m.WorkflowItemDataService), - [VOCABULARY.value]: () => import('./submission/vocabularies/vocabulary.data.service').then(m => m.VocabularyDataService), - [VOCABULARY_ENTRY_DETAIL.value]: () => import('./submission/vocabularies/vocabulary-entry-details.data.service').then(m => m.VocabularyEntryDetailsDataService), - [SUBMISSION_CC_LICENSE_URL.value]: () => import('./submission/submission-cc-license-url-data.service').then(m => m.SubmissionCcLicenseUrlDataService), - [SUBMISSION_CC_LICENSE.value]: () => import('./submission/submission-cc-license-data.service').then(m => m.SubmissionCcLicenseDataService), - [USAGE_REPORT.value]: () => import('./statistics/usage-report-data.service').then(m => m.UsageReportDataService), - [RESOURCE_POLICY.value]: () => import('./resource-policy/resource-policy-data.service').then(m => m.ResourcePolicyDataService), - [RESEARCHER_PROFILE.value]: () => import('./profile/researcher-profile-data.service').then(m => m.ResearcherProfileDataService), - [ORCID_QUEUE.value]: () => import('./orcid/orcid-queue-data.service').then(m => m.OrcidQueueDataService), - [ORCID_HISTORY.value]: () => import('./orcid/orcid-history-data.service').then(m => m.OrcidHistoryDataService), - [FEEDBACK.value]: () => import('./feedback/feedback-data.service').then(m => m.FeedbackDataService), - [GROUP.value]: () => import('./eperson/group-data.service').then(m => m.GroupDataService), - [EPERSON.value]: () => import('./eperson/eperson-data.service').then(m => m.EPersonDataService), - [WORKFLOW_ACTION.value]: () => import('./data/workflow-action-data.service').then(m => m.WorkflowActionDataService), - [VERSION_HISTORY.value]: () => import('./data/version-history-data.service').then(m => m.VersionHistoryDataService), - [SITE.value]: () => import('./data/site-data.service').then(m => m.SiteDataService), - [ROOT.value]: () => import('./data/root-data.service').then(m => m.RootDataService), - [RELATIONSHIP_TYPE.value]: () => import('./data/relationship-type-data.service').then(m => m.RelationshipTypeDataService), - [RELATIONSHIP.value]: () => import('./data/relationship-data.service').then(m => m.RelationshipDataService), - [SCRIPT.value]: () => import('./data/processes/script-data.service').then(m => m.ScriptDataService), - [PROCESS.value]: () => import('./data/processes/process-data.service').then(m => m.ProcessDataService), - [METADATA_FIELD.value]: () => import('./data/metadata-field-data.service').then(m => m.MetadataFieldDataService), - [ITEM.value]: () => import('./data/item-data.service').then(m => m.ItemDataService), - [VERSION.value]: () => import('./data/version-data.service').then(m => m.VersionDataService), - [IDENTIFIERS.value]: () => import('./data/identifier-data.service').then(m => m.IdentifierDataService), - [FEATURE.value]: () => import('./data/feature-authorization/authorization-data.service').then(m => m.AuthorizationDataService), - [DSPACE_OBJECT.value]: () => import('./data/dspace-object-data.service').then(m => m.DSpaceObjectDataService), - [BITSTREAM_FORMAT.value]: () => import('./data/bitstream-format-data.service').then(m => m.BitstreamFormatDataService), - [SUBMISSION_COAR_NOTIFY_CONFIG.value]: () => import('../submission/sections/section-coar-notify/coar-notify-config-data.service').then(m => m.CoarNotifyConfigDataService), - [LDN_SERVICE_CONSTRAINT_FILTERS.value]: () => import('../admin/admin-ldn-services/ldn-services-data/ldn-itemfilters-data.service').then(m => m.LdnItemfiltersService), - [LDN_SERVICE.value]: () => import('../admin/admin-ldn-services/ldn-services-data/ldn-services-data.service').then(m => m.LdnServicesService), - [ADMIN_NOTIFY_MESSAGE.value]: () => import('../admin/admin-notify-dashboard/services/admin-notify-messages.service').then(m => m.AdminNotifyMessagesService), - [SUBMISSION_FORMS_TYPE.value]: () => import('./config/submission-forms-config-data.service').then(m => m.SubmissionFormsConfigDataService), - [NOTIFYREQUEST.value]: () => import('./data/notify-services-status-data.service').then(m => m.NotifyRequestsStatusDataService), - [QUALITY_ASSURANCE_EVENT_OBJECT.value]: () => import('./notifications/qa/events/quality-assurance-event-data.service').then(m => m.QualityAssuranceEventDataService), - [QUALITY_ASSURANCE_SOURCE_OBJECT.value]: () => import('./notifications/qa/source/quality-assurance-source-data.service').then(m => m.QualityAssuranceSourceDataService), - [QUALITY_ASSURANCE_TOPIC_OBJECT.value]: () => import('./notifications/qa/topics/quality-assurance-topic-data.service').then(m => m.QualityAssuranceTopicDataService), - [SUGGESTION.value]: () => import('./notifications/suggestions-data.service').then(m => m.SuggestionsDataService), - [SUGGESTION_SOURCE.value]: () => import('./notifications/source/suggestion-source-data.service').then(m => m.SuggestionSourceDataService), - [SUGGESTION_TARGET.value]: () => import('./notifications/target/suggestion-target-data.service').then(m => m.SuggestionTargetDataService), - [DUPLICATE.value]: () => import('./submission/submission-duplicate-data.service').then(m => m.SubmissionDuplicateDataService), - [CorrectionType.type.value]: () => import('./submission/correctiontype-data.service').then(m => m.CorrectionTypeDataService), -}; - - +export const LAZY_DATA_SERVICES: LazyDataServicesMap = new Map([ + [AUTHORIZATION.value, () => import('./data/feature-authorization/authorization-data.service').then(m => m.AuthorizationDataService)], + [BROWSE_DEFINITION.value, () => import('./browse/browse-definition-data.service').then(m => m.BrowseDefinitionDataService)], + [BULK_ACCESS_CONDITION_OPTIONS.value, () => import('./config/bulk-access-config-data.service').then(m => m.BulkAccessConfigDataService)], + [METADATA_SCHEMA.value, () => import('./data/metadata-schema-data.service').then(m => m.MetadataSchemaDataService)], + [SUBMISSION_UPLOADS_TYPE.value, () => import('./config/submission-uploads-config-data.service').then(m => m.SubmissionUploadsConfigDataService)], + [BITSTREAM.value, () => import('./data/bitstream-data.service').then(m => m.BitstreamDataService)], + [SUBMISSION_ACCESSES_TYPE.value, () => import('./config/submission-accesses-config-data.service').then(m => m.SubmissionAccessesConfigDataService)], + [SYSTEMWIDEALERT.value, () => import('./data/system-wide-alert-data.service').then(m => m.SystemWideAlertDataService)], + [USAGE_REPORT.value, () => import('./statistics/usage-report-data.service').then(m => m.UsageReportDataService)], + [ACCESS_STATUS.value, () => import('./data/access-status-data.service').then(m => m.AccessStatusDataService)], + [COLLECTION.value, () => import('./data/collection-data.service').then(m => m.CollectionDataService)], + [CLAIMED_TASK.value, () => import('./tasks/claimed-task-data.service').then(m => m.ClaimedTaskDataService)], + [VOCABULARY_ENTRY.value, () => import('./data/href-only-data.service').then(m => m.HrefOnlyDataService)], + [ITEM_TYPE.value, () => import('./data/href-only-data.service').then(m => m.HrefOnlyDataService)], + [LICENSE.value, () => import('./data/href-only-data.service').then(m => m.HrefOnlyDataService)], + [SUBSCRIPTION.value, () => import('../shared/subscriptions/subscriptions-data.service').then(m => m.SubscriptionsDataService)], + [COMMUNITY.value, () => import('./data/community-data.service').then(m => m.CommunityDataService)], + [VOCABULARY.value, () => import('./submission/vocabularies/vocabulary.data.service').then(m => m.VocabularyDataService)], + [BUNDLE.value, () => import('./data/bundle-data.service').then(m => m.BundleDataService)], + [CONFIG_PROPERTY.value, () => import('./data/configuration-data.service').then(m => m.ConfigurationDataService)], + [POOL_TASK.value, () => import('./tasks/pool-task-data.service').then(m => m.PoolTaskDataService)], + [CLAIMED_TASK.value, () => import('./tasks/claimed-task-data.service').then(m => m.ClaimedTaskDataService)], + [SUPERVISION_ORDER.value, () => import('./supervision-order/supervision-order-data.service').then(m => m.SupervisionOrderDataService)], + [WORKSPACEITEM.value, () => import('./submission/workspaceitem-data.service').then(m => m.WorkspaceitemDataService)], + [WORKFLOWITEM.value, () => import('./submission/workflowitem-data.service').then(m => m.WorkflowItemDataService)], + [VOCABULARY.value, () => import('./submission/vocabularies/vocabulary.data.service').then(m => m.VocabularyDataService)], + [VOCABULARY_ENTRY_DETAIL.value, () => import('./submission/vocabularies/vocabulary-entry-details.data.service').then(m => m.VocabularyEntryDetailsDataService)], + [SUBMISSION_CC_LICENSE_URL.value, () => import('./submission/submission-cc-license-url-data.service').then(m => m.SubmissionCcLicenseUrlDataService)], + [SUBMISSION_CC_LICENSE.value, () => import('./submission/submission-cc-license-data.service').then(m => m.SubmissionCcLicenseDataService)], + [USAGE_REPORT.value, () => import('./statistics/usage-report-data.service').then(m => m.UsageReportDataService)], + [RESOURCE_POLICY.value, () => import('./resource-policy/resource-policy-data.service').then(m => m.ResourcePolicyDataService)], + [RESEARCHER_PROFILE.value, () => import('./profile/researcher-profile-data.service').then(m => m.ResearcherProfileDataService)], + [ORCID_QUEUE.value, () => import('./orcid/orcid-queue-data.service').then(m => m.OrcidQueueDataService)], + [ORCID_HISTORY.value, () => import('./orcid/orcid-history-data.service').then(m => m.OrcidHistoryDataService)], + [FEEDBACK.value, () => import('./feedback/feedback-data.service').then(m => m.FeedbackDataService)], + [GROUP.value, () => import('./eperson/group-data.service').then(m => m.GroupDataService)], + [EPERSON.value, () => import('./eperson/eperson-data.service').then(m => m.EPersonDataService)], + [WORKFLOW_ACTION.value, () => import('./data/workflow-action-data.service').then(m => m.WorkflowActionDataService)], + [VERSION_HISTORY.value, () => import('./data/version-history-data.service').then(m => m.VersionHistoryDataService)], + [SITE.value, () => import('./data/site-data.service').then(m => m.SiteDataService)], + [ROOT.value, () => import('./data/root-data.service').then(m => m.RootDataService)], + [RELATIONSHIP_TYPE.value, () => import('./data/relationship-type-data.service').then(m => m.RelationshipTypeDataService)], + [RELATIONSHIP.value, () => import('./data/relationship-data.service').then(m => m.RelationshipDataService)], + [SCRIPT.value, () => import('./data/processes/script-data.service').then(m => m.ScriptDataService)], + [PROCESS.value, () => import('./data/processes/process-data.service').then(m => m.ProcessDataService)], + [METADATA_FIELD.value, () => import('./data/metadata-field-data.service').then(m => m.MetadataFieldDataService)], + [ITEM.value, () => import('./data/item-data.service').then(m => m.ItemDataService)], + [VERSION.value, () => import('./data/version-data.service').then(m => m.VersionDataService)], + [IDENTIFIERS.value, () => import('./data/identifier-data.service').then(m => m.IdentifierDataService)], + [FEATURE.value, () => import('./data/feature-authorization/authorization-data.service').then(m => m.AuthorizationDataService)], + [DSPACE_OBJECT.value, () => import('./data/dspace-object-data.service').then(m => m.DSpaceObjectDataService)], + [BITSTREAM_FORMAT.value, () => import('./data/bitstream-format-data.service').then(m => m.BitstreamFormatDataService)], + [SUBMISSION_COAR_NOTIFY_CONFIG.value, () => import('../submission/sections/section-coar-notify/coar-notify-config-data.service').then(m => m.CoarNotifyConfigDataService)], + [LDN_SERVICE_CONSTRAINT_FILTERS.value, () => import('../admin/admin-ldn-services/ldn-services-data/ldn-itemfilters-data.service').then(m => m.LdnItemfiltersService)], + [LDN_SERVICE.value, () => import('../admin/admin-ldn-services/ldn-services-data/ldn-services-data.service').then(m => m.LdnServicesService)], + [ADMIN_NOTIFY_MESSAGE.value, () => import('../admin/admin-notify-dashboard/services/admin-notify-messages.service').then(m => m.AdminNotifyMessagesService)], + [SUBMISSION_FORMS_TYPE.value, () => import('./config/submission-forms-config-data.service').then(m => m.SubmissionFormsConfigDataService)], + [NOTIFYREQUEST.value, () => import('./data/notify-services-status-data.service').then(m => m.NotifyRequestsStatusDataService)], + [QUALITY_ASSURANCE_EVENT_OBJECT.value, () => import('./notifications/qa/events/quality-assurance-event-data.service').then(m => m.QualityAssuranceEventDataService)], + [QUALITY_ASSURANCE_SOURCE_OBJECT.value, () => import('./notifications/qa/source/quality-assurance-source-data.service').then(m => m.QualityAssuranceSourceDataService)], + [QUALITY_ASSURANCE_TOPIC_OBJECT.value, () => import('./notifications/qa/topics/quality-assurance-topic-data.service').then(m => m.QualityAssuranceTopicDataService)], + [SUGGESTION.value, () => import('./notifications/suggestions-data.service').then(m => m.SuggestionsDataService)], + [SUGGESTION_SOURCE.value, () => import('./notifications/source/suggestion-source-data.service').then(m => m.SuggestionSourceDataService)], + [SUGGESTION_TARGET.value, () => import('./notifications/target/suggestion-target-data.service').then(m => m.SuggestionTargetDataService)], + [DUPLICATE.value, () => import('./submission/submission-duplicate-data.service').then(m => m.SubmissionDuplicateDataService)], + [CorrectionType.type.value, () => import('./submission/correctiontype-data.service').then(m => m.CorrectionTypeDataService)], +]); 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 234c2e1628..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 @@ -1,6 +1,6 @@ import { ActivatedRouteSnapshot, - Resolve, + ResolveFn, Router, RouterStateSnapshot, } from '@angular/router'; @@ -12,21 +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>, - 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 { @@ -39,25 +48,16 @@ describe('DsoPageSingleFeatureGuard', () => { let authorizationService: AuthorizationDataService; let router: Router; let authService: AuthService; - let resolver: Resolve>; - 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), }); @@ -71,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 61e236188d..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 @@ -1,6 +1,6 @@ import { ActivatedRouteSnapshot, - Resolve, + ResolveFn, Router, RouterStateSnapshot, } from '@angular/router'; @@ -12,21 +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>, - 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 { @@ -39,25 +48,17 @@ describe('DsoPageSomeFeatureGuard', () => { let authorizationService: AuthorizationDataService; let router: Router; let authService: AuthService; - let resolver: Resolve>; - 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), }); @@ -71,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 eff2da2102..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 @@ -1,6 +1,6 @@ import { ActivatedRouteSnapshot, - Resolve, + ResolveFn, Router, RouterStateSnapshot, } from '@angular/router'; @@ -23,8 +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>, - protected authorizationService: AuthorizationDataService, + + protected abstract resolver: ResolveFn>; + + constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) { super(authorizationService, router, authService); @@ -35,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/feature-authorization/feature-authorization-guard/some-feature-authorization.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/some-feature-authorization.guard.ts index 0849c5a96a..229321452f 100644 --- a/src/app/core/data/feature-authorization/feature-authorization-guard/some-feature-authorization.guard.ts +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/some-feature-authorization.guard.ts @@ -1,6 +1,5 @@ import { ActivatedRouteSnapshot, - CanActivate, Router, RouterStateSnapshot, UrlTree, @@ -22,7 +21,7 @@ import { FeatureID } from '../feature-id'; * doesn't have authorized rights on any of the specified features and/or object. * Override the desired getters in the parent class for checking specific authorization on a list of features and/or object. */ -export abstract class SomeFeatureAuthorizationGuard implements CanActivate { +export abstract class SomeFeatureAuthorizationGuard { constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) { diff --git a/src/app/core/data/object-updates/object-updates.service.spec.ts b/src/app/core/data/object-updates/object-updates.service.spec.ts index 21486a581c..6602cda080 100644 --- a/src/app/core/data/object-updates/object-updates.service.spec.ts +++ b/src/app/core/data/object-updates/object-updates.service.spec.ts @@ -1,5 +1,6 @@ import { Injector } from '@angular/core'; import { Store } from '@ngrx/store'; +import { createMockStore } from '@ngrx/store/testing'; import { of as observableOf } from 'rxjs'; import { Notification } from '../../../shared/notifications/models/notification.model'; @@ -52,7 +53,7 @@ describe('ObjectUpdatesService', () => { const objectEntry = { fieldStates, fieldUpdates, lastModified: modDate, virtualMetadataSources: {}, patchOperationService, }; - store = new Store(undefined, undefined, undefined); + store = createMockStore({}); spyOn(store, 'dispatch'); injector = jasmine.createSpyObj('injector', { get: patchOperationService, diff --git a/src/app/core/data/request.effects.ts b/src/app/core/data/request.effects.ts index 6501f43133..bc403bc271 100644 --- a/src/app/core/data/request.effects.ts +++ b/src/app/core/data/request.effects.ts @@ -13,6 +13,7 @@ import { map, mergeMap, take, + withLatestFrom, } from 'rxjs/operators'; import { @@ -25,6 +26,7 @@ import { ParsedResponse } from '../cache/response.models'; import { DSpaceSerializer } from '../dspace-rest/dspace.serializer'; import { DspaceRestService } from '../dspace-rest/dspace-rest.service'; import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; +import { XSRFService } from '../xsrf/xsrf.service'; import { RequestActionTypes, RequestErrorAction, @@ -35,6 +37,7 @@ import { import { RequestService } from './request.service'; import { RequestEntry } from './request-entry.model'; import { RequestError } from './request-error.model'; +import { RestRequestMethod } from './rest-request-method'; import { RestRequestWithResponseParser } from './rest-request-with-response-parser.model'; @Injectable() @@ -48,7 +51,11 @@ export class RequestEffects { ); }), filter((entry: RequestEntry) => hasValue(entry)), - map((entry: RequestEntry) => entry.request), + withLatestFrom(this.xsrfService.tokenInitialized$), + // If it's a GET request, or we have an XSRF token, dispatch it immediately + // Otherwise wait for the XSRF token first + filter(([entry, tokenInitialized]: [RequestEntry, boolean]) => entry.request.method === RestRequestMethod.GET || tokenInitialized === true), + map(([entry, tokenInitialized]: [RequestEntry, boolean]) => entry.request), mergeMap((request: RestRequestWithResponseParser) => { let body = request.body; if (isNotEmpty(request.body) && !request.isMultipart) { @@ -89,6 +96,7 @@ export class RequestEffects { private restApi: DspaceRestService, private injector: Injector, protected requestService: RequestService, + protected xsrfService: XSRFService, ) { } } diff --git a/src/app/core/data/request.service.spec.ts b/src/app/core/data/request.service.spec.ts index 76e70e8a6d..d8fa04973e 100644 --- a/src/app/core/data/request.service.spec.ts +++ b/src/app/core/data/request.service.spec.ts @@ -1,5 +1,6 @@ import { fakeAsync, + flush, TestBed, waitForAsync, } from '@angular/core/testing'; @@ -104,11 +105,11 @@ describe('RequestService', () => { store = TestBed.inject(Store); mockStore = store as MockStore; mockStore.setState(initialState); + service = new RequestService( objectCache, uuidService, store, - undefined, ); serviceAsAny = service as any; }); @@ -501,21 +502,23 @@ describe('RequestService', () => { dispatchSpy = spyOn(store, 'dispatch'); }); - it('should dispatch a RequestConfigureAction', () => { + it('should dispatch a RequestConfigureAction', fakeAsync(() => { const request = testGetRequest; serviceAsAny.dispatchRequest(request); + flush(); const firstAction = dispatchSpy.calls.argsFor(0)[0]; expect(firstAction).toBeInstanceOf(RequestConfigureAction); expect(firstAction.payload).toEqual(request); - }); + })); - it('should dispatch a RequestExecuteAction', () => { + it('should dispatch a RequestExecuteAction', fakeAsync(() => { const request = testGetRequest; serviceAsAny.dispatchRequest(request); + flush(); const secondAction = dispatchSpy.calls.argsFor(1)[0]; expect(secondAction).toBeInstanceOf(RequestExecuteAction); expect(secondAction.payload).toEqual(request.uuid); - }); + })); describe('when it\'s not a GET request', () => { it('shouldn\'t track it', () => { diff --git a/src/app/core/data/request.service.ts b/src/app/core/data/request.service.ts index 9bd262b1ad..52ec9b56e2 100644 --- a/src/app/core/data/request.service.ts +++ b/src/app/core/data/request.service.ts @@ -8,6 +8,7 @@ import { } from '@ngrx/store'; import cloneDeep from 'lodash/cloneDeep'; import { + asapScheduler, from as observableFrom, Observable, } from 'rxjs'; @@ -33,10 +34,7 @@ import { ObjectCacheService } from '../cache/object-cache.service'; import { CommitSSBAction } from '../cache/server-sync-buffer.actions'; import { coreSelector } from '../core.selectors'; import { CoreState } from '../core-state.model'; -import { - IndexState, - MetaIndexState, -} from '../index/index.reducer'; +import { IndexState } from '../index/index.reducer'; import { getUrlWithoutEmbedParams, requestIndexSelector, @@ -167,8 +165,7 @@ export class RequestService { constructor(private objectCache: ObjectCacheService, private uuidService: UUIDService, - private store: Store, - private indexStore: Store) { + private store: Store) { } generateRequestId(): string { @@ -241,7 +238,7 @@ export class RequestService { return source.pipe( tap((entry: RequestEntry) => { if (hasValue(entry) && hasValue(entry.request) && !isStale(entry.state) && !isValid(entry)) { - this.store.dispatch(new RequestStaleAction(entry.request.uuid)); + asapScheduler.schedule(() => this.store.dispatch(new RequestStaleAction(entry.request.uuid))); } }), ); @@ -394,6 +391,7 @@ export class RequestService { const requestEntry$ = this.getByHref(href); requestEntry$.pipe( + filter((re: RequestEntry) => isNotEmpty(re)), map((re: RequestEntry) => re.request.uuid), take(1), ).subscribe((uuid: string) => { @@ -449,8 +447,10 @@ export class RequestService { * @param {RestRequest} request to dispatch */ private dispatchRequest(request: RestRequest) { - this.store.dispatch(new RequestConfigureAction(request)); - this.store.dispatch(new RequestExecuteAction(request.uuid)); + asapScheduler.schedule(() => { + this.store.dispatch(new RequestConfigureAction(request)); + this.store.dispatch(new RequestExecuteAction(request.uuid)); + }); } /** diff --git a/src/app/core/end-user-agreement/abstract-end-user-agreement.guard.ts b/src/app/core/end-user-agreement/abstract-end-user-agreement.guard.ts index 7d67872004..2937011a38 100644 --- a/src/app/core/end-user-agreement/abstract-end-user-agreement.guard.ts +++ b/src/app/core/end-user-agreement/abstract-end-user-agreement.guard.ts @@ -1,6 +1,5 @@ import { ActivatedRouteSnapshot, - CanActivate, Router, RouterStateSnapshot, UrlTree, @@ -17,7 +16,7 @@ import { returnEndUserAgreementUrlTreeOnFalse } from '../shared/authorized.opera * An abstract guard for redirecting users to the user agreement page if a certain condition is met * That condition is defined by abstract method hasAccepted */ -export abstract class AbstractEndUserAgreementGuard implements CanActivate { +export abstract class AbstractEndUserAgreementGuard { constructor(protected router: Router) { } diff --git a/src/app/core/eperson/group-data.service.spec.ts b/src/app/core/eperson/group-data.service.spec.ts index 04632763d5..5a4ec018f0 100644 --- a/src/app/core/eperson/group-data.service.spec.ts +++ b/src/app/core/eperson/group-data.service.spec.ts @@ -6,6 +6,7 @@ import { Store, StoreModule, } from '@ngrx/store'; +import { createMockStore } from '@ngrx/store/testing'; import { TranslateLoader, TranslateModule, @@ -104,7 +105,7 @@ describe('GroupDataService', () => { beforeEach(() => { init(); requestService = getMockRequestService(createRequestEntry$(groups)); - store = new Store(undefined, undefined, undefined); + store = createMockStore({}); service = initTestService(); spyOn(store, 'dispatch'); spyOn(rdbService, 'buildFromRequestUUIDAndAwait').and.callThrough(); diff --git a/src/app/core/feedback/feedback.guard.ts b/src/app/core/feedback/feedback.guard.ts index 8446debd53..eef5205ee4 100644 --- a/src/app/core/feedback/feedback.guard.ts +++ b/src/app/core/feedback/feedback.guard.ts @@ -1,7 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, - CanActivate, + CanActivateFn, RouterStateSnapshot, UrlTree, } from '@angular/router'; @@ -11,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 implements CanActivate { +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/lazy-data-service.ts b/src/app/core/lazy-data-service.ts new file mode 100644 index 0000000000..1ad3dec384 --- /dev/null +++ b/src/app/core/lazy-data-service.ts @@ -0,0 +1,50 @@ +import { + Injector, + Type, +} from '@angular/core'; +import { + defer, + Observable, +} from 'rxjs'; + +import { LazyDataServicesMap } from '../../config/app-config.interface'; +import { HALDataService } from './data/base/hal-data-service.interface'; + +/** + * Loads a service lazily. The service is loaded when the observable is subscribed to. + * + * @param dataServicesMap A map of promises returning the data services to load + * @param key The key of the service + * @param injector The injector to use to load the service. If not provided, the current injector is used. + * @returns An observable of the service. + * + * @example + * ```ts + * const dataService$ = lazyDataService({ 'data-service': () => import('./data-service').then((m) => m.MyService)}, 'data-service', this.injector); + * or + * const dataService$ = lazyDataService({'data-service': () => import('./data-service')}, 'data-service', this.injector); + * ``` + */ +export function lazyDataService( + dataServicesMap: LazyDataServicesMap, + key: string, + injector: Injector, +): Observable { + return defer(() => { + if (dataServicesMap.has(key) && typeof dataServicesMap.get(key) === 'function') { + const loader: () => Promise> | { default: HALDataService }> = dataServicesMap.get(key); + return loader() + .then((serviceOrDefault) => { + if ('default' in serviceOrDefault) { + return injector!.get(serviceOrDefault.default); + } + return injector!.get(serviceOrDefault); + }) + .catch((error) => { + throw error; + }); + } else { + return null; + } + }); +} diff --git a/src/app/core/lazy-service.ts b/src/app/core/lazy-service.ts deleted file mode 100644 index 701a1d7dac..0000000000 --- a/src/app/core/lazy-service.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { - Injector, - Type, -} from '@angular/core'; -import { - defer, - Observable, -} from 'rxjs'; - -/** - * Loads a service lazily. The service is loaded when the observable is subscribed to. - * - * @param loader A function that returns a promise of the service to load. - * @param injector The injector to use to load the service. If not provided, the current injector is used. - * @returns An observable of the service. - * - * @example - * ```ts - * const dataService$ = lazyService(() => import('./data-service').then((m) => m.MyService), this.injector); - * or - * const dataService$ = lazyService(() => import('./data-service'), this.injector); - * ``` - */ -export function lazyService( - loader: () => Promise> | Promise<{ default: Type }>, - injector: Injector, -): Observable { - return defer(() => { - return loader() - .then((serviceOrDefault) => { - if ('default' in serviceOrDefault) { - return injector!.get(serviceOrDefault.default); - } - return injector!.get(serviceOrDefault); - }) - .catch((error) => { - throw error; - }); - }); -} diff --git a/src/app/core/metadata/metadata.service.spec.ts b/src/app/core/metadata/metadata.service.spec.ts index b48da35a8e..66715f71f0 100644 --- a/src/app/core/metadata/metadata.service.spec.ts +++ b/src/app/core/metadata/metadata.service.spec.ts @@ -10,7 +10,7 @@ import { NavigationEnd, Router, } from '@angular/router'; -import { getMockStore } from '@ngrx/store/testing'; +import { createMockStore } from '@ngrx/store/testing'; import { TranslateService } from '@ngx-translate/core'; import { Observable, @@ -106,8 +106,7 @@ describe('MetadataService', () => { isAuthorized: observableOf(true), }); - // @ts-ignore - store = getMockStore({ initialState }); + store = createMockStore({ initialState }); spyOn(store, 'dispatch'); appConfig = { 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 3c4a5b6c2e..4e6b1f21a5 100644 --- a/src/app/core/reload/reload.guard.ts +++ b/src/app/core/reload/reload.guard.ts @@ -1,10 +1,7 @@ -import { - Inject, - Injectable, -} from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, - CanActivate, + CanActivateFn, Router, RouterStateSnapshot, UrlTree, @@ -14,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 implements CanActivate { - 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 ccad01d57e..6d4a81b7b5 100644 --- a/src/app/core/server-check/server-check.guard.ts +++ b/src/app/core/server-check/server-check.guard.ts @@ -1,15 +1,13 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, - CanActivateChild, - NavigationStart, + CanActivateChildFn, Router, RouterStateSnapshot, UrlTree, } from '@angular/router'; import { Observable } from 'rxjs'; import { - filter, map, take, } from 'rxjs/operators'; @@ -17,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 implements CanActivateChild { - 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/shared/client-math.service.ts b/src/app/core/shared/client-math.service.ts new file mode 100644 index 0000000000..2fde521ba9 --- /dev/null +++ b/src/app/core/shared/client-math.service.ts @@ -0,0 +1,114 @@ +import { DOCUMENT } from '@angular/common'; +import { + Inject, + Injectable, +} from '@angular/core'; +import { + BehaviorSubject, + Observable, + Subject, +} from 'rxjs'; +import { environment } from 'src/environments/environment'; + +import { + NativeWindowRef, + NativeWindowService, +} from '../services/window.service'; +import { + MathJaxConfig, + MathService, +} from './math.service'; + +@Injectable({ + providedIn: 'root', +}) +/** + * Provide the MathService for CSR + */ +export class ClientMathService extends MathService { + + protected isReady$: Subject; + + protected mathJaxOptions = { + tex: { + inlineMath: [['$', '$'], ['\\(', '\\)']], + }, + svg: { + fontCache: 'global', + }, + startup: { + typeset: false, + }, + }; + + protected mathJax: MathJaxConfig = { + source: 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js', + id: 'MathJaxScript', + }; + protected mathJaxFallback: MathJaxConfig = { + source: 'https://cdnjs.cloudflare.com/ajax/libs/mathjax/3.2.2/es5/tex-chtml.min.js', + id: 'MathJaxBackupScript', + }; + + constructor( + @Inject(DOCUMENT) private _document: Document, + @Inject(NativeWindowService) protected _window: NativeWindowRef, + ) { + super(); + + this.isReady$ = new BehaviorSubject(false); + + void this.registerMathJaxAsync(this.mathJax) + .then(() => this.isReady$.next(true)) + .catch(_ => { + void this.registerMathJaxAsync(this.mathJaxFallback) + .then(() => this.isReady$.next(true)); + }); + } + + /** + * Register the specified MathJax script in the document + * + * @param config The configuration object for the script + */ + protected async registerMathJaxAsync(config: MathJaxConfig): Promise { + if (environment.markdown.mathjax) { + return new Promise((resolve, reject) => { + + const optionsScript: HTMLScriptElement = this._document.createElement('script'); + optionsScript.type = 'text/javascript'; + optionsScript.text = `MathJax = ${JSON.stringify(this.mathJaxOptions)};`; + this._document.head.appendChild(optionsScript); + + const script: HTMLScriptElement = this._document.createElement('script'); + script.id = config.id; + script.type = 'text/javascript'; + script.src = config.source; + script.crossOrigin = 'anonymous'; + script.async = true; + script.onload = () => resolve(); + script.onerror = error => reject(error); + this._document.head.appendChild(script); + }); + } + return Promise.resolve(); + } + + /** + * Return the status of the script registration + */ + ready(): Observable { + return this.isReady$; + } + + /** + * Render the specified element using the MathJax JavaScript + * + * @param element The element to render with MathJax + */ + render(element: HTMLElement) { + if (environment.markdown.mathjax) { + this._window.nativeWindow.MathJax.typesetPromise([element]); + } + } +} diff --git a/src/app/core/shared/math.service.spec.ts b/src/app/core/shared/math.service.spec.ts new file mode 100644 index 0000000000..cc0b2814f2 --- /dev/null +++ b/src/app/core/shared/math.service.spec.ts @@ -0,0 +1,54 @@ +import { TestBed } from '@angular/core/testing'; +import { + Observable, + of, +} from 'rxjs'; + +import { + MathJaxConfig, + MathService, +} from './math.service'; + +export class MockMathService extends MathService { + protected mathJaxOptions: any = {}; + protected mathJax: MathJaxConfig = { source: '', id: '' }; + protected mathJaxFallback: MathJaxConfig = { source: '', id: '' }; + + protected registerMathJaxAsync(config: MathJaxConfig): Promise { + return Promise.resolve(); + } + + ready(): Observable { + return of(true); + } + + render(element: HTMLElement): void { + return; + } +} + +describe('MathService', () => { + let service: MockMathService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = new MockMathService(); + spyOn(service, 'render'); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should be ready', (done) => { + service.ready().subscribe(isReady => { + expect(isReady).toBe(true); + done(); + }); + }); + + it('should render', () => { + service.render(document.createElement('div')); + expect(service.render).toHaveBeenCalled(); + }); +}); diff --git a/src/app/core/shared/math.service.ts b/src/app/core/shared/math.service.ts new file mode 100644 index 0000000000..c06ce06220 --- /dev/null +++ b/src/app/core/shared/math.service.ts @@ -0,0 +1,19 @@ +import { Observable } from 'rxjs'; + +export interface MathJaxConfig { + source: string; + id: string; +} + +/** + * This service is used to provide the MathJax library with the ability to render markdown code + */ +export abstract class MathService { + protected abstract mathJaxOptions: any; + protected abstract mathJax: MathJaxConfig; + protected abstract mathJaxFallback: MathJaxConfig; + + protected abstract registerMathJaxAsync(config: MathJaxConfig): Promise; + abstract ready(): Observable; + abstract render(element: HTMLElement): void; +} diff --git a/src/app/core/shared/server-math.service.ts b/src/app/core/shared/server-math.service.ts new file mode 100644 index 0000000000..75fa775fee --- /dev/null +++ b/src/app/core/shared/server-math.service.ts @@ -0,0 +1,52 @@ +import { Injectable } from '@angular/core'; +import { + BehaviorSubject, + Observable, + Subject, +} from 'rxjs'; + +import { + MathJaxConfig, + MathService, +} from './math.service'; + +@Injectable({ + providedIn: 'root', +}) +/** + * Provide the MathService for SSR + */ +export class ServerMathService extends MathService { + + protected isReady$: Subject; + + protected mathJaxOptions = {}; + + protected mathJax: MathJaxConfig = { + source: '', + id: '', + }; + protected mathJaxFallback: MathJaxConfig = { + source: '', + id: '', + }; + + constructor() { + super(); + + this.isReady$ = new BehaviorSubject(false); + this.isReady$.next(true); + } + + protected async registerMathJaxAsync(config: MathJaxConfig): Promise { + return Promise.resolve(); + } + + ready(): Observable { + return this.isReady$; + } + + render(element: HTMLElement) { + return; + } +} diff --git a/src/app/core/submission/resolver/submission-object.resolver.ts b/src/app/core/submission/resolver/submission-object.resolver.ts index bdc44822c2..4ddd9dea93 100644 --- a/src/app/core/submission/resolver/submission-object.resolver.ts +++ b/src/app/core/submission/resolver/submission-object.resolver.ts @@ -1,46 +1,37 @@ -import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, - Resolve, 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 implements Resolve> { - 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/dso-shared/dso-edit-metadata/dso-edit-metadata.component.spec.ts b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.spec.ts index c3d82d0795..32822f1832 100644 --- a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.spec.ts +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.spec.ts @@ -38,9 +38,9 @@ const REINSTATE_BTN = 'reinstate'; const SAVE_BTN = 'save'; const DISCARD_BTN = 'discard'; -const mockDataServiceMap: any = { - [ITEM.value]: () => import('../../shared/testing/test-data-service.mock').then(m => m.TestDataService), -}; +const mockDataServiceMap: any = new Map([ + [ITEM.value, () => import('../../shared/testing/test-data-service.mock').then(m => m.TestDataService)], +]); describe('DsoEditMetadataComponent', () => { let component: DsoEditMetadataComponent; diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.ts b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.ts index 6365f1ea99..cded29230e 100644 --- a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.ts +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.ts @@ -7,7 +7,6 @@ import { ChangeDetectorRef, Component, Inject, - InjectionToken, Injector, Input, OnDestroy, @@ -42,7 +41,7 @@ import { import { ArrayMoveChangeAnalyzer } from '../../core/data/array-move-change-analyzer.service'; import { RemoteData } from '../../core/data/remote-data'; import { UpdateDataService } from '../../core/data/update-data.service'; -import { lazyService } from '../../core/lazy-service'; +import { lazyDataService } from '../../core/lazy-data-service'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { getFirstCompletedRemoteData } from '../../core/shared/operators'; import { ResourceType } from '../../core/shared/resource-type'; @@ -152,7 +151,7 @@ export class DsoEditMetadataComponent implements OnInit, OnDestroy { protected parentInjector: Injector, protected arrayMoveChangeAnalyser: ArrayMoveChangeAnalyzer, protected cdr: ChangeDetectorRef, - @Inject(APP_DATA_SERVICES_MAP) private dataServiceMap: InjectionToken) { + @Inject(APP_DATA_SERVICES_MAP) private dataServiceMap: LazyDataServicesMap) { } /** @@ -186,7 +185,7 @@ export class DsoEditMetadataComponent implements OnInit, OnDestroy { */ retrieveDataService(): Observable> { if (hasNoValue(this.updateDataService)) { - const lazyProvider$: Observable> = lazyService(this.dataServiceMap[this.dsoType], this.parentInjector); + const lazyProvider$: Observable> = lazyDataService(this.dataServiceMap, this.dsoType, this.parentInjector); return lazyProvider$; } else { return EMPTY; diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-suggestions/org-unit-input-suggestions.component.scss b/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-suggestions/org-unit-input-suggestions.component.scss index 20b48c805b..26b765b704 100644 --- a/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-suggestions/org-unit-input-suggestions.component.scss +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-suggestions/org-unit-input-suggestions.component.scss @@ -3,10 +3,9 @@ form { &:before { pointer-events: none; // prevent the icon from ‘catching‘ the click position: absolute; - font-weight: 900; - font-family: "Font Awesome 5 Free"; + font: var(--fa-font-solid); content: "\f0d7"; - top: 7px; + top: 10px; right: 0; height: 20px; width: 20px; 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 b041215c9c..b3c9ab1d9b 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/entity-groups/research-entities/submission/item-list-elements/person/person-suggestions/person-input-suggestions.component.scss b/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-suggestions/person-input-suggestions.component.scss index 86233c473a..d9a9544bfb 100644 --- a/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-suggestions/person-input-suggestions.component.scss +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-suggestions/person-input-suggestions.component.scss @@ -2,10 +2,9 @@ form { &:before { pointer-events: none; // prevent the icon from ‘catching‘ the click position: absolute; - font-weight: 900; - font-family: "Font Awesome 5 Free"; + font: var(--fa-font-solid); content: "\f0d7"; - top: 7px; + top: 10px; right: 0; height: 20px; width: 20px; diff --git a/src/app/footer/footer.component.html b/src/app/footer/footer.component.html index b937b2ce60..35487f91ae 100644 --- a/src/app/footer/footer.component.html +++ b/src/app/footer/footer.component.html @@ -82,10 +82,10 @@ - diff --git a/src/app/footer/footer.component.spec.ts b/src/app/footer/footer.component.spec.ts index 8608eeaf5e..527e570364 100644 --- a/src/app/footer/footer.component.spec.ts +++ b/src/app/footer/footer.component.spec.ts @@ -1,9 +1,3 @@ -// ... test imports -import { CommonModule } from '@angular/common'; -import { - CUSTOM_ELEMENTS_SCHEMA, - DebugElement, -} from '@angular/core'; import { ComponentFixture, fakeAsync, @@ -13,28 +7,19 @@ import { } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { ActivatedRoute } from '@angular/router'; -import { StoreModule } from '@ngrx/store'; -import { - TranslateLoader, - TranslateModule, -} from '@ngx-translate/core'; +import { TranslateModule } from '@ngx-translate/core'; import { of } from 'rxjs'; -import { environment } from 'src/environments/environment'; -import { storeModuleConfig } from '../app.reducer'; +import { APP_CONFIG } from '../../config/app-config.interface'; +import { environment } from '../../environments/environment.test'; import { NotifyInfoService } from '../core/coar-notify/notify-info/notify-info.service'; import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service'; -import { TranslateLoaderMock } from '../shared/mocks/translate-loader.mock'; import { ActivatedRouteStub } from '../shared/testing/active-router.stub'; import { AuthorizationDataServiceStub } from '../shared/testing/authorization-service.stub'; -// Load the implementations that should be tested import { FooterComponent } from './footer.component'; let comp: FooterComponent; -let compAny: any; let fixture: ComponentFixture; -let de: DebugElement; -let el: HTMLElement; let notifyInfoService = { isCoarConfigEnabled: () => of(true), @@ -43,19 +28,16 @@ let notifyInfoService = { describe('Footer component', () => { beforeEach(waitForAsync(() => { return TestBed.configureTestingModule({ - imports: [CommonModule, StoreModule.forRoot({}, storeModuleConfig), TranslateModule.forRoot({ - loader: { - provide: TranslateLoader, - useClass: TranslateLoaderMock, - }, - }), FooterComponent], + imports: [ + TranslateModule.forRoot(), + ], providers: [ FooterComponent, { provide: AuthorizationDataService, useClass: AuthorizationDataServiceStub }, { provide: NotifyInfoService, useValue: notifyInfoService }, { provide: ActivatedRoute, useValue: new ActivatedRouteStub() }, + { provide: APP_CONFIG, useValue: environment }, ], - schemas: [CUSTOM_ELEMENTS_SCHEMA], }); })); @@ -63,10 +45,6 @@ describe('Footer component', () => { beforeEach(() => { fixture = TestBed.createComponent(FooterComponent); comp = fixture.componentInstance; - compAny = comp as any; - // query for the title

by CSS element selector - de = fixture.debugElement.query(By.css('p')); - el = de.nativeElement; }); it('should create footer', inject([FooterComponent], (app: FooterComponent) => { @@ -76,23 +54,25 @@ describe('Footer component', () => { it('should set showPrivacyPolicy to the value of environment.info.enablePrivacyStatement', () => { + comp.ngOnInit(); expect(comp.showPrivacyPolicy).toBe(environment.info.enablePrivacyStatement); }); it('should set showEndUserAgreement to the value of environment.info.enableEndUserAgreement', () => { + comp.ngOnInit(); expect(comp.showEndUserAgreement).toBe(environment.info.enableEndUserAgreement); }); describe('showCookieSettings', () => { it('should call cookies.showSettings() if cookies is defined', () => { const cookies = jasmine.createSpyObj('cookies', ['showSettings']); - compAny.cookies = cookies; + comp.cookies = cookies; comp.showCookieSettings(); expect(cookies.showSettings).toHaveBeenCalled(); }); it('should not call cookies.showSettings() if cookies is undefined', () => { - compAny.cookies = undefined; + comp.cookies = undefined; expect(() => comp.showCookieSettings()).not.toThrow(); }); @@ -107,9 +87,7 @@ describe('Footer component', () => { fixture.detectChanges(); }); - it('should set coarLdnEnabled based on notifyInfoService', () => { - expect(comp.coarLdnEnabled).toBeTruthy(); - // Check if COAR Notify section is rendered + it('should render COAR notify support link', () => { const notifySection = fixture.debugElement.query(By.css('.notify-enabled')); expect(notifySection).toBeTruthy(); }); diff --git a/src/app/footer/footer.component.ts b/src/app/footer/footer.component.ts index 3043cf3284..f9fdde7834 100644 --- a/src/app/footer/footer.component.ts +++ b/src/app/footer/footer.component.ts @@ -5,13 +5,21 @@ import { } from '@angular/common'; import { Component, + Inject, + OnInit, Optional, } from '@angular/core'; import { RouterLink } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; -import { Observable } from 'rxjs'; +import { + Observable, + of as observableOf, +} from 'rxjs'; -import { environment } from '../../environments/environment'; +import { + APP_CONFIG, + AppConfig, +} from '../../config/app-config.interface'; import { NotifyInfoService } from '../core/coar-notify/notify-info/notify-info.service'; import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service'; import { FeatureID } from '../core/data/feature-authorization/feature-id'; @@ -25,27 +33,31 @@ import { hasValue } from '../shared/empty.util'; standalone: true, imports: [NgIf, RouterLink, AsyncPipe, DatePipe, TranslateModule], }) -export class FooterComponent { +export class FooterComponent implements OnInit { dateObj: number = Date.now(); /** * A boolean representing if to show or not the top footer container */ showTopFooter = false; - showPrivacyPolicy = environment.info.enablePrivacyStatement; - showEndUserAgreement = environment.info.enableEndUserAgreement; + showPrivacyPolicy: boolean; + showEndUserAgreement: boolean; showSendFeedback$: Observable; - coarLdnEnabled: boolean; + coarLdnEnabled$: Observable; constructor( - @Optional() private cookies: KlaroService, - private authorizationService: AuthorizationDataService, - private notifyInfoService: NotifyInfoService, + @Optional() public cookies: KlaroService, + protected authorizationService: AuthorizationDataService, + protected notifyInfoService: NotifyInfoService, + @Inject(APP_CONFIG) protected appConfig: AppConfig, ) { + } + + ngOnInit(): void { + this.showPrivacyPolicy = this.appConfig.info.enablePrivacyStatement; + this.showEndUserAgreement = this.appConfig.info.enableEndUserAgreement; + this.coarLdnEnabled$ = this.appConfig.info.enableCOARNotifySupport ? this.notifyInfoService.isCoarConfigEnabled() : observableOf(false); this.showSendFeedback$ = this.authorizationService.isAuthorized(FeatureID.CanSendFeedback); - this.notifyInfoService.isCoarConfigEnabled().subscribe(coarLdnEnabled => { - this.coarLdnEnabled = coarLdnEnabled; - }); } showCookieSettings() { 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-coar/home-coar.component.ts b/src/app/home-page/home-coar/home-coar.component.ts new file mode 100644 index 0000000000..f8efa29d07 --- /dev/null +++ b/src/app/home-page/home-coar/home-coar.component.ts @@ -0,0 +1,92 @@ +import { isPlatformServer } from '@angular/common'; +import { + Component, + Inject, + OnDestroy, + OnInit, + PLATFORM_ID, +} from '@angular/core'; +import { + of as observableOf, + Subscription, +} from 'rxjs'; +import { switchMap } from 'rxjs/operators'; + +import { NotifyInfoService } from '../../core/coar-notify/notify-info/notify-info.service'; +import { + LinkDefinition, + LinkHeadService, +} from '../../core/services/link-head.service'; +import { ServerResponseService } from '../../core/services/server-response.service'; +import { isNotEmpty } from '../../shared/empty.util'; + +@Component({ + selector: 'ds-home-coar', + template: '', + standalone: true, +}) +export class HomeCoarComponent implements OnInit, OnDestroy { + + /** + * An array of LinkDefinition objects representing inbox links for the home page. + */ + inboxLinks: LinkDefinition[] = []; + + subs: Subscription[] = []; + + constructor( + protected linkHeadService: LinkHeadService, + protected notifyInfoService: NotifyInfoService, + protected responseService: ServerResponseService, + @Inject(PLATFORM_ID) protected platformId: string, + ) { + } + + ngOnInit(): void { + // Get COAR REST API URLs from REST configuration + // only if COAR configuration is enabled + this.subs.push(this.notifyInfoService.isCoarConfigEnabled().pipe( + switchMap((coarLdnEnabled: boolean) => coarLdnEnabled ? this.notifyInfoService.getCoarLdnLocalInboxUrls() : observableOf([])), + ).subscribe((coarRestApiUrls: string[]) => { + if (coarRestApiUrls.length > 0) { + this.initPageLinks(coarRestApiUrls); + } + })); + } + + /** + * It removes the inbox links from the head of the html. + */ + ngOnDestroy(): void { + this.subs.forEach((sub: Subscription) => sub.unsubscribe()); + this.inboxLinks.forEach((link: LinkDefinition) => { + this.linkHeadService.removeTag(`href='${link.href}'`); + }); + } + + /** + * Initializes page links for COAR REST API URLs. + * @param coarRestApiUrls An array of COAR REST API URLs. + */ + private initPageLinks(coarRestApiUrls: string[]): void { + const rel = this.notifyInfoService.getInboxRelationLink(); + let links = ''; + coarRestApiUrls.forEach((coarRestApiUrl: string) => { + // Add link to head + const tag: LinkDefinition = { + href: coarRestApiUrl, + rel: rel, + }; + this.inboxLinks.push(tag); + this.linkHeadService.addTag(tag); + + links = links + (isNotEmpty(links) ? ', ' : '') + `<${coarRestApiUrl}> ; rel="${rel}"`; + }); + + if (isPlatformServer(this.platformId)) { + // Add link to response header + this.responseService.setHeader('Link', links); + } + } + +} 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.component.default.scss b/src/app/home-page/home-page.component.default.scss deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/app/home-page/home-page.component.html b/src/app/home-page/home-page.component.html index c1d4b8a98c..f0760f20aa 100644 --- a/src/app/home-page/home-page.component.html +++ b/src/app/home-page/home-page.component.html @@ -1,19 +1,29 @@ + -
-
-
- -
-
+
+ +
+ + [searchPlaceholder]="'home.search-form.placeholder' | translate"> +
-
+
+ + +
+ + +
+
diff --git a/src/app/home-page/home-page.component.scss b/src/app/home-page/home-page.component.scss index 29a5e65b4c..653de42b44 100644 --- a/src/app/home-page/home-page.component.scss +++ b/src/app/home-page/home-page.component.scss @@ -1,2 +1,5 @@ -:host { -} \ No newline at end of file +:host ::ng-deep { + .container-fluid .container { + padding: 0; + } +} diff --git a/src/app/home-page/home-page.component.ts b/src/app/home-page/home-page.component.ts index afc71ae381..4fa0e89e8e 100644 --- a/src/app/home-page/home-page.component.ts +++ b/src/app/home-page/home-page.component.ts @@ -1,44 +1,32 @@ import { AsyncPipe, - isPlatformServer, NgClass, NgIf, } from '@angular/common'; import { Component, Inject, - OnDestroy, OnInit, - PLATFORM_ID, } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; -import { - EMPTY, - Observable, -} from 'rxjs'; -import { - map, - switchMap, -} from 'rxjs/operators'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; import { APP_CONFIG, AppConfig, } from 'src/config/app-config.interface'; -import { environment } from '../../environments/environment'; -import { NotifyInfoService } from '../core/coar-notify/notify-info/notify-info.service'; -import { - LinkDefinition, - LinkHeadService, -} from '../core/services/link-head.service'; -import { ServerResponseService } from '../core/services/server-response.service'; import { Site } from '../core/shared/site.model'; import { SuggestionsPopupComponent } from '../notifications/suggestions-popup/suggestions-popup.component'; import { ConfigurationSearchPageComponent } from '../search-page/configuration-search-page.component'; -import { isNotEmpty } from '../shared/empty.util'; +import { ThemedConfigurationSearchPageComponent } from '../search-page/themed-configuration-search-page.component'; +import { HostWindowService } from '../shared/host-window.service'; import { ThemedSearchFormComponent } from '../shared/search-form/themed-search-form.component'; +import { PageWithSidebarComponent } from '../shared/sidebar/page-with-sidebar.component'; +import { SidebarService } from '../shared/sidebar/sidebar.service'; import { ViewTrackerComponent } from '../statistics/angulartics/dspace/view-tracker.component'; +import { HomeCoarComponent } from './home-coar/home-coar.component'; import { ThemedHomeNewsComponent } from './home-news/themed-home-news.component'; import { RecentItemListComponent } from './recent-item-list/recent-item-list.component'; import { ThemedTopLevelCommunityListComponent } from './top-level-community-list/themed-top-level-community-list.component'; @@ -48,80 +36,30 @@ import { ThemedTopLevelCommunityListComponent } from './top-level-community-list styleUrls: ['./home-page.component.scss'], templateUrl: './home-page.component.html', standalone: true, - imports: [ThemedHomeNewsComponent, NgIf, ViewTrackerComponent, ThemedSearchFormComponent, ThemedTopLevelCommunityListComponent, RecentItemListComponent, AsyncPipe, TranslateModule, NgClass, ConfigurationSearchPageComponent, SuggestionsPopupComponent], + imports: [ThemedHomeNewsComponent, NgIf, ViewTrackerComponent, ThemedSearchFormComponent, ThemedTopLevelCommunityListComponent, RecentItemListComponent, AsyncPipe, TranslateModule, NgClass, ConfigurationSearchPageComponent, SuggestionsPopupComponent, ThemedConfigurationSearchPageComponent, PageWithSidebarComponent, HomeCoarComponent], }) -export class HomePageComponent implements OnInit, OnDestroy { +export class HomePageComponent implements OnInit { site$: Observable; + isXsOrSm$: Observable; recentSubmissionspageSize: number; - /** - * An array of LinkDefinition objects representing inbox links for the home page. - */ - inboxLinks: LinkDefinition[] = []; + showDiscoverFilters: boolean; constructor( @Inject(APP_CONFIG) protected appConfig: AppConfig, - private route: ActivatedRoute, - private responseService: ServerResponseService, - private notifyInfoService: NotifyInfoService, - protected linkHeadService: LinkHeadService, - @Inject(PLATFORM_ID) private platformId: string, + protected route: ActivatedRoute, + protected sidebarService: SidebarService, + protected windowService: HostWindowService, ) { - this.recentSubmissionspageSize = environment.homePage.recentSubmissions.pageSize; - // Get COAR REST API URLs from REST configuration - // only if COAR configuration is enabled - this.notifyInfoService.isCoarConfigEnabled().pipe( - switchMap((coarLdnEnabled: boolean) => coarLdnEnabled ? this.notifyInfoService.getCoarLdnLocalInboxUrls() : EMPTY, /*{ - if (coarLdnEnabled) { - return this.notifyInfoService.getCoarLdnLocalInboxUrls(); - } else { - return of([]); - } - }*/), - ).subscribe((coarRestApiUrls: string[]) => { - if (coarRestApiUrls?.length > 0) { - this.initPageLinks(coarRestApiUrls); - } - }); + this.recentSubmissionspageSize = this.appConfig.homePage.recentSubmissions.pageSize; + this.showDiscoverFilters = this.appConfig.homePage.showDiscoverFilters; } ngOnInit(): void { + this.isXsOrSm$ = this.windowService.isXsOrSm(); this.site$ = this.route.data.pipe( map((data) => data.site as Site), ); } - /** - * Initializes page links for COAR REST API URLs. - * @param coarRestApiUrls An array of COAR REST API URLs. - */ - private initPageLinks(coarRestApiUrls: string[]): void { - const rel = this.notifyInfoService.getInboxRelationLink(); - let links = ''; - coarRestApiUrls.forEach((coarRestApiUrl: string) => { - // Add link to head - const tag: LinkDefinition = { - href: coarRestApiUrl, - rel: rel, - }; - this.inboxLinks.push(tag); - this.linkHeadService.addTag(tag); - - links = links + (isNotEmpty(links) ? ', ' : '') + `<${coarRestApiUrl}> ; rel="${rel}"`; - }); - - if (isPlatformServer(this.platformId)) { - // Add link to response header - this.responseService.setHeader('Link', links); - } - } - - /** - * It removes the inbox links from the head of the html. - */ - ngOnDestroy(): void { - this.inboxLinks.forEach((link: LinkDefinition) => { - this.linkHeadService.removeTag(`href='${link.href}'`); - }); - } } diff --git a/src/app/home-page/home-page.resolver.ts b/src/app/home-page/home-page.resolver.ts index 45c6402446..76f27edbf1 100644 --- a/src/app/home-page/home-page.resolver.ts +++ b/src/app/home-page/home-page.resolver.ts @@ -1,7 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, - Resolve, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; import { Observable } from 'rxjs'; @@ -10,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 implements Resolve { - 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..8d70ac740f 100644 --- a/src/app/info/info-routes.ts +++ b/src/app/info/info-routes.ts @@ -1,34 +1,55 @@ +import { + Route, + Routes, +} from '@angular/router'; + 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 { notifyInfoGuard } from '../core/coar-notify/notify-info/notify-info.guard'; +import { feedbackGuard } from '../core/feedback/feedback.guard'; +import { hasValue } from '../shared/empty.util'; import { ThemedEndUserAgreementComponent } from './end-user-agreement/themed-end-user-agreement.component'; import { ThemedFeedbackComponent } from './feedback/themed-feedback.component'; import { + COAR_NOTIFY_SUPPORT, END_USER_AGREEMENT_PATH, FEEDBACK_PATH, PRIVACY_PATH, } from './info-routing-paths'; +import { NotifyInfoComponent } from './notify-info/notify-info.component'; import { ThemedPrivacyComponent } from './privacy/themed-privacy.component'; -export const ROUTES = [ +export const ROUTES: 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, -]; + environment.info.enableCOARNotifySupport ? { + path: COAR_NOTIFY_SUPPORT, + component: NotifyInfoComponent, + canActivate: [notifyInfoGuard], + resolve: { + breadcrumb: i18nBreadcrumbResolver, + }, + data: { + title: 'info.coar-notify-support.title', + breadcrumbKey: 'info.coar-notify-support', + }, + } : undefined, +].filter((route: Route) => hasValue(route)); diff --git a/src/app/info/info-routing-paths.ts b/src/app/info/info-routing-paths.ts index a7fba45a6c..cd42dd9c1d 100644 --- a/src/app/info/info-routing-paths.ts +++ b/src/app/info/info-routing-paths.ts @@ -3,6 +3,7 @@ import { getInfoModulePath } from '../app-routing-paths'; export const END_USER_AGREEMENT_PATH = 'end-user-agreement'; export const PRIVACY_PATH = 'privacy'; export const FEEDBACK_PATH = 'feedback'; +export const COAR_NOTIFY_SUPPORT = 'coar-notify-support'; export function getEndUserAgreementPath() { return getSubPath(END_USER_AGREEMENT_PATH); @@ -16,6 +17,10 @@ export function getFeedbackPath() { return getSubPath(FEEDBACK_PATH); } +export function getCOARNotifySupportPath(): string { + return getSubPath(COAR_NOTIFY_SUPPORT); +} + function getSubPath(path: string) { return `${getInfoModulePath()}/${path}`; } diff --git a/src/app/core/coar-notify/notify-info/notify-info.component.html b/src/app/info/notify-info/notify-info.component.html similarity index 100% rename from src/app/core/coar-notify/notify-info/notify-info.component.html rename to src/app/info/notify-info/notify-info.component.html diff --git a/src/app/core/coar-notify/notify-info/notify-info.component.scss b/src/app/info/notify-info/notify-info.component.scss similarity index 100% rename from src/app/core/coar-notify/notify-info/notify-info.component.scss rename to src/app/info/notify-info/notify-info.component.scss diff --git a/src/app/core/coar-notify/notify-info/notify-info.component.spec.ts b/src/app/info/notify-info/notify-info.component.spec.ts similarity index 87% rename from src/app/core/coar-notify/notify-info/notify-info.component.spec.ts rename to src/app/info/notify-info/notify-info.component.spec.ts index 337a874866..010227b326 100644 --- a/src/app/core/coar-notify/notify-info/notify-info.component.spec.ts +++ b/src/app/info/notify-info/notify-info.component.spec.ts @@ -6,9 +6,9 @@ import { ActivatedRoute } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; import { of } from 'rxjs'; -import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub'; +import { NotifyInfoService } from '../../core/coar-notify/notify-info/notify-info.service'; +import { ActivatedRouteStub } from '../../shared/testing/active-router.stub'; import { NotifyInfoComponent } from './notify-info.component'; -import { NotifyInfoService } from './notify-info.service'; describe('NotifyInfoComponent', () => { let component: NotifyInfoComponent; diff --git a/src/app/core/coar-notify/notify-info/notify-info.component.ts b/src/app/info/notify-info/notify-info.component.ts similarity index 93% rename from src/app/core/coar-notify/notify-info/notify-info.component.ts rename to src/app/info/notify-info/notify-info.component.ts index 9822a07f9b..38bbf44595 100644 --- a/src/app/core/coar-notify/notify-info/notify-info.component.ts +++ b/src/app/info/notify-info/notify-info.component.ts @@ -11,7 +11,7 @@ import { of, } from 'rxjs'; -import { NotifyInfoService } from './notify-info.service'; +import { NotifyInfoService } from '../../core/coar-notify/notify-info/notify-info.service'; @Component({ selector: 'ds-notify-info', diff --git a/src/app/init.service.ts b/src/app/init.service.ts index afa547e678..47b2d8b02c 100644 --- a/src/app/init.service.ts +++ b/src/app/init.service.ts @@ -8,13 +8,11 @@ import { APP_INITIALIZER, Inject, + makeStateKey, Provider, + TransferState, Type, } from '@angular/core'; -import { - makeStateKey, - TransferState, -} from '@angular/platform-browser'; import { DYNAMIC_FORM_CONTROL_MAP_FN } from '@ng-dynamic-forms/core'; import { select, @@ -47,6 +45,7 @@ import { MenuService } from './shared/menu/menu.service'; import { ThemeService } from './shared/theme-support/theme.service'; import { Angulartics2DSpace } from './statistics/angulartics/dspace-provider'; + /** * Performs the initialization of the app. * 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 6d869e39b9..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,10 +12,9 @@ import { By } from '@angular/platform-browser'; import { ActivatedRoute, ActivatedRouteSnapshot, - CanActivate, + CanActivateFn, RouterModule, RouterStateSnapshot, - UrlTree, } from '@angular/router'; import { TranslateLoader, @@ -32,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 implements CanActivate { - 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 implements CanActivate { - 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: { @@ -64,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], }, ], @@ -78,13 +78,6 @@ describe('ItemPageComponent', () => { data: observableOf({ dso: createSuccessfulRemoteDataObject(new Item()) }), }; - const mockRouter = { - routerState: { - snapshot: undefined, - }, - events: observableOf(undefined), - }; - beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [ @@ -99,8 +92,6 @@ describe('ItemPageComponent', () => { ], providers: [ { provide: ActivatedRoute, useValue: mockRoute }, - AcceptAllGuard, - AcceptNoneGuard, ], schemas: [NO_ERRORS_SCHEMA], }).overrideComponent(EditItemPageComponent, { @@ -111,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 8a80d56344..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,10 +9,11 @@ import { Component, Injector, OnInit, + runInInjectionContext, } from '@angular/core'; import { ActivatedRoute, - CanActivate, + CanActivateFn, Route, Router, RouterLink, @@ -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,9 +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) => { - const guard: CanActivate = this.injector.get(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 0396f23b6d..fd6a8fe050 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/field-components/metadata-values/metadata-values.component.html b/src/app/item-page/field-components/metadata-values/metadata-values.component.html index 17eb4e42e3..44a3657fa5 100644 --- a/src/app/item-page/field-components/metadata-values/metadata-values.component.html +++ b/src/app/item-page/field-components/metadata-values/metadata-values.component.html @@ -12,7 +12,7 @@ - + diff --git a/src/app/item-page/field-components/metadata-values/metadata-values.component.ts b/src/app/item-page/field-components/metadata-values/metadata-values.component.ts index 8c319c6ad2..abc26c0d82 100644 --- a/src/app/item-page/field-components/metadata-values/metadata-values.component.ts +++ b/src/app/item-page/field-components/metadata-values/metadata-values.component.ts @@ -24,7 +24,7 @@ import { MetadataValue } from '../../../core/shared/metadata.models'; import { VALUE_LIST_BROWSE_DEFINITION } from '../../../core/shared/value-list-browse-definition.resource-type'; import { hasValue } from '../../../shared/empty.util'; import { MetadataFieldWrapperComponent } from '../../../shared/metadata-field-wrapper/metadata-field-wrapper.component'; -import { MarkdownPipe as MarkdownPipe_1 } from '../../../shared/utils/markdown.pipe'; +import { MarkdownDirective } from '../../../shared/utils/markdown.directive'; import { ImageField } from '../../simple/field-components/specific-field/image-field'; /** @@ -36,7 +36,7 @@ import { ImageField } from '../../simple/field-components/specific-field/image-f styleUrls: ['./metadata-values.component.scss'], templateUrl: './metadata-values.component.html', standalone: true, - imports: [MetadataFieldWrapperComponent, NgFor, NgTemplateOutlet, NgIf, RouterLink, AsyncPipe, MarkdownPipe_1, TranslateModule], + imports: [MetadataFieldWrapperComponent, NgFor, NgTemplateOutlet, NgIf, RouterLink, AsyncPipe, TranslateModule, MarkdownDirective], }) export class MetadataValuesComponent implements OnChanges { @@ -61,7 +61,7 @@ export class MetadataValuesComponent implements OnChanges { @Input() label: string; /** - * Whether the {@link MarkdownPipe} should be used to render these metadata values. + * Whether the {@link MarkdownDirective} should be used to render these metadata values. * This will only have effect if {@link MarkdownConfig#enabled} is true. * Mathjax will only be rendered if {@link MarkdownConfig#mathjax} is true. */ 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 cfd3df733e..343e0d1983 100644 --- a/src/app/item-page/item.resolver.ts +++ b/src/app/item-page/item.resolver.ts @@ -1,13 +1,13 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, - Resolve, - 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'; @@ -32,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 implements Resolve> { - 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/simple/field-components/specific-field/abstract/item-page-abstract-field.component.ts b/src/app/item-page/simple/field-components/specific-field/abstract/item-page-abstract-field.component.ts index 6a4ec85862..1453427db6 100644 --- a/src/app/item-page/simple/field-components/specific-field/abstract/item-page-abstract-field.component.ts +++ b/src/app/item-page/simple/field-components/specific-field/abstract/item-page-abstract-field.component.ts @@ -47,7 +47,7 @@ export class ItemPageAbstractFieldComponent extends ItemPageFieldComponent { label = 'item.page.abstract'; /** - * Use the {@link MarkdownPipe} to render dc.description.abstract values + * Use the {@link MarkdownDirective} to render dc.description.abstract values */ enableMarkdown = true; } diff --git a/src/app/item-page/simple/field-components/specific-field/generic/generic-item-page-field.component.ts b/src/app/item-page/simple/field-components/specific-field/generic/generic-item-page-field.component.ts index 52c07cca16..768608fd77 100644 --- a/src/app/item-page/simple/field-components/specific-field/generic/generic-item-page-field.component.ts +++ b/src/app/item-page/simple/field-components/specific-field/generic/generic-item-page-field.component.ts @@ -43,7 +43,7 @@ export class GenericItemPageFieldComponent extends ItemPageFieldComponent { @Input() label: string; /** - * Whether the {@link MarkdownPipe} should be used to render this metadata. + * Whether the {@link MarkdownDirective} should be used to render this metadata. */ @Input() enableMarkdown = false; diff --git a/src/app/item-page/simple/field-components/specific-field/item-page-field.component.spec.ts b/src/app/item-page/simple/field-components/specific-field/item-page-field.component.spec.ts index 41dac600b2..5815f18180 100644 --- a/src/app/item-page/simple/field-components/specific-field/item-page-field.component.spec.ts +++ b/src/app/item-page/simple/field-components/specific-field/item-page-field.component.spec.ts @@ -18,6 +18,7 @@ import { APP_CONFIG } from '../../../../../config/app-config.interface'; import { environment } from '../../../../../environments/environment'; import { BrowseDefinitionDataService } from '../../../../core/browse/browse-definition-data.service'; import { Item } from '../../../../core/shared/item.model'; +import { MathService } from '../../../../core/shared/math.service'; import { MetadataMap, MetadataValue, @@ -26,7 +27,7 @@ import { TranslateLoaderMock } from '../../../../shared/mocks/translate-loader.m import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; import { BrowseDefinitionDataServiceStub } from '../../../../shared/testing/browse-definition-data-service.stub'; import { createPaginatedList } from '../../../../shared/testing/utils.test'; -import { MarkdownPipe } from '../../../../shared/utils/markdown.pipe'; +import { MarkdownDirective } from '../../../../shared/utils/markdown.directive'; import { MetadataValuesComponent } from '../../../field-components/metadata-values/metadata-values.component'; import { ItemPageFieldComponent } from './item-page-field.component'; @@ -65,12 +66,13 @@ describe('ItemPageFieldComponent', () => { providers: [ { provide: APP_CONFIG, useValue: appConfig }, { provide: BrowseDefinitionDataService, useValue: BrowseDefinitionDataServiceStub }, + { provide: MathService, useValue: {} }, ], schemas: [NO_ERRORS_SCHEMA], }).overrideComponent(ItemPageFieldComponent, { set: { changeDetection: ChangeDetectionStrategy.Default }, }).compileComponents(); - markdownSpy = spyOn(MarkdownPipe.prototype, 'transform'); + markdownSpy = spyOn(MarkdownDirective.prototype, 'render'); fixture = TestBed.createComponent(ItemPageFieldComponent); comp = fixture.componentInstance; comp.item = mockItemWithMetadataFieldsAndValue(mockFields, mockValue); diff --git a/src/app/item-page/simple/field-components/specific-field/item-page-field.component.ts b/src/app/item-page/simple/field-components/specific-field/item-page-field.component.ts index 89d4be2459..39faae5ca4 100644 --- a/src/app/item-page/simple/field-components/specific-field/item-page-field.component.ts +++ b/src/app/item-page/simple/field-components/specific-field/item-page-field.component.ts @@ -39,7 +39,7 @@ export class ItemPageFieldComponent { @Input() item: Item; /** - * Whether the {@link MarkdownPipe} should be used to render this metadata. + * Whether the {@link MarkdownDirective} should be used to render this metadata. */ enableMarkdown = false; diff --git a/src/app/item-page/simple/related-entities/related-entities-search/related-entities-search.component.html b/src/app/item-page/simple/related-entities/related-entities-search/related-entities-search.component.html index 36340bebfa..939d502d99 100644 --- a/src/app/item-page/simple/related-entities/related-entities-search/related-entities-search.component.html +++ b/src/app/item-page/simple/related-entities/related-entities-search/related-entities-search.component.html @@ -1,7 +1,7 @@ - - + diff --git a/src/app/item-page/simple/related-entities/related-entities-search/related-entities-search.component.spec.ts b/src/app/item-page/simple/related-entities/related-entities-search/related-entities-search.component.spec.ts index 8f918c2916..6771665362 100644 --- a/src/app/item-page/simple/related-entities/related-entities-search/related-entities-search.component.spec.ts +++ b/src/app/item-page/simple/related-entities/related-entities-search/related-entities-search.component.spec.ts @@ -9,7 +9,7 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { TranslateModule } from '@ngx-translate/core'; import { Item } from '../../../../core/shared/item.model'; -import { ConfigurationSearchPageComponent } from '../../../../search-page/configuration-search-page.component'; +import { ThemedConfigurationSearchPageComponent } from '../../../../search-page/themed-configuration-search-page.component'; import { RelatedEntitiesSearchComponent } from './related-entities-search.component'; describe('RelatedEntitiesSearchComponent', () => { @@ -30,7 +30,7 @@ describe('RelatedEntitiesSearchComponent', () => { }) .overrideComponent(RelatedEntitiesSearchComponent, { remove: { - imports: [ConfigurationSearchPageComponent], + imports: [ThemedConfigurationSearchPageComponent], }, }) .compileComponents(); diff --git a/src/app/item-page/simple/related-entities/related-entities-search/related-entities-search.component.ts b/src/app/item-page/simple/related-entities/related-entities-search/related-entities-search.component.ts index 341d90f9c0..5f4966587b 100644 --- a/src/app/item-page/simple/related-entities/related-entities-search/related-entities-search.component.ts +++ b/src/app/item-page/simple/related-entities/related-entities-search/related-entities-search.component.ts @@ -5,7 +5,7 @@ import { } from '@angular/core'; import { Item } from '../../../../core/shared/item.model'; -import { ConfigurationSearchPageComponent } from '../../../../search-page/configuration-search-page.component'; +import { ThemedConfigurationSearchPageComponent } from '../../../../search-page/themed-configuration-search-page.component'; import { isNotEmpty } from '../../../../shared/empty.util'; import { getFilterByRelation } from '../../../../shared/utils/relation-query.utils'; @@ -13,7 +13,7 @@ import { getFilterByRelation } from '../../../../shared/utils/relation-query.uti selector: 'ds-related-entities-search', templateUrl: './related-entities-search.component.html', standalone: true, - imports: [ConfigurationSearchPageComponent], + imports: [ThemedConfigurationSearchPageComponent], }) /** * A component to show related items as search results. diff --git a/src/app/item-page/version-page/version.resolver.ts b/src/app/item-page/version-page/version.resolver.ts index 813420cf83..0364d4ac0d 100644 --- a/src/app/item-page/version-page/version.resolver.ts +++ b/src/app/item-page/version-page/version.resolver.ts @@ -1,13 +1,13 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, - Resolve, - 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'; @@ -27,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 implements Resolve> { - 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 e8d89fcf37..25813d28ff 100644 --- a/src/app/lookup-by-id/lookup-guard.ts +++ b/src/app/lookup-by-id/lookup-guard.ts @@ -1,7 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, - CanActivate, + CanActivateFn, RouterStateSnapshot, } from '@angular/router'; import { Observable } from 'rxjs'; @@ -17,44 +17,39 @@ interface LookupParams { id: string; } -@Injectable({ - providedIn: 'root', -}) -export class LookupGuard implements CanActivate { +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 7765ec0441..144cad4547 100644 --- a/src/app/menu.resolver.ts +++ b/src/app/menu-resolver.service.ts @@ -1,7 +1,6 @@ import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, - Resolve, RouterStateSnapshot, } from '@angular/router'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; @@ -55,7 +54,7 @@ import { MenuState } from './shared/menu/menu-state.model'; @Injectable({ providedIn: 'root', }) -export class MenuResolver implements Resolve { +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 f373d70938..c174c9594e 100644 --- a/src/app/my-dspace-page/my-dspace.guard.ts +++ b/src/app/my-dspace-page/my-dspace.guard.ts @@ -1,7 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, - CanActivate, + CanActivateFn, NavigationExtras, Router, RouterStateSnapshot, @@ -19,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 implements CanActivate { +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/navbar/expandable-navbar-section/expandable-navbar-section.component.spec.ts b/src/app/navbar/expandable-navbar-section/expandable-navbar-section.component.spec.ts index 6d0d72f0f5..d03c8d89eb 100644 --- a/src/app/navbar/expandable-navbar-section/expandable-navbar-section.component.spec.ts +++ b/src/app/navbar/expandable-navbar-section/expandable-navbar-section.component.spec.ts @@ -29,12 +29,7 @@ describe('ExpandableNavbarSectionComponent', () => { { provide: MenuService, useValue: menuService }, { provide: HostWindowService, useValue: new HostWindowServiceStub(800) }, ], - }).overrideComponent(ExpandableNavbarSectionComponent, { - set: { - entryComponents: [TestComponent], - }, - }) - .compileComponents(); + }).compileComponents(); })); beforeEach(() => { @@ -195,12 +190,7 @@ describe('ExpandableNavbarSectionComponent', () => { { provide: MenuService, useValue: menuService }, { provide: HostWindowService, useValue: new HostWindowServiceStub(300) }, ], - }).overrideComponent(ExpandableNavbarSectionComponent, { - set: { - entryComponents: [TestComponent], - }, - }) - .compileComponents(); + }).compileComponents(); })); beforeEach(() => { diff --git a/src/app/navbar/navbar-section/navbar-section.component.spec.ts b/src/app/navbar/navbar-section/navbar-section.component.spec.ts index f0765e1070..c98082cad5 100644 --- a/src/app/navbar/navbar-section/navbar-section.component.spec.ts +++ b/src/app/navbar/navbar-section/navbar-section.component.spec.ts @@ -26,12 +26,7 @@ describe('NavbarSectionComponent', () => { { provide: MenuService, useValue: menuService }, { provide: HostWindowService, useValue: new HostWindowServiceStub(800) }, ], - }).overrideComponent(NavbarSectionComponent, { - set: { - entryComponents: [TestComponent], - }, - }) - .compileComponents(); + }).compileComponents(); })); beforeEach(() => { diff --git a/src/app/notifications/suggestions.service.ts b/src/app/notifications/suggestions.service.ts index f456914e18..f128c87704 100644 --- a/src/app/notifications/suggestions.service.ts +++ b/src/app/notifications/suggestions.service.ts @@ -38,6 +38,7 @@ import { import { WorkspaceItem } from '../core/submission/models/workspaceitem.model'; import { WorkspaceitemDataService } from '../core/submission/workspaceitem-data.service'; import { + hasNoValue, hasValue, isNotEmpty, } from '../shared/empty.util'; @@ -165,6 +166,9 @@ export class SuggestionsService { * The EPerson id for which to retrieve suggestion targets */ public retrieveCurrentUserSuggestions(userUuid: string): Observable { + if (hasNoValue(userUuid)) { + return of([]); + } return this.researcherProfileService.findById(userUuid, true).pipe( getFirstCompletedRemoteData(), mergeMap((profile: RemoteData ) => { 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 ec12423d33..95596cfe2f 100644 --- a/src/app/process-page/process-breadcrumb.resolver.ts +++ b/src/app/process-page/process-breadcrumb.resolver.ts @@ -1,7 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, - Resolve, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; import { Observable } from 'rxjs'; @@ -16,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 implements Resolve> { - 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 2dc2dc19a2..3a074ce225 100644 --- a/src/app/process-page/process-page.resolver.ts +++ b/src/app/process-page/process-page.resolver.ts @@ -1,7 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, - Resolve, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; import { Observable } from 'rxjs'; @@ -17,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 implements Resolve> { - 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/notifications-suggestion-targets-page/notifications-suggestion-targets-page-resolver.service.ts b/src/app/quality-assurance-notifications-pages/notifications-suggestion-targets-page/notifications-suggestion-targets-page-resolver.service.ts index 058ede6736..e0982a6e60 100644 --- a/src/app/quality-assurance-notifications-pages/notifications-suggestion-targets-page/notifications-suggestion-targets-page-resolver.service.ts +++ b/src/app/quality-assurance-notifications-pages/notifications-suggestion-targets-page/notifications-suggestion-targets-page-resolver.service.ts @@ -1,7 +1,6 @@ import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, - Resolve, RouterStateSnapshot, } from '@angular/router'; @@ -20,7 +19,7 @@ export interface AdminNotificationsPublicationClaimPageParams { */ @Injectable({ providedIn: 'root' }) -export class AdminNotificationsPublicationClaimPageResolver implements Resolve { +export class AdminNotificationsPublicationClaimPageResolver { /** * Method for resolving the parameters in the current route. 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 28c1fd6b01..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,7 +1,6 @@ -import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, - Resolve, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; @@ -15,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 implements Resolve { - - /** - * 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 3e2ac6fa67..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,54 +1,53 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, - Resolve, + 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 implements Resolve> { - 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/quality-assurance-notifications-pages/quality-assurance-source-page-component/quality-assurance-source-page-resolver.service.ts b/src/app/quality-assurance-notifications-pages/quality-assurance-source-page-component/quality-assurance-source-page-resolver.service.ts index 50ea8780d5..f990a7c24c 100644 --- a/src/app/quality-assurance-notifications-pages/quality-assurance-source-page-component/quality-assurance-source-page-resolver.service.ts +++ b/src/app/quality-assurance-notifications-pages/quality-assurance-source-page-component/quality-assurance-source-page-resolver.service.ts @@ -1,7 +1,6 @@ import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, - Resolve, RouterStateSnapshot, } from '@angular/router'; @@ -18,7 +17,7 @@ export interface QualityAssuranceSourcePageParams { * This class represents a resolver that retrieve the route data before the route is activated. */ @Injectable({ providedIn: 'root' }) -export class QualityAssuranceSourcePageResolver implements Resolve { +export class QualityAssuranceSourcePageResolver { /** * Method for resolving the parameters in the current route. diff --git a/src/app/quality-assurance-notifications-pages/quality-assurance-topics-page/quality-assurance-topics-page-resolver.service.ts b/src/app/quality-assurance-notifications-pages/quality-assurance-topics-page/quality-assurance-topics-page-resolver.service.ts index c09c4ca7eb..8bc6261873 100644 --- a/src/app/quality-assurance-notifications-pages/quality-assurance-topics-page/quality-assurance-topics-page-resolver.service.ts +++ b/src/app/quality-assurance-notifications-pages/quality-assurance-topics-page/quality-assurance-topics-page-resolver.service.ts @@ -1,7 +1,6 @@ import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, - Resolve, RouterStateSnapshot, } from '@angular/router'; @@ -18,7 +17,7 @@ export interface QualityAssuranceTopicsPageParams { * This class represents a resolver that retrieve the route data before the route is activated. */ @Injectable({ providedIn: 'root' }) -export class QualityAssuranceTopicsPageResolver implements Resolve { +export class QualityAssuranceTopicsPageResolver { /** * Method for resolving the parameters in the current route. 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 d0b6fed0f2..b87f70bf4e 100644 --- a/src/app/register-email-form/registration.resolver.ts +++ b/src/app/register-email-form/registration.resolver.ts @@ -1,7 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, - Resolve, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; import { Observable } from 'rxjs'; @@ -11,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 implements Resolve> { - - 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 5eac132434..49614208e2 100644 --- a/src/app/register-page/registration.guard.ts +++ b/src/app/register-page/registration.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'; @@ -13,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 implements CanActivate { - 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 0720086301..b91e1b7d8c 100644 --- a/src/app/request-copy/request-copy.resolver.ts +++ b/src/app/request-copy/request-copy.resolver.ts @@ -1,7 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, - Resolve, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; import { Observable } from 'rxjs'; @@ -11,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 implements Resolve> { - - 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 28d68618b0..ec032bd04a 100644 --- a/src/app/search-page/configuration-search-page.guard.ts +++ b/src/app/search-page/configuration-search-page.guard.ts @@ -1,26 +1,22 @@ -import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, - CanActivate, + 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 implements CanActivate { - 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/search-page/themed-configuration-search-page.component.ts b/src/app/search-page/themed-configuration-search-page.component.ts index fc9497268c..0d39dca430 100644 --- a/src/app/search-page/themed-configuration-search-page.component.ts +++ b/src/app/search-page/themed-configuration-search-page.component.ts @@ -2,9 +2,12 @@ import { Component, Input, } from '@angular/core'; -import { Observable } from 'rxjs'; import { Context } from '../core/shared/context.model'; +import { ViewMode } from '../core/shared/view-mode.model'; +import { CollectionElementLinkType } from '../shared/object-collection/collection-element-link.type'; +import { SelectionConfig } from '../shared/search/search-results/search-results.component'; +import { SearchConfigurationOption } from '../shared/search/search-switch-configuration/search-configuration-option.model'; import { ThemedComponent } from '../shared/theme-support/themed.component'; import { ConfigurationSearchPageComponent } from './configuration-search-page.component'; @@ -13,53 +16,85 @@ import { ConfigurationSearchPageComponent } from './configuration-search-page.co */ @Component({ selector: 'ds-themed-configuration-search-page', - styleUrls: [], templateUrl: '../shared/theme-support/themed.component.html', standalone: true, }) export class ThemedConfigurationSearchPageComponent extends ThemedComponent { - /** - * The configuration to use for the search options - * If empty, the configuration will be determined by the route parameter called 'configuration' - */ + + @Input() configurationList: SearchConfigurationOption[] = []; + + @Input() context: Context; + @Input() configuration: string; - /** - * The actual query for the fixed filter. - * If empty, the query will be determined by the route parameter called 'filter' - */ @Input() fixedFilterQuery: string; - /** - * True when the search component should show results on the current page - */ + @Input() useCachedVersionIfAvailable: boolean; + @Input() inPlaceSearch: boolean; - /** - * Whether or not the search bar should be visible - */ + @Input() linkType: CollectionElementLinkType; + + @Input() paginationId: string; + @Input() searchEnabled: boolean; - /** - * The width of the sidebar (bootstrap columns) - */ - @Input() - sideBarWidth: number; + @Input() sideBarWidth: number; - /** - * The currently applied configuration (determines title of search) - */ - @Input() - configuration$: Observable; + @Input() searchFormPlaceholder: string; - /** - * The current context - */ - @Input() - context: Context; + @Input() selectable: boolean; - protected inAndOutputNames: (keyof ConfigurationSearchPageComponent & keyof this)[] = - ['context', 'configuration', 'fixedFilterQuery', 'inPlaceSearch', 'searchEnabled', 'sideBarWidth']; + @Input() selectionConfig: SelectionConfig; + + @Input() showCsvExport: boolean; + + @Input() showSidebar: boolean; + + @Input() showThumbnails: boolean; + + @Input() showViewModes: boolean; + + @Input() useUniquePageId: boolean; + + @Input() viewModeList: ViewMode[]; + + @Input() showScopeSelector: boolean; + + @Input() trackStatistics: boolean; + + @Input() query: string; + + @Input() scope: string; + + @Input() hideScopeInUrl: boolean; + + protected inAndOutputNames: (keyof ConfigurationSearchPageComponent & keyof this)[] = [ + 'configurationList', + 'context', + 'configuration', + 'fixedFilterQuery', + 'useCachedVersionIfAvailable', + 'inPlaceSearch', + 'linkType', + 'paginationId', + 'searchEnabled', + 'sideBarWidth', + 'searchFormPlaceholder', + 'selectable', + 'selectionConfig', + 'showCsvExport', + 'showSidebar', + 'showThumbnails', + 'showViewModes', + 'useUniquePageId', + 'viewModeList', + 'showScopeSelector', + 'trackStatistics', + 'query', + 'scope', + 'hideScopeInUrl', + ]; protected getComponentName(): string { return 'ConfigurationSearchPageComponent'; 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/comcol/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.html b/src/app/shared/comcol/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.html index ec470fbac5..fa9dd5da09 100644 --- a/src/app/shared/comcol/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.html +++ b/src/app/shared/comcol/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.html @@ -16,7 +16,7 @@
{{'comcol-role.edit.no-group' | translate}} -
V +
{{'comcol-role.edit.' + (comcolRole$ | async)?.name + '.anonymous-group' | translate}}
diff --git a/src/app/shared/comcol/comcol-page-browse-by/comcol-page-browse-by.component.ts b/src/app/shared/comcol/comcol-page-browse-by/comcol-page-browse-by.component.ts index c61be492cd..bcfb04a443 100644 --- a/src/app/shared/comcol/comcol-page-browse-by/comcol-page-browse-by.component.ts +++ b/src/app/shared/comcol/comcol-page-browse-by/comcol-page-browse-by.component.ts @@ -12,6 +12,7 @@ import { import { FormsModule } from '@angular/forms'; import { EventType, + NavigationEnd, Router, RouterLink, RouterLinkActive, @@ -129,7 +130,7 @@ export class ComcolPageBrowseByComponent implements OnDestroy, OnInit { this.router.events.pipe( startWith(this.router), filter((next: Router|Scroll) => (isNotEmpty((next as Router)?.url) || (next as Scroll)?.type === EventType.Scroll)), - map((next: Router|Scroll) => (next as Router)?.url || (next as Scroll).routerEvent.urlAfterRedirects), + map((next: Router|Scroll) => (next as Router)?.url || ((next as Scroll).routerEvent as NavigationEnd).urlAfterRedirects), distinctUntilChanged(), ), ]).subscribe(([navOptions, url]: [ComColPageNavOption[], string]) => { 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 a40cabf6b0..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,325 +1,21 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, - Resolve, + 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 implements Resolve<{ [key: string]: MenuSection[] }> { - - 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/dso-page/dso-edit-menu/dso-edit-expandable-menu-section/dso-edit-menu-expandable-section.component.spec.ts b/src/app/shared/dso-page/dso-edit-menu/dso-edit-expandable-menu-section/dso-edit-menu-expandable-section.component.spec.ts index 4c4e6d3446..a4e85f18f2 100644 --- a/src/app/shared/dso-page/dso-edit-menu/dso-edit-expandable-menu-section/dso-edit-menu-expandable-section.component.spec.ts +++ b/src/app/shared/dso-page/dso-edit-menu/dso-edit-expandable-menu-section/dso-edit-menu-expandable-section.component.spec.ts @@ -44,12 +44,7 @@ describe('DsoEditMenuExpandableSectionComponent', () => { { provide: CSSVariableService, useClass: CSSVariableServiceStub }, { provide: Router, useValue: new RouterStub() }, ], - }).overrideComponent(DsoEditMenuExpandableSectionComponent, { - set: { - entryComponents: [TestComponent], - }, - }) - .compileComponents(); + }).compileComponents(); })); beforeEach(() => { diff --git a/src/app/shared/eperson-group-list/eperson-group-list.component.spec.ts b/src/app/shared/eperson-group-list/eperson-group-list.component.spec.ts index e09fa32526..ca3837130d 100644 --- a/src/app/shared/eperson-group-list/eperson-group-list.component.spec.ts +++ b/src/app/shared/eperson-group-list/eperson-group-list.component.spec.ts @@ -17,7 +17,10 @@ import { cold } from 'jasmine-marbles'; import uniqueId from 'lodash/uniqueId'; import { of as observableOf } from 'rxjs'; -import { APP_DATA_SERVICES_MAP } from '../../../config/app-config.interface'; +import { + APP_DATA_SERVICES_MAP, + LazyDataServicesMap, +} from '../../../config/app-config.interface'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { buildPaginatedList } from '../../core/data/paginated-list.model'; import { RequestService } from '../../core/data/request.service'; @@ -41,10 +44,10 @@ import { SearchEvent } from './eperson-group-list-event-type'; import { EpersonSearchBoxComponent } from './eperson-search-box/eperson-search-box.component'; import { GroupSearchBoxComponent } from './group-search-box/group-search-box.component'; -const mockDataServiceMap: any = { - [EPERSON.value]: () => import('../../core/eperson/eperson-data.service').then(m => m.EPersonDataService), - [GROUP.value]: () => import('../../core/eperson/group-data.service').then(m => m.GroupDataService), -}; +const mockDataServiceMap: LazyDataServicesMap = new Map([ + [EPERSON.value, () => import('../../core/eperson/eperson-data.service').then(m => m.EPersonDataService)], + [GROUP.value, () => import('../../core/eperson/group-data.service').then(m => m.GroupDataService)], +]); describe('EpersonGroupListComponent test suite', () => { let comp: EpersonGroupListComponent; diff --git a/src/app/shared/eperson-group-list/eperson-group-list.component.ts b/src/app/shared/eperson-group-list/eperson-group-list.component.ts index 0a33767e34..663418d20c 100644 --- a/src/app/shared/eperson-group-list/eperson-group-list.component.ts +++ b/src/app/shared/eperson-group-list/eperson-group-list.component.ts @@ -7,7 +7,6 @@ import { Component, EventEmitter, Inject, - InjectionToken, Injector, Input, OnDestroy, @@ -35,7 +34,7 @@ import { EPersonDataService } from '../../core/eperson/eperson-data.service'; import { GroupDataService } from '../../core/eperson/group-data.service'; import { EPERSON } from '../../core/eperson/models/eperson.resource-type'; import { GROUP } from '../../core/eperson/models/group.resource-type'; -import { lazyService } from '../../core/lazy-service'; +import { lazyDataService } from '../../core/lazy-data-service'; import { PaginationService } from '../../core/pagination/pagination.service'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { getFirstCompletedRemoteData } from '../../core/shared/operators'; @@ -133,7 +132,7 @@ export class EpersonGroupListComponent implements OnInit, OnDestroy { constructor(public dsoNameService: DSONameService, private parentInjector: Injector, private paginationService: PaginationService, - @Inject(APP_DATA_SERVICES_MAP) private dataServiceMap: InjectionToken) { + @Inject(APP_DATA_SERVICES_MAP) private dataServiceMap: LazyDataServicesMap) { } /** @@ -141,7 +140,7 @@ export class EpersonGroupListComponent implements OnInit, OnDestroy { */ ngOnInit(): void { const resourceType: ResourceType = (this.isListOfEPerson) ? EPERSON : GROUP; - const lazyProvider$: Observable = lazyService(this.dataServiceMap[resourceType.value], this.parentInjector); + const lazyProvider$: Observable = lazyDataService(this.dataServiceMap, resourceType.value, this.parentInjector); lazyProvider$.subscribe((dataService: EPersonDataService | GroupDataService) => { this.dataService = dataService; console.log(dataService); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts index d718c6a5eb..d8be890c72 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts @@ -17,7 +17,6 @@ import { UntypedFormGroup, } from '@angular/forms'; import { By } from '@angular/platform-browser'; -import { BrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/testing'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { DYNAMIC_FORM_CONTROL_MAP_FN, @@ -213,13 +212,6 @@ describe('DsDynamicFormControlContainerComponent test suite', () => { testWSI.item = observableOf(createSuccessfulRemoteDataObject(testItem)); beforeEach(waitForAsync(() => { - TestBed.overrideModule(BrowserDynamicTestingModule, { - - set: { - entryComponents: [DynamicNGBootstrapInputComponent], - }, - }); - TestBed.configureTestingModule({ imports: [ 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.html b/src/app/shared/form/chips/chips.component.html index 24fdbbc8cf..927ca426ef 100644 --- a/src/app/shared/form/chips/chips.component.html +++ b/src/app/shared/form/chips/chips.component.html @@ -1,26 +1,25 @@
-