Merge remote-tracking branch 'upstream/main' into w2p-112970_added-missing-breadcrumbs_contribute-main

# Conflicts:
#	src/app/collection-page/collection-page-routes.ts
#	src/app/community-page/community-page-routes.ts
#	src/app/core/breadcrumbs/community-breadcrumb.resolver.ts
#	src/app/core/breadcrumbs/dso-breadcrumb.resolver.spec.ts
#	src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts
#	src/app/core/submission/resolver/submission-object.resolver.ts
#	src/app/workflowitems-edit-page/item-from-workflow.resolver.spec.ts
#	src/app/workflowitems-edit-page/item-from-workflow.resolver.ts
#	src/app/workflowitems-edit-page/workflowitems-edit-page-routes.ts
#	src/app/workspaceitems-edit-page/item-from-workspace.resolver.spec.ts
#	src/app/workspaceitems-edit-page/item-from-workspace.resolver.ts
#	src/app/workspaceitems-edit-page/workspaceitems-edit-page-routes.ts
This commit is contained in:
Alexandre Vryghem
2024-04-10 22:21:07 +02:00
230 changed files with 7780 additions and 7476 deletions

View File

@@ -38,7 +38,7 @@ jobs:
strategy: strategy:
# Create a matrix of Node versions to test against (in parallel) # Create a matrix of Node versions to test against (in parallel)
matrix: matrix:
node-version: [16.x, 18.x] node-version: [18.x, 20.x]
# Do NOT exit immediately if one matrix job fails # Do NOT exit immediately if one matrix job fails
fail-fast: false fail-fast: false
# These are the actual CI steps to perform per job # These are the actual CI steps to perform per job
@@ -74,7 +74,7 @@ jobs:
id: yarn-cache-dir-path id: yarn-cache-dir-path
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
- name: Cache Yarn dependencies - name: Cache Yarn dependencies
uses: actions/cache@v3 uses: actions/cache@v4
with: with:
# Cache entire Yarn cache directory (see previous step) # Cache entire Yarn cache directory (see previous step)
path: ${{ steps.yarn-cache-dir-path.outputs.dir }} path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
@@ -101,10 +101,10 @@ jobs:
# so that it can be shared with the 'codecov' job (see below) # 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 # 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 - name: Upload code coverage report to Artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
if: matrix.node-version == '18.x' if: matrix.node-version == '18.x'
with: with:
name: dspace-angular coverage report name: coverage-report-${{ matrix.node-version }}
path: 'coverage/dspace-angular/lcov.info' path: 'coverage/dspace-angular/lcov.info'
retention-days: 14 retention-days: 14
@@ -135,19 +135,19 @@ jobs:
# Cypress always creates a video of all e2e tests (whether they succeeded or failed) # Cypress always creates a video of all e2e tests (whether they succeeded or failed)
# Save those in an Artifact # Save those in an Artifact
- name: Upload e2e test videos to Artifacts - name: Upload e2e test videos to Artifacts
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
if: always() if: always()
with: with:
name: e2e-test-videos name: e2e-test-videos-${{ matrix.node-version }}
path: cypress/videos path: cypress/videos
# If e2e tests fail, Cypress creates a screenshot of what happened # If e2e tests fail, Cypress creates a screenshot of what happened
# Save those in an Artifact # Save those in an Artifact
- name: Upload e2e test failure screenshots to Artifacts - name: Upload e2e test failure screenshots to Artifacts
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
if: failure() if: failure()
with: with:
name: e2e-test-screenshots name: e2e-test-screenshots-${{ matrix.node-version }}
path: cypress/screenshots path: cypress/screenshots
- name: Stop app (in case it stays up after e2e tests) - name: Stop app (in case it stays up after e2e tests)
@@ -197,7 +197,7 @@ jobs:
# Download artifacts from previous 'tests' job # Download artifacts from previous 'tests' job
- name: Download coverage artifacts - name: Download coverage artifacts
uses: actions/download-artifact@v3 uses: actions/download-artifact@v4
# Now attempt upload to Codecov using its action. # 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. # 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 - name: Upload coverage to Codecov.io
uses: Wandalen/wretry.action@v1.3.0 uses: Wandalen/wretry.action@v1.3.0
with: with:
action: codecov/codecov-action@v3 action: codecov/codecov-action@v4
# Ensure codecov-action throws an error when it fails to upload # Ensure codecov-action throws an error when it fails to upload
# This allows us to auto-restart the action if an error is thrown # This allows us to auto-restart the action if an error is thrown
with: | with: |
fail_ci_if_error: true fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}
# Try re-running action 5 times max # Try re-running action 5 times max
attempt_limit: 5 attempt_limit: 5
# Run again in 30 seconds # Run again in 30 seconds

View File

@@ -28,7 +28,7 @@ jobs:
# Use the reusable-docker-build.yml script from DSpace/DSpace repo to build our Docker image # 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 uses: DSpace/DSpace/.github/workflows/reusable-docker-build.yml@main
with: with:
build_id: dspace-angular build_id: dspace-angular-dev
image_name: dspace/dspace-angular image_name: dspace/dspace-angular
dockerfile_path: ./Dockerfile dockerfile_path: ./Dockerfile
secrets: secrets:

View File

@@ -16,7 +16,7 @@ jobs:
# Only add to project board if issue is flagged as "needs triage" or has no labels # 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 # 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) == '') 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. # 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 # 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 # See: https://docs.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token#permissions-for-the-github_token

View File

@@ -21,4 +21,4 @@ jobs:
# Assign the PR to whomever created it. This is useful for visualizing assignments on project boards # Assign the PR to whomever created it. This is useful for visualizing assignments on project boards
# See https://github.com/toshimaru/auto-author-assign # See https://github.com/toshimaru/auto-author-assign
- name: Assign PR to creator - name: Assign PR to creator
uses: toshimaru/auto-author-assign@v2.0.1 uses: toshimaru/auto-author-assign@v2.1.0

View File

@@ -17,6 +17,13 @@ ui:
# Trust X-FORWARDED-* headers from proxies (default = true) # Trust X-FORWARDED-* headers from proxies (default = true)
useProxies: 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 # The REST API server settings
# NOTE: these settings define which (publicly available) REST API to use. They are usually # 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. # 'synced' with the 'dspace.server.url' setting in your backend's local.cfg.

View File

@@ -55,28 +55,28 @@
"ts-node": "10.2.1" "ts-node": "10.2.1"
}, },
"dependencies": { "dependencies": {
"@angular/animations": "^15.2.8", "@angular/animations": "^16.2.12",
"@angular/cdk": "^15.2.8", "@angular/cdk": "^16.2.12",
"@angular/common": "^15.2.8", "@angular/common": "^16.2.12",
"@angular/compiler": "^15.2.8", "@angular/compiler": "^16.2.12",
"@angular/core": "^15.2.8", "@angular/core": "^16.2.12",
"@angular/forms": "^15.2.8", "@angular/forms": "^16.2.12",
"@angular/localize": "15.2.8", "@angular/localize": "16.2.12",
"@angular/platform-browser": "^15.2.8", "@angular/platform-browser": "^16.2.12",
"@angular/platform-browser-dynamic": "^15.2.8", "@angular/platform-browser-dynamic": "^16.2.12",
"@angular/platform-server": "^15.2.8", "@angular/platform-server": "^16.2.12",
"@angular/router": "^15.2.8", "@angular/router": "^16.2.12",
"@babel/runtime": "7.21.0", "@babel/runtime": "7.21.0",
"@kolkov/ngx-gallery": "^2.0.1", "@kolkov/ngx-gallery": "^2.0.1",
"@material-ui/core": "^4.11.0", "@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.11.3", "@material-ui/icons": "^4.11.3",
"@ng-bootstrap/ng-bootstrap": "^11.0.0", "@ng-bootstrap/ng-bootstrap": "^11.0.0",
"@ng-dynamic-forms/core": "^15.0.0", "@ng-dynamic-forms/core": "^16.0.0",
"@ng-dynamic-forms/ui-ng-bootstrap": "^15.0.0", "@ng-dynamic-forms/ui-ng-bootstrap": "^16.0.0",
"@ngrx/effects": "^15.4.0", "@ngrx/effects": "^16.3.0",
"@ngrx/router-store": "^15.4.0", "@ngrx/router-store": "^16.3.0",
"@ngrx/store": "^15.4.0", "@ngrx/store": "^16.3.0",
"@nguniversal/express-engine": "^15.2.1", "@nguniversal/express-engine": "^16.2.0",
"@ngx-translate/core": "^14.0.0", "@ngx-translate/core": "^14.0.0",
"@nicky-lenaers/ngx-scroll-to": "^14.0.0", "@nicky-lenaers/ngx-scroll-to": "^14.0.0",
"@types/grecaptcha": "^3.0.4", "@types/grecaptcha": "^3.0.4",
@@ -94,7 +94,7 @@
"date-fns-tz": "^1.3.7", "date-fns-tz": "^1.3.7",
"deepmerge": "^4.3.1", "deepmerge": "^4.3.1",
"ejs": "^3.1.9", "ejs": "^3.1.9",
"express": "^4.18.2", "express": "^4.19.2",
"express-rate-limit": "^5.1.3", "express-rate-limit": "^5.1.3",
"fast-json-patch": "^3.1.1", "fast-json-patch": "^3.1.1",
"filesize": "^6.1.0", "filesize": "^6.1.0",
@@ -110,17 +110,15 @@
"lodash": "^4.17.21", "lodash": "^4.17.21",
"lru-cache": "^7.14.1", "lru-cache": "^7.14.1",
"markdown-it": "^13.0.1", "markdown-it": "^13.0.1",
"markdown-it-mathjax3": "^4.3.2",
"mirador": "^3.3.0", "mirador": "^3.3.0",
"mirador-dl-plugin": "^0.13.0", "mirador-dl-plugin": "^0.13.0",
"mirador-share-plugin": "^0.11.0", "mirador-share-plugin": "^0.11.0",
"morgan": "^1.10.0", "morgan": "^1.10.0",
"ng-mocks": "^14.10.0", "ng-mocks": "^14.10.0",
"ng2-file-upload": "1.4.0", "ng2-file-upload": "5.0.0",
"ng2-nouislider": "^2.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-pagination": "6.0.3",
"ngx-sortablejs": "^11.1.0",
"ngx-ui-switch": "^14.1.0", "ngx-ui-switch": "^14.1.0",
"nouislider": "^15.7.1", "nouislider": "^15.7.1",
"pem": "1.14.7", "pem": "1.14.7",
@@ -132,24 +130,24 @@
"sortablejs": "1.15.0", "sortablejs": "1.15.0",
"uuid": "^8.3.2", "uuid": "^8.3.2",
"webfontloader": "1.6.28", "webfontloader": "1.6.28",
"zone.js": "~0.11.5" "zone.js": "~0.13.3"
}, },
"devDependencies": { "devDependencies": {
"@angular-builders/custom-webpack": "~15.0.0", "@angular-builders/custom-webpack": "~16.0.0",
"@angular-devkit/build-angular": "^15.2.6", "@angular-devkit/build-angular": "^16.2.12",
"@angular-eslint/builder": "15.2.1", "@angular-eslint/builder": "16.3.1",
"@angular-eslint/eslint-plugin": "15.2.1", "@angular-eslint/eslint-plugin": "16.3.1",
"@angular-eslint/eslint-plugin-template": "15.2.1", "@angular-eslint/eslint-plugin-template": "16.3.1",
"@angular-eslint/schematics": "15.2.1", "@angular-eslint/schematics": "16.3.1",
"@angular-eslint/template-parser": "15.2.1", "@angular-eslint/template-parser": "16.3.1",
"@angular/cli": "^16.0.4", "@angular/cli": "^16.2.12",
"@angular/compiler-cli": "^15.2.8", "@angular/compiler-cli": "^16.2.12",
"@angular/language-service": "^15.2.8", "@angular/language-service": "^16.2.12",
"@cypress/schematic": "^1.5.0", "@cypress/schematic": "^1.5.0",
"@fortawesome/fontawesome-free": "^6.4.0", "@fortawesome/fontawesome-free": "^6.4.0",
"@ngrx/store-devtools": "^15.4.0", "@ngrx/store-devtools": "^16.3.0",
"@ngtools/webpack": "^15.2.6", "@ngtools/webpack": "^16.2.12",
"@nguniversal/builders": "^15.2.1", "@nguniversal/builders": "^16.2.0",
"@types/deep-freeze": "0.1.2", "@types/deep-freeze": "0.1.2",
"@types/ejs": "^3.1.2", "@types/ejs": "^3.1.2",
"@types/express": "^4.17.17", "@types/express": "^4.17.17",
@@ -186,7 +184,7 @@
"karma-jasmine": "~4.0.0", "karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "^1.5.0", "karma-jasmine-html-reporter": "^1.5.0",
"karma-mocha-reporter": "2.2.5", "karma-mocha-reporter": "2.2.5",
"ngx-mask": "^13.1.7", "ngx-mask": "14.2.4",
"nodemon": "^2.0.22", "nodemon": "^2.0.22",
"postcss": "^8.4", "postcss": "^8.4",
"postcss-apply": "0.12.0", "postcss-apply": "0.12.0",
@@ -202,7 +200,7 @@
"sass-loader": "^12.6.0", "sass-loader": "^12.6.0",
"sass-resources-loader": "^2.2.5", "sass-resources-loader": "^2.2.5",
"ts-node": "^8.10.2", "ts-node": "^8.10.2",
"typescript": "~4.8.4", "typescript": "~4.9.3",
"webpack": "5.76.1", "webpack": "5.76.1",
"webpack-bundle-analyzer": "^4.8.0", "webpack-bundle-analyzer": "^4.8.0",
"webpack-cli": "^4.2.0", "webpack-cli": "^4.2.0",

View File

@@ -48,7 +48,7 @@ import { hasNoValue, hasValue } from './src/app/shared/empty.util';
import { UIServerConfig } from './src/config/ui-server-config.interface'; 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 { buildAppConfig } from './src/config/config.server';
import { APP_CONFIG, AppConfig } from './src/config/app-config.interface'; 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) // Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine)
server.engine('html', (_, options, callback) => server.engine('html', (_, options, callback) =>
ngExpressEngine({ ngExpressEngine({
bootstrap: ServerAppModule, bootstrap,
inlineCriticalCss: environment.universal.inlineCriticalCss,
providers: [ providers: [
{ {
provide: REQUEST, provide: REQUEST,
@@ -142,10 +143,10 @@ export function app() {
}, },
{ {
provide: APP_CONFIG, provide: APP_CONFIG,
useValue: environment useValue: environment,
} },
] ],
})(_, (options as any), callback) })(_, (options as any), callback),
); );
server.engine('ejs', ejs.renderFile); server.engine('ejs', ejs.renderFile);
@@ -162,7 +163,7 @@ export function app() {
server.get('/robots.txt', (req, res) => { server.get('/robots.txt', (req, res) => {
res.setHeader('content-type', 'text/plain'); res.setHeader('content-type', 'text/plain');
res.render('assets/robots.txt.ejs', { 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({ router.use('/sitemap**', createProxyMiddleware({
target: `${environment.rest.baseUrl}/sitemaps`, target: `${environment.rest.baseUrl}/sitemaps`,
pathRewrite: path => path.replace(environment.ui.nameSpace, '/'), pathRewrite: path => path.replace(environment.ui.nameSpace, '/'),
changeOrigin: true changeOrigin: true,
})); }));
/** /**
@@ -186,7 +187,7 @@ export function app() {
router.use('/signposting**', createProxyMiddleware({ router.use('/signposting**', createProxyMiddleware({
target: `${environment.rest.baseUrl}`, target: `${environment.rest.baseUrl}`,
pathRewrite: path => path.replace(environment.ui.nameSpace, '/'), 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 RateLimit = require('express-rate-limit');
const limiter = new RateLimit({ const limiter = new RateLimit({
windowMs: (environment.ui as UIServerConfig).rateLimiter.windowMs, windowMs: (environment.ui as UIServerConfig).rateLimiter.windowMs,
max: (environment.ui as UIServerConfig).rateLimiter.max max: (environment.ui as UIServerConfig).rateLimiter.max,
}); });
server.use(limiter); server.use(limiter);
} }
@@ -325,7 +326,7 @@ function initCache() {
botCache = new LRU( { botCache = new LRU( {
max: environment.cache.serverSide.botCache.max, max: environment.cache.serverSide.botCache.max,
ttl: environment.cache.serverSide.botCache.timeToLive, 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( { anonymousCache = new LRU( {
max: environment.cache.serverSide.anonymousCache.max, max: environment.cache.serverSide.anonymousCache.max,
ttl: environment.cache.serverSide.anonymousCache.timeToLive, 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<string, any>, req, r
const key = getCacheKey(req); const key = getCacheKey(req);
// Check if this page is in our cache // Check if this page is in our cache
let cachedCopy = cache.get(key); const cachedCopy = cache.get(key);
if (cachedCopy) { if (cachedCopy) {
if (environment.cache.serverSide.debug) { console.log(`CACHE HIT FOR ${key} in ${cacheName} cache`); } if (environment.cache.serverSide.debug) { console.log(`CACHE HIT FOR ${key} in ${cacheName} cache`); }
@@ -529,20 +530,20 @@ function serverStarted() {
function createHttpsServer(keys) { function createHttpsServer(keys) {
const listener = createServer({ const listener = createServer({
key: keys.serviceKey, key: keys.serviceKey,
cert: keys.certificate cert: keys.certificate,
}, app).listen(environment.ui.port, environment.ui.host, () => { }, app).listen(environment.ui.port, environment.ui.host, () => {
serverStarted(); serverStarted();
}); });
// Graceful shutdown when signalled // Graceful shutdown when signalled
const terminator = createHttpTerminator({server: listener}); const terminator = createHttpTerminator({ server: listener });
process.on('SIGINT', () => { process.on('SIGINT', () => {
void (async ()=> { void (async ()=> {
console.debug('Closing HTTPS server on signal'); console.debug('Closing HTTPS server on signal');
await terminator.terminate().catch(e => { console.error(e); }); await terminator.terminate().catch(e => { console.error(e); });
console.debug('HTTPS server closed'); console.debug('HTTPS server closed');
})(); })();
}); });
} }
/** /**
@@ -559,14 +560,14 @@ function run() {
}); });
// Graceful shutdown when signalled // Graceful shutdown when signalled
const terminator = createHttpTerminator({server: listener}); const terminator = createHttpTerminator({ server: listener });
process.on('SIGINT', () => { process.on('SIGINT', () => {
void (async () => { void (async () => {
console.debug('Closing HTTP server on signal'); console.debug('Closing HTTP server on signal');
await terminator.terminate().catch(e => { console.error(e); }); await terminator.terminate().catch(e => { console.error(e); });
console.debug('HTTP server closed.');return undefined; console.debug('HTTP server closed.');return undefined;
})(); })();
}); });
} }
function start() { function start() {
@@ -597,7 +598,7 @@ function start() {
if (serviceKey && certificate) { if (serviceKey && certificate) {
createHttpsServer({ createHttpsServer({
serviceKey: serviceKey, serviceKey: serviceKey,
certificate: certificate certificate: certificate,
}); });
} else { } 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.'); 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({ createCertificate({
days: 1, days: 1,
selfSigned: true selfSigned: true,
}, (error, keys) => { }, (error, keys) => {
createHttpsServer(keys); createHttpsServer(keys);
}); });
@@ -627,7 +628,7 @@ function healthCheck(req, res) {
}) })
.catch((error) => { .catch((error) => {
res.status(error.response.status).send({ res.status(error.response.status).send({
error: error.message error: error.message,
}); });
}); });
} }

View File

@@ -1,11 +1,14 @@
import { AbstractControl } from '@angular/forms'; import { AbstractControl } from '@angular/forms';
import { Route } from '@angular/router'; import {
mapToCanActivate,
Route,
} from '@angular/router';
import { import {
DYNAMIC_ERROR_MESSAGES_MATCHER, DYNAMIC_ERROR_MESSAGES_MATCHER,
DynamicErrorMessagesMatcher, DynamicErrorMessagesMatcher,
} from '@ng-dynamic-forms/core'; } 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 { 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 { SiteAdministratorGuard } from '../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard';
import { import {
@@ -39,76 +42,76 @@ export const ROUTES: Route[] = [
path: EPERSON_PATH, path: EPERSON_PATH,
component: EPeopleRegistryComponent, component: EPeopleRegistryComponent,
resolve: { resolve: {
breadcrumb: I18nBreadcrumbResolver, breadcrumb: i18nBreadcrumbResolver,
}, },
providers, providers,
data: { title: 'admin.access-control.epeople.title', breadcrumbKey: 'admin.access-control.epeople' }, data: { title: 'admin.access-control.epeople.title', breadcrumbKey: 'admin.access-control.epeople' },
canActivate: [SiteAdministratorGuard], canActivate: mapToCanActivate([SiteAdministratorGuard]),
}, },
{ {
path: `${EPERSON_PATH}/create`, path: `${EPERSON_PATH}/create`,
component: EPersonFormComponent, component: EPersonFormComponent,
resolve: { resolve: {
breadcrumb: I18nBreadcrumbResolver, breadcrumb: i18nBreadcrumbResolver,
}, },
providers, providers,
data: { title: 'admin.access-control.epeople.add.title', breadcrumbKey: 'admin.access-control.epeople.add' }, data: { title: 'admin.access-control.epeople.add.title', breadcrumbKey: 'admin.access-control.epeople.add' },
canActivate: [SiteAdministratorGuard], canActivate: mapToCanActivate([SiteAdministratorGuard]),
}, },
{ {
path: `${EPERSON_PATH}/:id/edit`, path: `${EPERSON_PATH}/:id/edit`,
component: EPersonFormComponent, component: EPersonFormComponent,
resolve: { resolve: {
breadcrumb: I18nBreadcrumbResolver, breadcrumb: i18nBreadcrumbResolver,
ePerson: EPersonResolver, ePerson: EPersonResolver,
}, },
providers, providers,
data: { title: 'admin.access-control.epeople.edit.title', breadcrumbKey: 'admin.access-control.epeople.edit' }, data: { title: 'admin.access-control.epeople.edit.title', breadcrumbKey: 'admin.access-control.epeople.edit' },
canActivate: [SiteAdministratorGuard], canActivate: mapToCanActivate([SiteAdministratorGuard]),
}, },
{ {
path: GROUP_PATH, path: GROUP_PATH,
component: GroupsRegistryComponent, component: GroupsRegistryComponent,
resolve: { resolve: {
breadcrumb: I18nBreadcrumbResolver, breadcrumb: i18nBreadcrumbResolver,
}, },
providers, providers,
data: { title: 'admin.access-control.groups.title', breadcrumbKey: 'admin.access-control.groups' }, data: { title: 'admin.access-control.groups.title', breadcrumbKey: 'admin.access-control.groups' },
canActivate: [GroupAdministratorGuard], canActivate: mapToCanActivate([GroupAdministratorGuard]),
}, },
{ {
path: `${GROUP_PATH}/create`, path: `${GROUP_PATH}/create`,
component: GroupFormComponent, component: GroupFormComponent,
resolve: { resolve: {
breadcrumb: I18nBreadcrumbResolver, breadcrumb: i18nBreadcrumbResolver,
}, },
providers, providers,
data: { data: {
title: 'admin.access-control.groups.title.addGroup', title: 'admin.access-control.groups.title.addGroup',
breadcrumbKey: 'admin.access-control.groups.addGroup', breadcrumbKey: 'admin.access-control.groups.addGroup',
}, },
canActivate: [GroupAdministratorGuard], canActivate: mapToCanActivate([GroupAdministratorGuard]),
}, },
{ {
path: `${GROUP_PATH}/:groupId/edit`, path: `${GROUP_PATH}/:groupId/edit`,
component: GroupFormComponent, component: GroupFormComponent,
resolve: { resolve: {
breadcrumb: I18nBreadcrumbResolver, breadcrumb: i18nBreadcrumbResolver,
}, },
providers, providers,
data: { data: {
title: 'admin.access-control.groups.title.singleGroup', title: 'admin.access-control.groups.title.singleGroup',
breadcrumbKey: 'admin.access-control.groups.singleGroup', breadcrumbKey: 'admin.access-control.groups.singleGroup',
}, },
canActivate: [GroupPageGuard], canActivate: mapToCanActivate([GroupPageGuard]),
}, },
{ {
path: 'bulk-access', path: 'bulk-access',
component: BulkAccessComponent, component: BulkAccessComponent,
resolve: { resolve: {
breadcrumb: I18nBreadcrumbResolver, breadcrumb: i18nBreadcrumbResolver,
}, },
data: { title: 'admin.access-control.bulk-access.title', breadcrumbKey: 'admin.access-control.bulk-access' }, data: { title: 'admin.access-control.bulk-access.title', breadcrumbKey: 'admin.access-control.bulk-access' },
canActivate: [SiteAdministratorGuard], canActivate: mapToCanActivate([SiteAdministratorGuard]),
}, },
]; ];

View File

@@ -1,7 +1,6 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
Resolve,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } from '@angular/router';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
@@ -27,7 +26,7 @@ export const EPERSON_EDIT_FOLLOW_LINKS: FollowLinkConfig<EPerson>[] = [
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class EPersonResolver implements Resolve<RemoteData<EPerson>> { export class EPersonResolver {
constructor( constructor(
protected ePersonService: EPersonDataService, protected ePersonService: EPersonDataService,

View File

@@ -1,7 +1,7 @@
import { Routes } from '@angular/router'; import { Routes } from '@angular/router';
import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver'; import { i18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver';
import { NavigationBreadcrumbResolver } from '../../core/breadcrumbs/navigation-breadcrumb.resolver'; import { navigationBreadcrumbResolver } from '../../core/breadcrumbs/navigation-breadcrumb.resolver';
import { LdnServiceFormComponent } from './ldn-service-form/ldn-service-form.component'; import { LdnServiceFormComponent } from './ldn-service-form/ldn-service-form.component';
import { LdnServicesOverviewComponent } from './ldn-services-directory/ldn-services-directory.component'; import { LdnServicesOverviewComponent } from './ldn-services-directory/ldn-services-directory.component';
@@ -10,18 +10,18 @@ const moduleRoutes: Routes = [
path: '', path: '',
pathMatch: 'full', pathMatch: 'full',
component: LdnServicesOverviewComponent, component: LdnServicesOverviewComponent,
resolve: { breadcrumb: I18nBreadcrumbResolver }, resolve: { breadcrumb: i18nBreadcrumbResolver },
data: { title: 'ldn-registered-services.title', breadcrumbKey: 'ldn-registered-services.new' }, data: { title: 'ldn-registered-services.title', breadcrumbKey: 'ldn-registered-services.new' },
}, },
{ {
path: 'new', path: 'new',
resolve: { breadcrumb: NavigationBreadcrumbResolver }, resolve: { breadcrumb: navigationBreadcrumbResolver },
component: LdnServiceFormComponent, component: LdnServiceFormComponent,
data: { title: 'ldn-register-new-service.title', breadcrumbKey: 'ldn-register-new-service' }, data: { title: 'ldn-register-new-service.title', breadcrumbKey: 'ldn-register-new-service' },
}, },
{ {
path: 'edit/:serviceId', path: 'edit/:serviceId',
resolve: { breadcrumb: NavigationBreadcrumbResolver }, resolve: { breadcrumb: navigationBreadcrumbResolver },
component: LdnServiceFormComponent, component: LdnServiceFormComponent,
data: { title: 'ldn-edit-service.title', breadcrumbKey: 'ldn-edit-service' }, data: { title: 'ldn-edit-service.title', breadcrumbKey: 'ldn-edit-service' },
}, },

View File

@@ -1,7 +1,6 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
Resolve,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } 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. * This class represents a resolver that retrieve the route data before the route is activated.
*/ */
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class NotificationsSuggestionTargetsPageResolver implements Resolve<NotificationsSuggestionTargetsPageParams> { export class NotificationsSuggestionTargetsPageResolver {
/** /**
* Method for resolving the parameters in the current route. * Method for resolving the parameters in the current route.

View File

@@ -1,12 +1,12 @@
import { Route } from '@angular/router'; import { Route } from '@angular/router';
import { AuthenticatedGuard } from '../../core/auth/authenticated.guard'; import { authenticatedGuard } from '../../core/auth/authenticated.guard';
import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver'; import { i18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver';
import { QualityAssuranceBreadcrumbResolver } from '../../core/breadcrumbs/quality-assurance-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 { 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 { 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 { 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 { 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 { 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 { 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'; 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[] = [ export const ROUTES: Route[] = [
{ {
canActivate: [ AuthenticatedGuard ], canActivate: [ authenticatedGuard ],
path: `${PUBLICATION_CLAIMS_PATH}`, path: `${PUBLICATION_CLAIMS_PATH}`,
component: AdminNotificationsPublicationClaimPageComponent, component: AdminNotificationsPublicationClaimPageComponent,
pathMatch: 'full', pathMatch: 'full',
resolve: { resolve: {
breadcrumb: I18nBreadcrumbResolver, breadcrumb: i18nBreadcrumbResolver,
suggestionTargetParams: AdminNotificationsPublicationClaimPageResolver, suggestionTargetParams: AdminNotificationsPublicationClaimPageResolver,
}, },
data: { data: {
@@ -34,12 +34,12 @@ export const ROUTES: Route[] = [
}, },
}, },
{ {
canActivate: [AuthenticatedGuard], canActivate: [authenticatedGuard],
path: `${QUALITY_ASSURANCE_EDIT_PATH}/:sourceId`, path: `${QUALITY_ASSURANCE_EDIT_PATH}/:sourceId`,
component: QualityAssuranceTopicsPageComponent, component: QualityAssuranceTopicsPageComponent,
pathMatch: 'full', pathMatch: 'full',
resolve: { resolve: {
breadcrumb: QualityAssuranceBreadcrumbResolver, breadcrumb: qualityAssuranceBreadcrumbResolver,
openaireQualityAssuranceTopicsParams: QualityAssuranceTopicsPageResolver, openaireQualityAssuranceTopicsParams: QualityAssuranceTopicsPageResolver,
}, },
data: { data: {
@@ -49,12 +49,12 @@ export const ROUTES: Route[] = [
}, },
}, },
{ {
canActivate: [ AuthenticatedGuard ], canActivate: [ authenticatedGuard ],
path: `${QUALITY_ASSURANCE_EDIT_PATH}/:sourceId/target/:targetId`, path: `${QUALITY_ASSURANCE_EDIT_PATH}/:sourceId/target/:targetId`,
component: QualityAssuranceTopicsPageComponent, component: QualityAssuranceTopicsPageComponent,
pathMatch: 'full', pathMatch: 'full',
resolve: { resolve: {
breadcrumb: I18nBreadcrumbResolver, breadcrumb: i18nBreadcrumbResolver,
openaireQualityAssuranceTopicsParams: QualityAssuranceTopicsPageResolver, openaireQualityAssuranceTopicsParams: QualityAssuranceTopicsPageResolver,
}, },
data: { data: {
@@ -64,14 +64,14 @@ export const ROUTES: Route[] = [
}, },
}, },
{ {
canActivate: [AuthenticatedGuard], canActivate: [authenticatedGuard],
path: `${QUALITY_ASSURANCE_EDIT_PATH}`, path: `${QUALITY_ASSURANCE_EDIT_PATH}`,
component: QualityAssuranceSourcePageComponent, component: QualityAssuranceSourcePageComponent,
pathMatch: 'full', pathMatch: 'full',
resolve: { resolve: {
breadcrumb: I18nBreadcrumbResolver, breadcrumb: i18nBreadcrumbResolver,
openaireQualityAssuranceSourceParams: QualityAssuranceSourcePageResolver, openaireQualityAssuranceSourceParams: QualityAssuranceSourcePageResolver,
sourceData: SourceDataResolver, sourceData: qualityAssuranceSourceDataResolver,
}, },
data: { data: {
title: 'admin.notifications.source.breadcrumbs', title: 'admin.notifications.source.breadcrumbs',
@@ -80,13 +80,13 @@ export const ROUTES: Route[] = [
}, },
}, },
{ {
canActivate: [AuthenticatedGuard], canActivate: [authenticatedGuard],
path: `${QUALITY_ASSURANCE_EDIT_PATH}/:sourceId/:topicId`, path: `${QUALITY_ASSURANCE_EDIT_PATH}/:sourceId/:topicId`,
component: QualityAssuranceEventsPageComponent, component: QualityAssuranceEventsPageComponent,
pathMatch: 'full', pathMatch: 'full',
resolve: { resolve: {
breadcrumb: QualityAssuranceBreadcrumbResolver, breadcrumb: qualityAssuranceBreadcrumbResolver,
openaireQualityAssuranceEventsParams: QualityAssuranceEventsPageResolver, openaireQualityAssuranceEventsParams: qualityAssuranceEventsPageResolver,
}, },
data: { data: {
title: 'admin.notifications.event.page.title', title: 'admin.notifications.event.page.title',

View File

@@ -1,7 +1,10 @@
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 { NotifyInfoGuard } from '../../core/coar-notify/notify-info/notify-info.guard'; 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 { SiteAdministratorGuard } from '../../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard';
import { AdminNotifyDashboardComponent } from './admin-notify-dashboard.component'; import { AdminNotifyDashboardComponent } from './admin-notify-dashboard.component';
import { AdminNotifyIncomingComponent } from './admin-notify-logs/admin-notify-incoming/admin-notify-incoming.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[] = [ export const ROUTES: Route[] = [
{ {
canActivate: [SiteAdministratorGuard, NotifyInfoGuard], canActivate: [...mapToCanActivate([SiteAdministratorGuard]), notifyInfoGuard],
path: '', path: '',
resolve: { resolve: {
breadcrumb: I18nBreadcrumbResolver, breadcrumb: i18nBreadcrumbResolver,
}, },
component: AdminNotifyDashboardComponent, component: AdminNotifyDashboardComponent,
pathMatch: 'full', pathMatch: 'full',
@@ -24,10 +27,10 @@ export const ROUTES: Route[] = [
{ {
path: 'inbound', path: 'inbound',
resolve: { resolve: {
breadcrumb: I18nBreadcrumbResolver, breadcrumb: i18nBreadcrumbResolver,
}, },
component: AdminNotifyIncomingComponent, component: AdminNotifyIncomingComponent,
canActivate: [SiteAdministratorGuard, NotifyInfoGuard], canActivate: [...mapToCanActivate([SiteAdministratorGuard]), notifyInfoGuard],
data: { data: {
title: 'admin.notify.dashboard.page.title', title: 'admin.notify.dashboard.page.title',
breadcrumbKey: 'admin.notify.dashboard', breadcrumbKey: 'admin.notify.dashboard',
@@ -36,10 +39,10 @@ export const ROUTES: Route[] = [
{ {
path: 'outbound', path: 'outbound',
resolve: { resolve: {
breadcrumb: I18nBreadcrumbResolver, breadcrumb: i18nBreadcrumbResolver,
}, },
component: AdminNotifyOutgoingComponent, component: AdminNotifyOutgoingComponent,
canActivate: [SiteAdministratorGuard, NotifyInfoGuard], canActivate: [...mapToCanActivate([SiteAdministratorGuard]), notifyInfoGuard],
data: { data: {
title: 'admin.notify.dashboard.page.title', title: 'admin.notify.dashboard.page.title',
breadcrumbKey: 'admin.notify.dashboard', breadcrumbKey: 'admin.notify.dashboard',

View File

@@ -1,6 +1,6 @@
import { Route } from '@angular/router'; 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 { BITSTREAMFORMATS_MODULE_PATH } from './admin-registries-routing-paths';
import { MetadataRegistryComponent } from './metadata-registry/metadata-registry.component'; import { MetadataRegistryComponent } from './metadata-registry/metadata-registry.component';
import { MetadataSchemaComponent } from './metadata-schema/metadata-schema.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[] = [ export const ROUTES: Route[] = [
{ {
path: 'metadata', path: 'metadata',
resolve: { breadcrumb: I18nBreadcrumbResolver }, resolve: { breadcrumb: i18nBreadcrumbResolver },
data: { title: 'admin.registries.metadata.title', breadcrumbKey: 'admin.registries.metadata' }, data: { title: 'admin.registries.metadata.title', breadcrumbKey: 'admin.registries.metadata' },
children: [ children: [
{ {
@@ -17,7 +17,7 @@ export const ROUTES: Route[] = [
}, },
{ {
path: ':schemaName', path: ':schemaName',
resolve: { breadcrumb: I18nBreadcrumbResolver }, resolve: { breadcrumb: i18nBreadcrumbResolver },
component: MetadataSchemaComponent, component: MetadataSchemaComponent,
data: { title: 'admin.registries.schema.title', breadcrumbKey: 'admin.registries.schema' }, data: { title: 'admin.registries.schema.title', breadcrumbKey: 'admin.registries.schema' },
}, },
@@ -25,7 +25,7 @@ export const ROUTES: Route[] = [
}, },
{ {
path: BITSTREAMFORMATS_MODULE_PATH, path: BITSTREAMFORMATS_MODULE_PATH,
resolve: { breadcrumb: I18nBreadcrumbResolver }, resolve: { breadcrumb: i18nBreadcrumbResolver },
loadChildren: () => import('./bitstream-formats/bitstream-formats-routes') loadChildren: () => import('./bitstream-formats/bitstream-formats-routes')
.then((m) => m.ROUTES), .then((m) => m.ROUTES),
data: { title: 'admin.registries.bitstream-formats.title', breadcrumbKey: 'admin.registries.bitstream-formats' }, data: { title: 'admin.registries.bitstream-formats.title', breadcrumbKey: 'admin.registries.bitstream-formats' },

View File

@@ -1,9 +1,9 @@
import { Route } from '@angular/router'; 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 { AddBitstreamFormatComponent } from './add-bitstream-format/add-bitstream-format.component';
import { BitstreamFormatsComponent } from './bitstream-formats.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'; import { EditBitstreamFormatComponent } from './edit-bitstream-format/edit-bitstream-format.component';
const BITSTREAMFORMAT_EDIT_PATH = ':id/edit'; const BITSTREAMFORMAT_EDIT_PATH = ':id/edit';
@@ -19,7 +19,7 @@ export const ROUTES: Route[] = [
}, },
{ {
path: BITSTREAMFORMAT_ADD_PATH, path: BITSTREAMFORMAT_ADD_PATH,
resolve: { breadcrumb: I18nBreadcrumbResolver }, resolve: { breadcrumb: i18nBreadcrumbResolver },
providers, providers,
component: AddBitstreamFormatComponent, component: AddBitstreamFormatComponent,
data: { breadcrumbKey: 'admin.registries.bitstream-formats.create' }, data: { breadcrumbKey: 'admin.registries.bitstream-formats.create' },
@@ -29,8 +29,8 @@ export const ROUTES: Route[] = [
providers, providers,
component: EditBitstreamFormatComponent, component: EditBitstreamFormatComponent,
resolve: { resolve: {
bitstreamFormat: BitstreamFormatsResolver, bitstreamFormat: bitstreamFormatsResolver,
breadcrumb: I18nBreadcrumbResolver, breadcrumb: i18nBreadcrumbResolver,
}, },
data: { breadcrumbKey: 'admin.registries.bitstream-formats.edit' }, data: { breadcrumbKey: 'admin.registries.bitstream-formats.edit' },
}, },

View File

@@ -1,7 +1,7 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
Resolve, ResolveFn,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } from '@angular/router';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
@@ -12,24 +12,20 @@ import { BitstreamFormat } from '../../../core/shared/bitstream-format.model';
import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; 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<<RemoteData<BitstreamFormat>> Emits the found bitstreamFormat based on the parameters in the current route,
* or an error if something went wrong
*/ */
@Injectable({ providedIn: 'root' }) export const bitstreamFormatsResolver: ResolveFn<RemoteData<BitstreamFormat>> = (
export class BitstreamFormatsResolver implements Resolve<RemoteData<BitstreamFormat>> { route: ActivatedRouteSnapshot,
constructor(private bitstreamFormatDataService: BitstreamFormatDataService) { state: RouterStateSnapshot,
} bitstreamFormatDataService: BitstreamFormatDataService = inject(BitstreamFormatDataService),
): Observable<RemoteData<BitstreamFormat>> => {
/** return bitstreamFormatDataService.findById(route.params.id)
* Method for resolving an bitstreamFormat based on the parameters in the current route .pipe(
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot getFirstCompletedRemoteData(),
* @param {RouterStateSnapshot} state The current RouterStateSnapshot );
* @returns Observable<<RemoteData<BitstreamFormat>> 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<RemoteData<BitstreamFormat>> {
return this.bitstreamFormatDataService.findById(route.params.id)
.pipe(
getFirstCompletedRemoteData(),
);
}
}

View File

@@ -1,13 +1,13 @@
import { Route } from '@angular/router'; 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 { FilteredCollectionsComponent } from './filtered-collections/filtered-collections.component';
import { FilteredItemsComponent } from './filtered-items/filtered-items.component'; import { FilteredItemsComponent } from './filtered-items/filtered-items.component';
export const ROUTES: Route[] = [ export const ROUTES: Route[] = [
{ {
path: 'collections', path: 'collections',
resolve: { breadcrumb: I18nBreadcrumbResolver }, resolve: { breadcrumb: i18nBreadcrumbResolver },
data: { title: 'admin.reports.collections.title', breadcrumbKey: 'admin.reports.collections' }, data: { title: 'admin.reports.collections.title', breadcrumbKey: 'admin.reports.collections' },
children: [ children: [
{ {
@@ -18,7 +18,7 @@ export const ROUTES: Route[] = [
}, },
{ {
path: 'queries', path: 'queries',
resolve: { breadcrumb: I18nBreadcrumbResolver }, resolve: { breadcrumb: i18nBreadcrumbResolver },
data: { title: 'admin.reports.items.title', breadcrumbKey: 'admin.reports.items' }, data: { title: 'admin.reports.items.title', breadcrumbKey: 'admin.reports.items' },
children: [ children: [
{ {

View File

@@ -1,6 +1,6 @@
import { Route } from '@angular/router'; 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 { AdminCurationTasksComponent } from './admin-curation-tasks/admin-curation-tasks.component';
import { BatchImportPageComponent } from './admin-import-batch-page/batch-import-page.component'; import { BatchImportPageComponent } from './admin-import-batch-page/batch-import-page.component';
import { MetadataImportPageComponent } from './admin-import-metadata-page/metadata-import-page.component'; import { MetadataImportPageComponent } from './admin-import-metadata-page/metadata-import-page.component';
@@ -27,37 +27,37 @@ export const ROUTES: Route[] = [
}, },
{ {
path: 'search', path: 'search',
resolve: { breadcrumb: I18nBreadcrumbResolver }, resolve: { breadcrumb: i18nBreadcrumbResolver },
component: AdminSearchPageComponent, component: AdminSearchPageComponent,
data: { title: 'admin.search.title', breadcrumbKey: 'admin.search' }, data: { title: 'admin.search.title', breadcrumbKey: 'admin.search' },
}, },
{ {
path: 'workflow', path: 'workflow',
resolve: { breadcrumb: I18nBreadcrumbResolver }, resolve: { breadcrumb: i18nBreadcrumbResolver },
component: AdminWorkflowPageComponent, component: AdminWorkflowPageComponent,
data: { title: 'admin.workflow.title', breadcrumbKey: 'admin.workflow' }, data: { title: 'admin.workflow.title', breadcrumbKey: 'admin.workflow' },
}, },
{ {
path: 'curation-tasks', path: 'curation-tasks',
resolve: { breadcrumb: I18nBreadcrumbResolver }, resolve: { breadcrumb: i18nBreadcrumbResolver },
component: AdminCurationTasksComponent, component: AdminCurationTasksComponent,
data: { title: 'admin.curation-tasks.title', breadcrumbKey: 'admin.curation-tasks' }, data: { title: 'admin.curation-tasks.title', breadcrumbKey: 'admin.curation-tasks' },
}, },
{ {
path: 'metadata-import', path: 'metadata-import',
resolve: { breadcrumb: I18nBreadcrumbResolver }, resolve: { breadcrumb: i18nBreadcrumbResolver },
component: MetadataImportPageComponent, component: MetadataImportPageComponent,
data: { title: 'admin.metadata-import.title', breadcrumbKey: 'admin.metadata-import' }, data: { title: 'admin.metadata-import.title', breadcrumbKey: 'admin.metadata-import' },
}, },
{ {
path: 'batch-import', path: 'batch-import',
resolve: { breadcrumb: I18nBreadcrumbResolver }, resolve: { breadcrumb: i18nBreadcrumbResolver },
component: BatchImportPageComponent, component: BatchImportPageComponent,
data: { title: 'admin.batch-import.title', breadcrumbKey: 'admin.batch-import' }, data: { title: 'admin.batch-import.title', breadcrumbKey: 'admin.batch-import' },
}, },
{ {
path: 'system-wide-alert', path: 'system-wide-alert',
resolve: { breadcrumb: I18nBreadcrumbResolver }, resolve: { breadcrumb: i18nBreadcrumbResolver },
loadChildren: () => import('../system-wide-alert/system-wide-alert-routes').then((m) => m.ROUTES), 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' }, data: { title: 'admin.system-wide-alert.title', breadcrumbKey: 'admin.system-wide-alert' },
}, },

View File

@@ -31,12 +31,7 @@ describe('AdminSidebarSectionComponent', () => {
{ provide: MenuService, useValue: menuService }, { provide: MenuService, useValue: menuService },
{ provide: CSSVariableService, useClass: CSSVariableServiceStub }, { provide: CSSVariableService, useClass: CSSVariableServiceStub },
], ],
}).overrideComponent(AdminSidebarSectionComponent, { }).compileComponents();
set: {
entryComponents: [TestComponent],
},
})
.compileComponents();
})); }));
beforeEach(() => { beforeEach(() => {
@@ -70,12 +65,7 @@ describe('AdminSidebarSectionComponent', () => {
{ provide: MenuService, useValue: menuService }, { provide: MenuService, useValue: menuService },
{ provide: CSSVariableService, useClass: CSSVariableServiceStub }, { provide: CSSVariableService, useClass: CSSVariableServiceStub },
], ],
}).overrideComponent(AdminSidebarSectionComponent, { }).compileComponents();
set: {
entryComponents: [TestComponent],
},
})
.compileComponents();
})); }));
beforeEach(() => { beforeEach(() => {

View File

@@ -31,12 +31,7 @@ describe('ExpandableAdminSidebarSectionComponent', () => {
{ provide: CSSVariableService, useClass: CSSVariableServiceStub }, { provide: CSSVariableService, useClass: CSSVariableServiceStub },
{ provide: Router, useValue: new RouterStub() }, { provide: Router, useValue: new RouterStub() },
], ],
}).overrideComponent(ExpandableAdminSidebarSectionComponent, { }).compileComponents();
set: {
entryComponents: [TestComponent],
},
})
.compileComponents();
})); }));
beforeEach(() => { beforeEach(() => {

View File

@@ -1,5 +1,6 @@
import { import {
InMemoryScrollingOptions, InMemoryScrollingOptions,
mapToCanActivate,
Route, Route,
RouterConfigOptions, RouterConfigOptions,
} from '@angular/router'; } from '@angular/router';
@@ -23,18 +24,18 @@ import {
} from './app-routing-paths'; } from './app-routing-paths';
import { COLLECTION_MODULE_PATH } from './collection-page/collection-page-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 { COMMUNITY_MODULE_PATH } from './community-page/community-page-routing-paths';
import { AuthBlockingGuard } from './core/auth/auth-blocking.guard'; import { authBlockingGuard } from './core/auth/auth-blocking.guard';
import { AuthenticatedGuard } from './core/auth/authenticated.guard'; import { authenticatedGuard } from './core/auth/authenticated.guard';
import { GroupAdministratorGuard } from './core/data/feature-authorization/feature-authorization-guard/group-administrator.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 { 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 { 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 { 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 { ForgotPasswordCheckGuard } from './core/rest-property/forgot-password-check-guard.guard';
import { ServerCheckGuard } from './core/server-check/server-check.guard'; import { ServerCheckGuard } from './core/server-check/server-check.guard';
import { ThemedForbiddenComponent } from './forbidden/themed-forbidden.component'; import { ThemedForbiddenComponent } from './forbidden/themed-forbidden.component';
import { ITEM_MODULE_PATH } from './item-page/item-page-routing-paths'; 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 { provideSuggestionNotificationsState } from './notifications/provide-suggestion-notifications-state';
import { ThemedPageErrorComponent } from './page-error/themed-page-error.component'; import { ThemedPageErrorComponent } from './page-error/themed-page-error.component';
import { ThemedPageInternalServerErrorComponent } from './page-internal-server-error/themed-page-internal-server-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: ERROR_PAGE, component: ThemedPageErrorComponent },
{ {
path: '', path: '',
canActivate: [AuthBlockingGuard], canActivate: [authBlockingGuard],
canActivateChild: [ServerCheckGuard], canActivateChild: [ServerCheckGuard],
resolve: [MenuResolver], resolve: [menuResolver],
children: [ children: [
{ path: '', redirectTo: '/home', pathMatch: 'full' }, { path: '', redirectTo: '/home', pathMatch: 'full' },
{ {
path: 'reload/:rnd', path: 'reload/:rnd',
component: ThemedPageNotFoundComponent, component: ThemedPageNotFoundComponent,
pathMatch: 'full', pathMatch: 'full',
canActivate: [ReloadGuard], canActivate: [reloadGuard],
}, },
{ {
path: 'home', path: 'home',
@@ -65,105 +66,105 @@ export const APP_ROUTES: Route[] = [
.then((m) => m.ROUTES), .then((m) => m.ROUTES),
data: { showBreadcrumbs: false }, data: { showBreadcrumbs: false },
providers: [provideSuggestionNotificationsState()], providers: [provideSuggestionNotificationsState()],
canActivate: [EndUserAgreementCurrentUserGuard], canActivate: mapToCanActivate([EndUserAgreementCurrentUserGuard]),
}, },
{ {
path: 'community-list', path: 'community-list',
loadChildren: () => import('./community-list-page/community-list-page-routes') loadChildren: () => import('./community-list-page/community-list-page-routes')
.then((m) => m.ROUTES), .then((m) => m.ROUTES),
canActivate: [EndUserAgreementCurrentUserGuard], canActivate: mapToCanActivate([EndUserAgreementCurrentUserGuard]),
}, },
{ {
path: 'id', path: 'id',
loadChildren: () => import('./lookup-by-id/lookup-by-id-routes') loadChildren: () => import('./lookup-by-id/lookup-by-id-routes')
.then((m) => m.ROUTES), .then((m) => m.ROUTES),
canActivate: [EndUserAgreementCurrentUserGuard], canActivate: mapToCanActivate([EndUserAgreementCurrentUserGuard]),
}, },
{ {
path: 'handle', path: 'handle',
loadChildren: () => import('./lookup-by-id/lookup-by-id-routes') loadChildren: () => import('./lookup-by-id/lookup-by-id-routes')
.then((m) => m.ROUTES), .then((m) => m.ROUTES),
canActivate: [EndUserAgreementCurrentUserGuard], canActivate: mapToCanActivate([EndUserAgreementCurrentUserGuard]),
}, },
{ {
path: REGISTER_PATH, path: REGISTER_PATH,
loadChildren: () => import('./register-page/register-page-routes') loadChildren: () => import('./register-page/register-page-routes')
.then((m) => m.ROUTES), .then((m) => m.ROUTES),
canActivate: [SiteRegisterGuard], canActivate: mapToCanActivate([SiteRegisterGuard]),
}, },
{ {
path: FORGOT_PASSWORD_PATH, path: FORGOT_PASSWORD_PATH,
loadChildren: () => import('./forgot-password/forgot-password-routes') loadChildren: () => import('./forgot-password/forgot-password-routes')
.then((m) => m.ROUTES), .then((m) => m.ROUTES),
canActivate: [EndUserAgreementCurrentUserGuard, ForgotPasswordCheckGuard], canActivate: mapToCanActivate([EndUserAgreementCurrentUserGuard, ForgotPasswordCheckGuard]),
}, },
{ {
path: COMMUNITY_MODULE_PATH, path: COMMUNITY_MODULE_PATH,
loadChildren: () => import('./community-page/community-page-routes') loadChildren: () => import('./community-page/community-page-routes')
.then((m) => m.ROUTES), .then((m) => m.ROUTES),
canActivate: [EndUserAgreementCurrentUserGuard], canActivate: mapToCanActivate([EndUserAgreementCurrentUserGuard]),
}, },
{ {
path: COLLECTION_MODULE_PATH, path: COLLECTION_MODULE_PATH,
loadChildren: () => import('./collection-page/collection-page-routes') loadChildren: () => import('./collection-page/collection-page-routes')
.then((m) => m.ROUTES), .then((m) => m.ROUTES),
canActivate: [EndUserAgreementCurrentUserGuard], canActivate: mapToCanActivate([EndUserAgreementCurrentUserGuard]),
}, },
{ {
path: ITEM_MODULE_PATH, path: ITEM_MODULE_PATH,
loadChildren: () => import('./item-page/item-page-routes') loadChildren: () => import('./item-page/item-page-routes')
.then((m) => m.ROUTES), .then((m) => m.ROUTES),
canActivate: [EndUserAgreementCurrentUserGuard], canActivate: mapToCanActivate([EndUserAgreementCurrentUserGuard]),
}, },
{ {
path: 'entities/:entity-type', path: 'entities/:entity-type',
loadChildren: () => import('./item-page/item-page-routes') loadChildren: () => import('./item-page/item-page-routes')
.then((m) => m.ROUTES), .then((m) => m.ROUTES),
canActivate: [EndUserAgreementCurrentUserGuard], canActivate: mapToCanActivate([EndUserAgreementCurrentUserGuard]),
}, },
{ {
path: LEGACY_BITSTREAM_MODULE_PATH, path: LEGACY_BITSTREAM_MODULE_PATH,
loadChildren: () => import('./bitstream-page/bitstream-page-routes') loadChildren: () => import('./bitstream-page/bitstream-page-routes')
.then((m) => m.ROUTES), .then((m) => m.ROUTES),
canActivate: [EndUserAgreementCurrentUserGuard], canActivate: mapToCanActivate([EndUserAgreementCurrentUserGuard]),
}, },
{ {
path: BITSTREAM_MODULE_PATH, path: BITSTREAM_MODULE_PATH,
loadChildren: () => import('./bitstream-page/bitstream-page-routes') loadChildren: () => import('./bitstream-page/bitstream-page-routes')
.then((m) => m.ROUTES), .then((m) => m.ROUTES),
canActivate: [EndUserAgreementCurrentUserGuard], canActivate: mapToCanActivate([EndUserAgreementCurrentUserGuard]),
}, },
{ {
path: 'mydspace', path: 'mydspace',
loadChildren: () => import('./my-dspace-page/my-dspace-page-routes') loadChildren: () => import('./my-dspace-page/my-dspace-page-routes')
.then((m) => m.ROUTES), .then((m) => m.ROUTES),
providers: [provideSuggestionNotificationsState()], providers: [provideSuggestionNotificationsState()],
canActivate: [AuthenticatedGuard, EndUserAgreementCurrentUserGuard], canActivate: [authenticatedGuard, ...mapToCanActivate([EndUserAgreementCurrentUserGuard])],
}, },
{ {
path: 'search', path: 'search',
loadChildren: () => import('./search-page/search-page-routes') loadChildren: () => import('./search-page/search-page-routes')
.then((m) => m.ROUTES), .then((m) => m.ROUTES),
canActivate: [EndUserAgreementCurrentUserGuard], canActivate: mapToCanActivate([EndUserAgreementCurrentUserGuard]),
}, },
{ {
path: 'browse', path: 'browse',
loadChildren: () => import('./browse-by/browse-by-page-routes') loadChildren: () => import('./browse-by/browse-by-page-routes')
.then((m) => m.ROUTES), .then((m) => m.ROUTES),
canActivate: [EndUserAgreementCurrentUserGuard], canActivate: mapToCanActivate([EndUserAgreementCurrentUserGuard]),
}, },
{ {
path: ADMIN_MODULE_PATH, path: ADMIN_MODULE_PATH,
loadChildren: () => import('./admin/admin-routes') loadChildren: () => import('./admin/admin-routes')
.then((m) => m.ROUTES), .then((m) => m.ROUTES),
canActivate: [SiteAdministratorGuard, EndUserAgreementCurrentUserGuard], canActivate: mapToCanActivate([SiteAdministratorGuard, EndUserAgreementCurrentUserGuard]),
}, },
{ {
path: NOTIFICATIONS_MODULE_PATH, path: NOTIFICATIONS_MODULE_PATH,
loadChildren: () => import('./quality-assurance-notifications-pages/notifications-pages-routes') loadChildren: () => import('./quality-assurance-notifications-pages/notifications-pages-routes')
.then((m) => m.ROUTES), .then((m) => m.ROUTES),
providers: [provideSuggestionNotificationsState()], providers: [provideSuggestionNotificationsState()],
canActivate: [AuthenticatedGuard, EndUserAgreementCurrentUserGuard], canActivate: [authenticatedGuard, ...mapToCanActivate([EndUserAgreementCurrentUserGuard])],
}, },
{ {
path: 'login', path: 'login',
@@ -180,47 +181,47 @@ export const APP_ROUTES: Route[] = [
loadChildren: () => import('./submit-page/submit-page-routes') loadChildren: () => import('./submit-page/submit-page-routes')
.then((m) => m.ROUTES), .then((m) => m.ROUTES),
providers: [provideSubmissionState()], providers: [provideSubmissionState()],
canActivate: [EndUserAgreementCurrentUserGuard], canActivate: mapToCanActivate([EndUserAgreementCurrentUserGuard]),
}, },
{ {
path: 'import-external', path: 'import-external',
loadChildren: () => import('./import-external-page/import-external-page-routes') loadChildren: () => import('./import-external-page/import-external-page-routes')
.then((m) => m.ROUTES), .then((m) => m.ROUTES),
canActivate: [EndUserAgreementCurrentUserGuard], canActivate: mapToCanActivate([EndUserAgreementCurrentUserGuard]),
}, },
{ {
path: 'workspaceitems', path: 'workspaceitems',
loadChildren: () => import('./workspaceitems-edit-page/workspaceitems-edit-page-routes') loadChildren: () => import('./workspaceitems-edit-page/workspaceitems-edit-page-routes')
.then((m) => m.ROUTES), .then((m) => m.ROUTES),
providers: [provideSubmissionState()], providers: [provideSubmissionState()],
canActivate: [EndUserAgreementCurrentUserGuard], canActivate: mapToCanActivate([EndUserAgreementCurrentUserGuard]),
}, },
{ {
path: WORKFLOW_ITEM_MODULE_PATH, path: WORKFLOW_ITEM_MODULE_PATH,
providers: [provideSubmissionState()], providers: [provideSubmissionState()],
loadChildren: () => import('./workflowitems-edit-page/workflowitems-edit-page-routes') loadChildren: () => import('./workflowitems-edit-page/workflowitems-edit-page-routes')
.then((m) => m.ROUTES), .then((m) => m.ROUTES),
canActivate: [EndUserAgreementCurrentUserGuard], canActivate: mapToCanActivate([EndUserAgreementCurrentUserGuard]),
}, },
{ {
path: PROFILE_MODULE_PATH, path: PROFILE_MODULE_PATH,
loadChildren: () => import('./profile-page/profile-page-routes') loadChildren: () => import('./profile-page/profile-page-routes')
.then((m) => m.ROUTES), .then((m) => m.ROUTES),
providers: [provideSuggestionNotificationsState()], providers: [provideSuggestionNotificationsState()],
canActivate: [AuthenticatedGuard, EndUserAgreementCurrentUserGuard], canActivate: [authenticatedGuard, ...mapToCanActivate([EndUserAgreementCurrentUserGuard])],
}, },
{ {
path: PROCESS_MODULE_PATH, path: PROCESS_MODULE_PATH,
loadChildren: () => import('./process-page/process-page-routes') loadChildren: () => import('./process-page/process-page-routes')
.then((m) => m.ROUTES), .then((m) => m.ROUTES),
canActivate: [AuthenticatedGuard, EndUserAgreementCurrentUserGuard], canActivate: [authenticatedGuard, ...mapToCanActivate([EndUserAgreementCurrentUserGuard])],
}, },
{ {
path: SUGGESTION_MODULE_PATH, path: SUGGESTION_MODULE_PATH,
loadChildren: () => import('./suggestions-page/suggestions-page-routes') loadChildren: () => import('./suggestions-page/suggestions-page-routes')
.then((m) => m.ROUTES), .then((m) => m.ROUTES),
providers: [provideSuggestionNotificationsState()], providers: [provideSuggestionNotificationsState()],
canActivate: [AuthenticatedGuard, EndUserAgreementCurrentUserGuard], canActivate: [authenticatedGuard, ...mapToCanActivate([EndUserAgreementCurrentUserGuard])],
}, },
{ {
path: INFO_MODULE_PATH, path: INFO_MODULE_PATH,
@@ -229,7 +230,7 @@ export const APP_ROUTES: Route[] = [
{ {
path: REQUEST_COPY_MODULE_PATH, path: REQUEST_COPY_MODULE_PATH,
loadChildren: () => import('./request-copy/request-copy-routes').then((m) => m.ROUTES), loadChildren: () => import('./request-copy/request-copy-routes').then((m) => m.ROUTES),
canActivate: [EndUserAgreementCurrentUserGuard], canActivate: mapToCanActivate([EndUserAgreementCurrentUserGuard]),
}, },
{ {
path: FORBIDDEN_PATH, path: FORBIDDEN_PATH,
@@ -239,7 +240,7 @@ export const APP_ROUTES: Route[] = [
path: 'statistics', path: 'statistics',
loadChildren: () => import('./statistics-page/statistics-page-routes') loadChildren: () => import('./statistics-page/statistics-page-routes')
.then((m) => m.ROUTES), .then((m) => m.ROUTES),
canActivate: [EndUserAgreementCurrentUserGuard], canActivate: mapToCanActivate([EndUserAgreementCurrentUserGuard]),
}, },
{ {
path: HEALTH_PAGE_PATH, path: HEALTH_PAGE_PATH,
@@ -249,13 +250,13 @@ export const APP_ROUTES: Route[] = [
{ {
path: ACCESS_CONTROL_MODULE_PATH, path: ACCESS_CONTROL_MODULE_PATH,
loadChildren: () => import('./access-control/access-control-routes').then((m) => m.ROUTES), loadChildren: () => import('./access-control/access-control-routes').then((m) => m.ROUTES),
canActivate: [GroupAdministratorGuard, EndUserAgreementCurrentUserGuard], canActivate: mapToCanActivate([GroupAdministratorGuard, EndUserAgreementCurrentUserGuard]),
}, },
{ {
path: 'subscriptions', path: 'subscriptions',
loadChildren: () => import('./subscriptions-page/subscriptions-page-routes') loadChildren: () => import('./subscriptions-page/subscriptions-page-routes')
.then((m) => m.ROUTES), .then((m) => m.ROUTES),
canActivate: [AuthenticatedGuard], canActivate: [authenticatedGuard],
}, },
{ path: '**', pathMatch: 'full', component: ThemedPageNotFoundComponent }, { path: '**', pathMatch: 'full', component: ThemedPageNotFoundComponent },
], ],

View File

@@ -36,7 +36,7 @@ export function getBitstreamRequestACopyRoute(item, bitstream): { routerLink: st
} }
export const COAR_NOTIFY_SUPPORT = 'coar-notify-support'; export const COAR_NOTIFY_SUPPORT = 'coar-notify-support';
export const HOME_PAGE_PATH = 'admin'; export const HOME_PAGE_PATH = 'home';
export function getHomePageRoute() { export function getHomePageRoute() {
return `/${HOME_PAGE_PATH}`; return `/${HOME_PAGE_PATH}`;

View File

@@ -35,6 +35,7 @@ import {
NativeWindowRef, NativeWindowRef,
NativeWindowService, NativeWindowService,
} from './core/services/window.service'; } from './core/services/window.service';
import { ThemedRootComponent } from './root/themed-root.component';
import { HostWindowResizeAction } from './shared/host-window.actions'; import { HostWindowResizeAction } from './shared/host-window.actions';
import { HostWindowService } from './shared/host-window.service'; import { HostWindowService } from './shared/host-window.service';
import { MenuService } from './shared/menu/menu.service'; import { MenuService } from './shared/menu/menu.service';
@@ -84,7 +85,6 @@ describe('App component', () => {
}, },
}), }),
], ],
declarations: [AppComponent], // declare the test component
providers: [ providers: [
{ provide: NativeWindowService, useValue: new NativeWindowRef() }, { provide: NativeWindowService, useValue: new NativeWindowRef() },
{ provide: MetadataService, useValue: new MetadataServiceMock() }, { provide: MetadataService, useValue: new MetadataServiceMock() },
@@ -109,7 +109,13 @@ describe('App component', () => {
// waitForAsync beforeEach // waitForAsync beforeEach
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
return TestBed.configureTestingModule(getDefaultTestBedConf()); return TestBed.configureTestingModule(getDefaultTestBedConf()).overrideComponent(
AppComponent, {
remove: {
imports: [ ThemedRootComponent ],
},
},
);
})); }));
// synchronous beforeEach // synchronous beforeEach

View File

@@ -1,4 +1,5 @@
import { import {
AsyncPipe,
DOCUMENT, DOCUMENT,
isPlatformBrowser, isPlatformBrowser,
} from '@angular/common'; } from '@angular/common';
@@ -44,6 +45,7 @@ import {
NativeWindowService, NativeWindowService,
} from './core/services/window.service'; } from './core/services/window.service';
import { distinctNext } from './core/shared/distinct-next'; import { distinctNext } from './core/shared/distinct-next';
import { ThemedRootComponent } from './root/themed-root.component';
import { HostWindowResizeAction } from './shared/host-window.actions'; import { HostWindowResizeAction } from './shared/host-window.actions';
import { IdleModalComponent } from './shared/idle-modal/idle-modal.component'; import { IdleModalComponent } from './shared/idle-modal/idle-modal.component';
import { CSSVariableService } from './shared/sass-helper/css-variable.service'; 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', templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'], styleUrls: ['./app.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [
ThemedRootComponent,
AsyncPipe,
],
}) })
export class AppComponent implements OnInit, AfterViewInit { export class AppComponent implements OnInit, AfterViewInit {
notificationOptions; notificationOptions;

View File

@@ -1,14 +1,12 @@
import { import {
APP_BASE_HREF, APP_BASE_HREF,
CommonModule,
DOCUMENT, DOCUMENT,
} from '@angular/common'; } from '@angular/common';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { import {
HTTP_INTERCEPTORS, ApplicationConfig,
HttpClientModule, importProvidersFrom,
} from '@angular/common/http'; } from '@angular/core';
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { import {
NoPreloading, NoPreloading,
provideRouter, provideRouter,
@@ -29,7 +27,6 @@ import {
StoreModule, StoreModule,
USER_PROVIDED_META_REDUCERS, USER_PROVIDED_META_REDUCERS,
} from '@ngrx/store'; } from '@ngrx/store';
import { TranslateModule } from '@ngx-translate/core';
import { ScrollToModule } from '@nicky-lenaers/ngx-scroll-to'; import { ScrollToModule } from '@nicky-lenaers/ngx-scroll-to';
import { NgxMaskModule } from 'ngx-mask'; import { NgxMaskModule } from 'ngx-mask';
@@ -40,7 +37,6 @@ import {
import { StoreDevModules } from '../config/store/devtools'; import { StoreDevModules } from '../config/store/devtools';
import { environment } from '../environments/environment'; import { environment } from '../environments/environment';
import { EagerThemesModule } from '../themes/eager-themes.module'; import { EagerThemesModule } from '../themes/eager-themes.module';
import { AppComponent } from './app.component';
import { appEffects } from './app.effects'; import { appEffects } from './app.effects';
import { import {
appMetaReducers, appMetaReducers,
@@ -68,7 +64,6 @@ import { ClientCookieService } from './core/services/client-cookie.service';
import { ListableModule } from './core/shared/listable.module'; import { ListableModule } from './core/shared/listable.module';
import { XsrfInterceptor } from './core/xsrf/xsrf.interceptor'; import { XsrfInterceptor } from './core/xsrf/xsrf.interceptor';
import { RootModule } from './root.module'; 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 { 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 { METADATA_REPRESENTATION_COMPONENT_DECORATOR_MAP } from './shared/metadata-representation/metadata-representation.decorator';
import { import {
@@ -92,97 +87,79 @@ export function getMetaReducers(appConfig: AppConfig): MetaReducer<AppState>[] {
return appConfig.debug ? [...appMetaReducers, ...debugMetaReducers] : appMetaReducers; return appConfig.debug ? [...appMetaReducers, ...debugMetaReducers] : appMetaReducers;
} }
const IMPORTS = [ export const commonAppConfig: ApplicationConfig = {
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,
],
providers: [ 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(), provideCore(),
], ],
bootstrap: [AppComponent], };
})
export class AppModule {
/* Use models object so all decorators are actually called */
modelList = models; /* Use models object so all decorators are actually called */
workflowTasks = WORKFLOW_TASK_OPTION_DECORATOR_MAP; const modelList = models;
advancedWorfklowTasks = ADVANCED_WORKFLOW_TASK_OPTION_DECORATOR_MAP; const workflowTasks = WORKFLOW_TASK_OPTION_DECORATOR_MAP;
metadataRepresentations = METADATA_REPRESENTATION_COMPONENT_DECORATOR_MAP; const advancedWorfklowTasks = ADVANCED_WORKFLOW_TASK_OPTION_DECORATOR_MAP;
startsWithDecoratorMap = STARTS_WITH_DECORATOR_MAP; const metadataRepresentations = METADATA_REPRESENTATION_COMPONENT_DECORATOR_MAP;
browseByDecoratorMap = BROWSE_BY_DECORATOR_MAP; const startsWithDecoratorMap = STARTS_WITH_DECORATOR_MAP;
authMethodForDecoratorMap = AUTH_METHOD_FOR_DECORATOR_MAP; const browseByDecoratorMap = BROWSE_BY_DECORATOR_MAP;
} const authMethodForDecoratorMap = AUTH_METHOD_FOR_DECORATOR_MAP;

View File

@@ -1,17 +1,17 @@
import { Route } from '@angular/router'; import { Route } from '@angular/router';
import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; import { authenticatedGuard } from '../core/auth/authenticated.guard';
import { BitstreamBreadcrumbResolver } from '../core/breadcrumbs/bitstream-breadcrumb.resolver'; import { bitstreamBreadcrumbResolver } from '../core/breadcrumbs/bitstream-breadcrumb.resolver';
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
import { ResourcePolicyCreateComponent } from '../shared/resource-policies/create/resource-policy-create.component'; import { ResourcePolicyCreateComponent } from '../shared/resource-policies/create/resource-policy-create.component';
import { ResourcePolicyEditComponent } from '../shared/resource-policies/edit/resource-policy-edit.component'; import { ResourcePolicyEditComponent } from '../shared/resource-policies/edit/resource-policy-edit.component';
import { ResourcePolicyResolver } from '../shared/resource-policies/resolvers/resource-policy.resolver'; import { resourcePolicyResolver } from '../shared/resource-policies/resolvers/resource-policy.resolver';
import { ResourcePolicyTargetResolver } from '../shared/resource-policies/resolvers/resource-policy-target.resolver'; import { resourcePolicyTargetResolver } from '../shared/resource-policies/resolvers/resource-policy-target.resolver';
import { BitstreamAuthorizationsComponent } from './bitstream-authorizations/bitstream-authorizations.component'; import { BitstreamAuthorizationsComponent } from './bitstream-authorizations/bitstream-authorizations.component';
import { BitstreamDownloadPageComponent } from './bitstream-download-page/bitstream-download-page.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 { 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_PATH = ':id/edit';
const EDIT_BITSTREAM_AUTHORIZATIONS_PATH = ':id/authorizations'; const EDIT_BITSTREAM_AUTHORIZATIONS_PATH = ':id/authorizations';
@@ -25,7 +25,7 @@ export const ROUTES: Route[] = [
path: 'handle/:prefix/:suffix/:filename', path: 'handle/:prefix/:suffix/:filename',
component: BitstreamDownloadPageComponent, component: BitstreamDownloadPageComponent,
resolve: { resolve: {
bitstream: LegacyBitstreamUrlResolver, bitstream: legacyBitstreamUrlResolver,
}, },
}, },
{ {
@@ -33,7 +33,7 @@ export const ROUTES: Route[] = [
path: ':prefix/:suffix/:sequence_id/:filename', path: ':prefix/:suffix/:sequence_id/:filename',
component: BitstreamDownloadPageComponent, component: BitstreamDownloadPageComponent,
resolve: { resolve: {
bitstream: LegacyBitstreamUrlResolver, bitstream: legacyBitstreamUrlResolver,
}, },
}, },
{ {
@@ -41,17 +41,17 @@ export const ROUTES: Route[] = [
path: ':id/download', path: ':id/download',
component: BitstreamDownloadPageComponent, component: BitstreamDownloadPageComponent,
resolve: { resolve: {
bitstream: BitstreamPageResolver, bitstream: bitstreamPageResolver,
}, },
}, },
{ {
path: EDIT_BITSTREAM_PATH, path: EDIT_BITSTREAM_PATH,
component: ThemedEditBitstreamPageComponent, component: ThemedEditBitstreamPageComponent,
resolve: { resolve: {
bitstream: BitstreamPageResolver, bitstream: bitstreamPageResolver,
breadcrumb: BitstreamBreadcrumbResolver, breadcrumb: bitstreamBreadcrumbResolver,
}, },
canActivate: [AuthenticatedGuard], canActivate: [authenticatedGuard],
}, },
{ {
path: EDIT_BITSTREAM_AUTHORIZATIONS_PATH, path: EDIT_BITSTREAM_AUTHORIZATIONS_PATH,
@@ -59,7 +59,7 @@ export const ROUTES: Route[] = [
{ {
path: 'create', path: 'create',
resolve: { resolve: {
resourcePolicyTarget: ResourcePolicyTargetResolver, resourcePolicyTarget: resourcePolicyTargetResolver,
}, },
component: ResourcePolicyCreateComponent, component: ResourcePolicyCreateComponent,
data: { title: 'resource-policies.create.page.title', showBreadcrumbs: true }, data: { title: 'resource-policies.create.page.title', showBreadcrumbs: true },
@@ -67,8 +67,8 @@ export const ROUTES: Route[] = [
{ {
path: 'edit', path: 'edit',
resolve: { resolve: {
breadcrumb: I18nBreadcrumbResolver, breadcrumb: i18nBreadcrumbResolver,
resourcePolicy: ResourcePolicyResolver, resourcePolicy: resourcePolicyResolver,
}, },
component: ResourcePolicyEditComponent, component: ResourcePolicyEditComponent,
data: { breadcrumbKey: 'item.edit', title: 'resource-policies.edit.page.title', showBreadcrumbs: true }, data: { breadcrumbKey: 'item.edit', title: 'resource-policies.edit.page.title', showBreadcrumbs: true },
@@ -76,8 +76,8 @@ export const ROUTES: Route[] = [
{ {
path: '', path: '',
resolve: { resolve: {
bitstream: BitstreamPageResolver, bitstream: bitstreamPageResolver,
breadcrumb: BitstreamBreadcrumbResolver, breadcrumb: bitstreamBreadcrumbResolver,
}, },
component: BitstreamAuthorizationsComponent, component: BitstreamAuthorizationsComponent,
data: { title: 'bitstream.edit.authorizations.title', showBreadcrumbs: true }, data: { title: 'bitstream.edit.authorizations.title', showBreadcrumbs: true },

View File

@@ -1,7 +1,7 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
Resolve, ResolveFn,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } from '@angular/router';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
@@ -25,32 +25,20 @@ export const BITSTREAM_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig<Bitstream>[] = [
]; ];
/** /**
* 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<<RemoteData<Item>> Emits the found bitstream based on the parameters in the current route,
* or an error if something went wrong
*/ */
@Injectable({ providedIn: 'root' }) export const bitstreamPageResolver: ResolveFn<RemoteData<Bitstream>> = (
export class BitstreamPageResolver implements Resolve<RemoteData<Bitstream>> { route: ActivatedRouteSnapshot,
constructor(private bitstreamService: BitstreamDataService) { state: RouterStateSnapshot,
} bitstreamService: BitstreamDataService = inject(BitstreamDataService),
): Observable<RemoteData<Bitstream>> => {
/** return bitstreamService.findById(route.params.id, true, false, ...BITSTREAM_PAGE_LINKS_TO_FOLLOW)
* Method for resolving a bitstream based on the parameters in the current route .pipe(
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot getFirstCompletedRemoteData(),
* @param {RouterStateSnapshot} state The current RouterStateSnapshot );
* @returns Observable<<RemoteData<Item>> 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<RemoteData<Bitstream>> {
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<Bitstream>[] {
return BITSTREAM_PAGE_LINKS_TO_FOLLOW;
}
}

View File

@@ -4,10 +4,10 @@ import { TestScheduler } from 'rxjs/testing';
import { BitstreamDataService } from '../core/data/bitstream-data.service'; import { BitstreamDataService } from '../core/data/bitstream-data.service';
import { RemoteData } from '../core/data/remote-data'; import { RemoteData } from '../core/data/remote-data';
import { RequestEntryState } from '../core/data/request-entry-state.model'; 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`, () => { describe(`legacyBitstreamUrlResolver`, () => {
let resolver: LegacyBitstreamUrlResolver; let resolver: any;
let bitstreamDataService: BitstreamDataService; let bitstreamDataService: BitstreamDataService;
let testScheduler; let testScheduler;
let remoteDataMocks; let remoteDataMocks;
@@ -33,7 +33,7 @@ describe(`LegacyBitstreamUrlResolver`, () => {
bitstreamDataService = { bitstreamDataService = {
findByItemHandle: () => undefined, findByItemHandle: () => undefined,
} as any; } as any;
resolver = new LegacyBitstreamUrlResolver(bitstreamDataService); resolver = legacyBitstreamUrlResolver;
}); });
describe(`resolve`, () => { describe(`resolve`, () => {
@@ -51,7 +51,7 @@ describe(`LegacyBitstreamUrlResolver`, () => {
}); });
it(`should call findByItemHandle with the handle, sequence id, and filename from the route`, () => { it(`should call findByItemHandle with the handle, sequence id, and filename from the route`, () => {
testScheduler.run(() => { testScheduler.run(() => {
resolver.resolve(route, state); resolver(route, state, bitstreamDataService);
expect(bitstreamDataService.findByItemHandle).toHaveBeenCalledWith( expect(bitstreamDataService.findByItemHandle).toHaveBeenCalledWith(
`${route.params.prefix}/${route.params.suffix}`, `${route.params.prefix}/${route.params.suffix}`,
route.params.sequence_id, 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`, () => { it(`should call findByItemHandle with the handle and filename from the route, and the sequence ID from the queryParams`, () => {
testScheduler.run(() => { testScheduler.run(() => {
resolver.resolve(route, state); resolver(route, state, bitstreamDataService);
expect(bitstreamDataService.findByItemHandle).toHaveBeenCalledWith( expect(bitstreamDataService.findByItemHandle).toHaveBeenCalledWith(
`${route.params.prefix}/${route.params.suffix}`, `${route.params.prefix}/${route.params.suffix}`,
route.queryParams.sequenceId, route.queryParams.sequenceId,
@@ -100,7 +100,7 @@ describe(`LegacyBitstreamUrlResolver`, () => {
}); });
it(`should call findByItemHandle with the handle, and filename from the route`, () => { it(`should call findByItemHandle with the handle, and filename from the route`, () => {
testScheduler.run(() => { testScheduler.run(() => {
resolver.resolve(route, state); resolver(route, state, bitstreamDataService);
expect(bitstreamDataService.findByItemHandle).toHaveBeenCalledWith( expect(bitstreamDataService.findByItemHandle).toHaveBeenCalledWith(
`${route.params.prefix}/${route.params.suffix}`, `${route.params.prefix}/${route.params.suffix}`,
undefined, undefined,
@@ -123,7 +123,7 @@ describe(`LegacyBitstreamUrlResolver`, () => {
c: remoteDataMocks.Error, c: remoteDataMocks.Error,
}; };
expectObservable(resolver.resolve(route, state)).toBe(expected, values); expectObservable(resolver(route, state, bitstreamDataService)).toBe(expected, values);
}); });
}); });
it(`...succeeded`, () => { it(`...succeeded`, () => {
@@ -138,7 +138,7 @@ describe(`LegacyBitstreamUrlResolver`, () => {
c: remoteDataMocks.Success, c: remoteDataMocks.Success,
}; };
expectObservable(resolver.resolve(route, state)).toBe(expected, values); expectObservable(resolver(route, state, bitstreamDataService)).toBe(expected, values);
}); });
}); });
}); });

View File

@@ -1,7 +1,7 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
Resolve, ResolveFn,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } from '@angular/router';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
@@ -13,41 +13,34 @@ import { getFirstCompletedRemoteData } from '../core/shared/operators';
import { hasNoValue } from '../shared/empty.util'; 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<<RemoteData<Item>> Emits the found bitstream based on the parameters in
* current route, or an error if something went wrong
*/ */
@Injectable({ export const legacyBitstreamUrlResolver: ResolveFn<RemoteData<Bitstream>> = (
providedIn: 'root', route: ActivatedRouteSnapshot,
}) state: RouterStateSnapshot,
export class LegacyBitstreamUrlResolver implements Resolve<RemoteData<Bitstream>> { bitstreamDataService: BitstreamDataService = inject(BitstreamDataService),
constructor(protected bitstreamDataService: BitstreamDataService) { ): Observable<RemoteData<Bitstream>> => {
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 bitstreamDataService.findByItemHandle(
* Resolve a bitstream based on the handle of the item, and the sequence id or the filename of the `${prefix}/${suffix}`,
* bitstream sequenceId,
* filename,
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot ).pipe(
* @param {RouterStateSnapshot} state The current RouterStateSnapshot getFirstCompletedRemoteData(),
* @returns Observable<<RemoteData<Item>> Emits the found bitstream based on the parameters in );
* current route, or an error if something went wrong };
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot):
Observable<RemoteData<Bitstream>> {
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(),
);
}
}

View File

@@ -1,6 +1,7 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
ResolveFn,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } from '@angular/router';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
@@ -19,30 +20,28 @@ import {
import { hasValue } from '../shared/empty.util'; 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 const browseByDSOBreadcrumbResolver: ResolveFn<BreadcrumbConfig<Community | Collection>> = (
export class BrowseByDSOBreadcrumbResolver { route: ActivatedRouteSnapshot,
constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: DSpaceObjectDataService) { state: RouterStateSnapshot,
breadcrumbService: DSOBreadcrumbsService = inject(DSOBreadcrumbsService),
dataService: DSpaceObjectDataService = inject(DSpaceObjectDataService),
): Observable<BreadcrumbConfig<Community | Collection>> => {
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) };
}),
);
} }
return undefined;
/** };
* 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<BreadcrumbConfig<Community | Collection>> {
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;
}
}

View File

@@ -6,12 +6,12 @@ import {
createSuccessfulRemoteDataObject$, createSuccessfulRemoteDataObject$,
} from '../shared/remote-data.utils'; } from '../shared/remote-data.utils';
import { RouterStub } from '../shared/testing/router.stub'; 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'; import { BrowseByDataType } from './browse-by-switcher/browse-by-data-type';
describe('BrowseByGuard', () => { describe('browseByGuard', () => {
describe('canActivate', () => { describe('canActivate', () => {
let guard: BrowseByGuard; let guard: any;
let translateService: any; let translateService: any;
let browseDefinitionService: any; let browseDefinitionService: any;
let router: any; let router: any;
@@ -35,7 +35,7 @@ describe('BrowseByGuard', () => {
router = new RouterStub() as any; 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', () => { it('should return true, and sets up the data correctly, with a scope and value', () => {
@@ -53,7 +53,7 @@ describe('BrowseByGuard', () => {
value, value,
}, },
}; };
guard.canActivate(scopedRoute as any, undefined) guard(scopedRoute as any, undefined, browseDefinitionService, router, translateService)
.pipe(first()) .pipe(first())
.subscribe( .subscribe(
(canActivate) => { (canActivate) => {
@@ -86,7 +86,7 @@ describe('BrowseByGuard', () => {
}, },
}; };
guard.canActivate(scopedNoValueRoute as any, undefined) guard(scopedNoValueRoute, undefined, browseDefinitionService, router, translateService)
.pipe(first()) .pipe(first())
.subscribe( .subscribe(
(canActivate) => { (canActivate) => {
@@ -123,7 +123,7 @@ describe('BrowseByGuard', () => {
}, },
}; };
guard.canActivate(scopedNoValueRoute as any, undefined).pipe( guard(scopedNoValueRoute as any, undefined, browseDefinitionService, router, translateService).pipe(
first(), first(),
).subscribe((canActivate) => { ).subscribe((canActivate) => {
const result = { const result = {
@@ -154,7 +154,8 @@ describe('BrowseByGuard', () => {
value, value,
}, },
}; };
guard.canActivate(route as any, undefined)
guard(route as any, undefined, browseDefinitionService, router, translateService)
.pipe(first()) .pipe(first())
.subscribe( .subscribe(
(canActivate) => { (canActivate) => {
@@ -189,7 +190,8 @@ describe('BrowseByGuard', () => {
value, value,
}, },
}; };
guard.canActivate(scopedRoute as any, undefined)
guard(scopedRoute as any, undefined, browseDefinitionService, router, translateService)
.pipe(first()) .pipe(first())
.subscribe((canActivate) => { .subscribe((canActivate) => {
expect(router.navigate).toHaveBeenCalled(); expect(router.navigate).toHaveBeenCalled();

View File

@@ -1,7 +1,7 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
CanActivate, CanActivateFn,
Data, Data,
Router, Router,
RouterStateSnapshot, RouterStateSnapshot,
@@ -26,55 +26,47 @@ import {
hasValue, hasValue,
} from '../shared/empty.util'; } from '../shared/empty.util';
@Injectable({ providedIn: 'root' }) export const browseByGuard: CanActivateFn = (
/** route: ActivatedRouteSnapshot,
* A guard taking care of the correct route.data being set for the Browse-By components state: RouterStateSnapshot,
*/ browseDefinitionService: BrowseDefinitionDataService = inject(BrowseDefinitionDataService),
export class BrowseByGuard implements CanActivate { router: Router = inject(Router),
translate: TranslateService = inject(TranslateService),
constructor( ): Observable<boolean> => {
protected translate: TranslateService, const title = route.data.title;
protected browseDefinitionService: BrowseDefinitionDataService, const id = route.params.id || route.queryParams.id || route.data.id;
protected router: Router, let browseDefinition$: Observable<BrowseDefinition | undefined>;
) { if (hasNoValue(route.data.browseDefinition) && hasValue(id)) {
} browseDefinition$ = browseDefinitionService.findById(id).pipe(
getFirstCompletedRemoteData(),
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> { map((browseDefinitionRD: RemoteData<BrowseDefinition>) => browseDefinitionRD.payload),
const title = route.data.title;
const id = route.params.id || route.queryParams.id || route.data.id;
let browseDefinition$: Observable<BrowseDefinition | undefined>;
if (hasNoValue(route.data.browseDefinition) && hasValue(id)) {
browseDefinition$ = this.browseDefinitionService.findById(id).pipe(
getFirstCompletedRemoteData(),
map((browseDefinitionRD: RemoteData<BrowseDefinition>) => 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);
}
}),
); );
} 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 { function createData(title: string, id: string, browseDefinition: BrowseDefinition, field: string, value: string, route: ActivatedRouteSnapshot, scope: string): Data {
return Object.assign({}, route.data, { return Object.assign({}, route.data, {
title: title, title: title,
id: id, id: id,
browseDefinition: browseDefinition, browseDefinition: browseDefinition,
field: field, field: field,
value: hasValue(value) ? `"${value}"` : '', value: hasValue(value) ? `"${value}"` : '',
scope: scope, scope: scope,
}); });
}
} }

View File

@@ -1,32 +1,23 @@
import { Injectable } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
ResolveFn,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } from '@angular/router';
import { BreadcrumbConfig } from '../breadcrumbs/breadcrumb/breadcrumb-config.model'; import { BreadcrumbConfig } from '../breadcrumbs/breadcrumb/breadcrumb-config.model';
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service';
/** /**
* This class resolves a BreadcrumbConfig object with an i18n key string for a route * Method for resolving a browse-by i18n breadcrumb configuration object
* It adds the metadata field of the current browse-by page * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
* @returns BreadcrumbConfig object for a browse-by page
*/ */
@Injectable({ providedIn: 'root' }) export const browseByI18nBreadcrumbResolver: ResolveFn<BreadcrumbConfig<string>> = (
export class BrowseByI18nBreadcrumbResolver extends I18nBreadcrumbResolver { route: ActivatedRouteSnapshot,
constructor(protected breadcrumbService: I18nBreadcrumbsService) { state: RouterStateSnapshot,
super(breadcrumbService); ): BreadcrumbConfig<string> => {
} const extendedBreadcrumbKey = route.data.breadcrumbKey + '.' + route.params.id;
route.data = Object.assign({}, route.data, { breadcrumbKey: extendedBreadcrumbKey });
/** return i18nBreadcrumbResolver(route, state) as BreadcrumbConfig<string>;
* 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<string> {
const extendedBreadcrumbKey = route.data.breadcrumbKey + '.' + route.params.id;
route.data = Object.assign({}, route.data, { breadcrumbKey: extendedBreadcrumbKey });
return super.resolve(route, state);
}
}

View File

@@ -1,24 +1,24 @@
import { Route } from '@angular/router'; import { Route } from '@angular/router';
import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver'; import { dsoEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver';
import { BrowseByDSOBreadcrumbResolver } from './browse-by-dso-breadcrumb.resolver'; import { browseByDSOBreadcrumbResolver } from './browse-by-dso-breadcrumb.resolver';
import { BrowseByGuard } from './browse-by-guard'; import { browseByGuard } from './browse-by-guard';
import { BrowseByI18nBreadcrumbResolver } from './browse-by-i18n-breadcrumb.resolver'; import { browseByI18nBreadcrumbResolver } from './browse-by-i18n-breadcrumb.resolver';
import { BrowseByPageComponent } from './browse-by-page/browse-by-page.component'; import { BrowseByPageComponent } from './browse-by-page/browse-by-page.component';
export const ROUTES: Route[] = [ export const ROUTES: Route[] = [
{ {
path: '', path: '',
resolve: { resolve: {
breadcrumb: BrowseByDSOBreadcrumbResolver, breadcrumb: browseByDSOBreadcrumbResolver,
menu: DSOEditMenuResolver, menu: dsoEditMenuResolver,
}, },
children: [ children: [
{ {
path: ':id', path: ':id',
component: BrowseByPageComponent, component: BrowseByPageComponent,
canActivate: [BrowseByGuard], canActivate: [browseByGuard],
resolve: { breadcrumb: BrowseByI18nBreadcrumbResolver }, resolve: { breadcrumb: browseByI18nBreadcrumbResolver },
data: { title: 'browse.title.page', breadcrumbKey: 'browse.metadata' }, data: { title: 'browse.title.page', breadcrumbKey: 'browse.metadata' },
}, },
], ],

View File

@@ -1,6 +1,7 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
ResolveFn,
Router, Router,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } 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 { 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 { DsoPageSingleFeatureGuard } from '../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard';
import { FeatureID } from '../core/data/feature-authorization/feature-id'; import { FeatureID } from '../core/data/feature-authorization/feature-id';
import { RemoteData } from '../core/data/remote-data';
import { Collection } from '../core/shared/collection.model'; import { Collection } from '../core/shared/collection.model';
import { CollectionPageResolver } from './collection-page.resolver'; import { collectionPageResolver } from './collection-page.resolver';
@Injectable({ @Injectable({
providedIn: 'root', 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 * Guard for preventing unauthorized access to certain {@link Collection} pages requiring administrator rights
*/ */
export class CollectionPageAdministratorGuard extends DsoPageSingleFeatureGuard<Collection> { export class CollectionPageAdministratorGuard extends DsoPageSingleFeatureGuard<Collection> {
constructor(protected resolver: CollectionPageResolver,
protected authorizationService: AuthorizationDataService, protected resolver: ResolveFn<RemoteData<Collection>> = collectionPageResolver;
constructor(protected authorizationService: AuthorizationDataService,
protected router: Router, protected router: Router,
protected authService: AuthService) { protected authService: AuthService) {
super(resolver, authorizationService, router, authService); super(authorizationService, router, authService);
} }
/** /**

View File

@@ -1,17 +1,20 @@
import { Route } from '@angular/router'; import {
mapToCanActivate,
Route,
} from '@angular/router';
import { BrowseByGuard } from '../browse-by/browse-by-guard'; import { browseByGuard } from '../browse-by/browse-by-guard';
import { BrowseByI18nBreadcrumbResolver } from '../browse-by/browse-by-i18n-breadcrumb.resolver'; import { browseByI18nBreadcrumbResolver } from '../browse-by/browse-by-i18n-breadcrumb.resolver';
import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; import { authenticatedGuard } from '../core/auth/authenticated.guard';
import { CollectionBreadcrumbResolver } from '../core/breadcrumbs/collection-breadcrumb.resolver'; import { collectionBreadcrumbResolver } from '../core/breadcrumbs/collection-breadcrumb.resolver';
import { CommunityBreadcrumbResolver } from '../core/breadcrumbs/community-breadcrumb.resolver'; import { communityBreadcrumbResolver } from '../core/breadcrumbs/community-breadcrumb.resolver';
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
import { ComcolBrowseByComponent } from '../shared/comcol/sections/comcol-browse-by/comcol-browse-by.component'; 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 { 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 { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
import { MenuItemType } from '../shared/menu/menu-item-type.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 { CollectionPageAdministratorGuard } from './collection-page-administrator.guard';
import { import {
COLLECTION_CREATE_PATH, COLLECTION_CREATE_PATH,
@@ -19,22 +22,22 @@ import {
ITEMTEMPLATE_PATH, ITEMTEMPLATE_PATH,
} from './collection-page-routing-paths'; } from './collection-page-routing-paths';
import { CreateCollectionPageComponent } from './create-collection-page/create-collection-page.component'; 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 { 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 { ThemedEditItemTemplatePageComponent } from './edit-item-template-page/themed-edit-item-template-page.component';
import { ThemedCollectionPageComponent } from './themed-collection-page.component'; import { ThemedCollectionPageComponent } from './themed-collection-page.component';
export const ROUTES: Route[] = [ export const ROUTES: Route[] = [
{ {
path: COLLECTION_CREATE_PATH, path: COLLECTION_CREATE_PATH,
canActivate: [AuthenticatedGuard, CreateCollectionPageGuard], canActivate: [authenticatedGuard, createCollectionPageGuard],
children: [ children: [
{ {
path: '', path: '',
component: CreateCollectionPageComponent, component: CreateCollectionPageComponent,
resolve: { resolve: {
breadcrumb: I18nBreadcrumbResolver, breadcrumb: i18nBreadcrumbResolver,
}, },
data: { data: {
breadcrumbKey: 'collection.create', breadcrumbKey: 'collection.create',
@@ -45,15 +48,15 @@ export const ROUTES: Route[] = [
breadcrumbQueryParam: 'parent', breadcrumbQueryParam: 'parent',
}, },
resolve: { resolve: {
breadcrumb: CommunityBreadcrumbResolver, breadcrumb: communityBreadcrumbResolver,
}, },
}, },
{ {
path: ':id', path: ':id',
resolve: { resolve: {
dso: CollectionPageResolver, dso: collectionPageResolver,
breadcrumb: CollectionBreadcrumbResolver, breadcrumb: collectionBreadcrumbResolver,
menu: DSOEditMenuResolver, menu: dsoEditMenuResolver,
}, },
runGuardsAndResolvers: 'always', runGuardsAndResolvers: 'always',
children: [ children: [
@@ -61,21 +64,21 @@ export const ROUTES: Route[] = [
path: COLLECTION_EDIT_PATH, path: COLLECTION_EDIT_PATH,
loadChildren: () => import('./edit-collection-page/edit-collection-page-routes') loadChildren: () => import('./edit-collection-page/edit-collection-page-routes')
.then((m) => m.ROUTES), .then((m) => m.ROUTES),
canActivate: [CollectionPageAdministratorGuard], canActivate: mapToCanActivate([CollectionPageAdministratorGuard]),
}, },
{ {
path: 'delete', path: 'delete',
pathMatch: 'full', pathMatch: 'full',
component: DeleteCollectionPageComponent, component: DeleteCollectionPageComponent,
canActivate: [AuthenticatedGuard], canActivate: [authenticatedGuard],
}, },
{ {
path: ITEMTEMPLATE_PATH, path: ITEMTEMPLATE_PATH,
component: ThemedEditItemTemplatePageComponent, component: ThemedEditItemTemplatePageComponent,
canActivate: [AuthenticatedGuard], canActivate: [authenticatedGuard],
resolve: { resolve: {
item: ItemTemplatePageResolver, item: itemTemplatePageResolver,
breadcrumb: I18nBreadcrumbResolver, breadcrumb: i18nBreadcrumbResolver,
}, },
data: { title: 'collection.edit.template.title', breadcrumbKey: 'collection.edit.template' }, data: { title: 'collection.edit.template.title', breadcrumbKey: 'collection.edit.template' },
}, },
@@ -92,9 +95,9 @@ export const ROUTES: Route[] = [
path: 'browse/:id', path: 'browse/:id',
pathMatch: 'full', pathMatch: 'full',
component: ComcolBrowseByComponent, component: ComcolBrowseByComponent,
canActivate: [BrowseByGuard], canActivate: [browseByGuard],
resolve: { resolve: {
breadcrumb: BrowseByI18nBreadcrumbResolver, breadcrumb: browseByI18nBreadcrumbResolver,
}, },
data: { breadcrumbKey: 'browse.metadata' }, data: { breadcrumbKey: 'browse.metadata' },
}, },

View File

@@ -1,11 +1,12 @@
import { Observable } from 'rxjs';
import { first } from 'rxjs/operators'; import { first } from 'rxjs/operators';
import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; 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', () => { describe('resolve', () => {
let resolver: CollectionPageResolver; let resolver: any;
let collectionService: any; let collectionService: any;
let store: any; let store: any;
const uuid = '1234-65487-12354-1235'; const uuid = '1234-65487-12354-1235';
@@ -17,12 +18,11 @@ describe('CollectionPageResolver', () => {
store = jasmine.createSpyObj('store', { store = jasmine.createSpyObj('store', {
dispatch: {}, dispatch: {},
}); });
resolver = new CollectionPageResolver(collectionService, store); resolver = collectionPageResolver;
}); });
it('should resolve a collection with the correct id', (done) => { it('should resolve a collection 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, collectionService, store) as Observable<any>).pipe(first())
.pipe(first())
.subscribe( .subscribe(
(resolved) => { (resolved) => {
expect(resolved.payload.id).toEqual(uuid); expect(resolved.payload.id).toEqual(uuid);

View File

@@ -1,12 +1,13 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
Resolve, ResolveFn,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } from '@angular/router';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { AppState } from '../app.reducer';
import { CollectionDataService } from '../core/data/collection-data.service'; import { CollectionDataService } from '../core/data/collection-data.service';
import { RemoteData } from '../core/data/remote-data'; import { RemoteData } from '../core/data/remote-data';
import { ResolvedAction } from '../core/resolving/resolver.actions'; import { ResolvedAction } from '../core/resolving/resolver.actions';
@@ -29,37 +30,32 @@ export const COLLECTION_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig<Collection>[] = [
]; ];
/** /**
* 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<<RemoteData<Collection>> Emits the found collection based on the parameters in the current route,
* or an error if something went wrong
*/ */
@Injectable({ providedIn: 'root' }) export const collectionPageResolver: ResolveFn<RemoteData<Collection>> = (
export class CollectionPageResolver implements Resolve<RemoteData<Collection>> { route: ActivatedRouteSnapshot,
constructor( state: RouterStateSnapshot,
private collectionService: CollectionDataService, collectionService: CollectionDataService = inject(CollectionDataService),
private store: Store<any>, store: Store<AppState> = inject(Store<AppState>),
) { ): Observable<RemoteData<Collection>> => {
} const collectionRD$ = collectionService.findById(
route.params.id,
true,
false,
...COLLECTION_PAGE_LINKS_TO_FOLLOW,
).pipe(
getFirstCompletedRemoteData(),
);
/** collectionRD$.subscribe((collectionRD: RemoteData<Collection>) => {
* Method for resolving a collection based on the parameters in the current route store.dispatch(new ResolvedAction(state.url, collectionRD.payload));
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot });
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
* @returns Observable<<RemoteData<Collection>> 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<RemoteData<Collection>> {
const collectionRD$ = this.collectionService.findById(
route.params.id,
true,
false,
...COLLECTION_PAGE_LINKS_TO_FOLLOW,
).pipe(
getFirstCompletedRemoteData(),
);
collectionRD$.subscribe((collectionRD: RemoteData<Collection>) => { return collectionRD$;
this.store.dispatch(new ResolvedAction(state.url, collectionRD.payload)); };
});
return collectionRD$;
}
}

View File

@@ -6,11 +6,11 @@ import {
createFailedRemoteDataObject$, createFailedRemoteDataObject$,
createSuccessfulRemoteDataObject$, createSuccessfulRemoteDataObject$,
} from '../../shared/remote-data.utils'; } 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', () => { describe('canActivate', () => {
let guard: CreateCollectionPageGuard; let guard: any;
let router; let router;
let communityDataServiceStub: any; let communityDataServiceStub: any;
@@ -28,11 +28,11 @@ describe('CreateCollectionPageGuard', () => {
}; };
router = new RouterMock(); router = new RouterMock();
guard = new CreateCollectionPageGuard(router, communityDataServiceStub); guard = createCollectionPageGuard;
}); });
it('should return true when the parent ID resolves to a community', () => { 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()) .pipe(first())
.subscribe( .subscribe(
(canActivate) => (canActivate) =>
@@ -41,7 +41,7 @@ describe('CreateCollectionPageGuard', () => {
}); });
it('should return false when no parent ID has been provided', () => { 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()) .pipe(first())
.subscribe( .subscribe(
(canActivate) => (canActivate) =>
@@ -50,7 +50,7 @@ describe('CreateCollectionPageGuard', () => {
}); });
it('should return false when the parent ID does not resolve to a community', () => { 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()) .pipe(first())
.subscribe( .subscribe(
(canActivate) => (canActivate) =>
@@ -59,7 +59,7 @@ describe('CreateCollectionPageGuard', () => {
}); });
it('should return false when the parent ID resolves to an error response', () => { 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()) .pipe(first())
.subscribe( .subscribe(
(canActivate) => (canActivate) =>

View File

@@ -1,7 +1,7 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
CanActivate, CanActivateFn,
Router, Router,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } from '@angular/router';
@@ -24,34 +24,29 @@ import {
} from '../../shared/empty.util'; } from '../../shared/empty.util';
/** /**
* Prevent creation of a collection without a parent community provided * True when either a parent ID query parameter has been provided and the parent ID resolves to a valid parent community
* @class CreateCollectionPageGuard * Reroutes to a 404 page when the page cannot be activated
*/ */
@Injectable({ providedIn: 'root' }) export const createCollectionPageGuard: CanActivateFn = (
export class CreateCollectionPageGuard implements CanActivate { route: ActivatedRouteSnapshot,
public constructor(private router: Router, private communityService: CommunityDataService) { state: RouterStateSnapshot,
communityService: CommunityDataService = inject(CommunityDataService),
router: Router = inject(Router),
): Observable<boolean> => {
const parentID = route.queryParams.parent;
if (hasNoValue(parentID)) {
router.navigate(['/404']);
return observableOf(false);
} }
return communityService.findById(parentID)
.pipe(
getFirstCompletedRemoteData(),
map((communityRD: RemoteData<Community>) => 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<boolean> {
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<Community>) => hasValue(communityRD) && communityRD.hasSucceeded && hasValue(communityRD.payload)),
tap((isValid: boolean) => {
if (!isValid) {
this.router.navigate(['/404']);
}
}),
);
}
}

View File

@@ -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 { CollectionAdministratorGuard } from '../../core/data/feature-authorization/feature-authorization-guard/collection-administrator.guard';
import { ResourcePolicyCreateComponent } from '../../shared/resource-policies/create/resource-policy-create.component'; import { ResourcePolicyCreateComponent } from '../../shared/resource-policies/create/resource-policy-create.component';
import { ResourcePolicyEditComponent } from '../../shared/resource-policies/edit/resource-policy-edit.component'; import { ResourcePolicyEditComponent } from '../../shared/resource-policies/edit/resource-policy-edit.component';
import { ResourcePolicyResolver } from '../../shared/resource-policies/resolvers/resource-policy.resolver'; import { resourcePolicyResolver } from '../../shared/resource-policies/resolvers/resource-policy.resolver';
import { ResourcePolicyTargetResolver } from '../../shared/resource-policies/resolvers/resource-policy-target.resolver'; import { resourcePolicyTargetResolver } from '../../shared/resource-policies/resolvers/resource-policy-target.resolver';
import { CollectionItemMapperComponent } from '../collection-item-mapper/collection-item-mapper.component'; import { CollectionItemMapperComponent } from '../collection-item-mapper/collection-item-mapper.component';
import { CollectionAccessControlComponent } from './collection-access-control/collection-access-control.component'; import { CollectionAccessControlComponent } from './collection-access-control/collection-access-control.component';
import { CollectionAuthorizationsComponent } from './collection-authorizations/collection-authorizations.component'; import { CollectionAuthorizationsComponent } from './collection-authorizations/collection-authorizations.component';
@@ -23,11 +26,11 @@ export const ROUTES: Route[] = [
{ {
path: '', path: '',
resolve: { resolve: {
breadcrumb: I18nBreadcrumbResolver, breadcrumb: i18nBreadcrumbResolver,
}, },
data: { breadcrumbKey: 'collection.edit' }, data: { breadcrumbKey: 'collection.edit' },
component: EditCollectionPageComponent, component: EditCollectionPageComponent,
canActivate: [CollectionAdministratorGuard], canActivate: mapToCanActivate([CollectionAdministratorGuard]),
children: [ children: [
{ {
path: '', path: '',
@@ -70,7 +73,7 @@ export const ROUTES: Route[] = [
{ {
path: 'create', path: 'create',
resolve: { resolve: {
resourcePolicyTarget: ResourcePolicyTargetResolver, resourcePolicyTarget: resourcePolicyTargetResolver,
}, },
component: ResourcePolicyCreateComponent, component: ResourcePolicyCreateComponent,
data: { title: 'resource-policies.create.page.title' }, data: { title: 'resource-policies.create.page.title' },
@@ -78,7 +81,7 @@ export const ROUTES: Route[] = [
{ {
path: 'edit', path: 'edit',
resolve: { resolve: {
resourcePolicy: ResourcePolicyResolver, resourcePolicy: resourcePolicyResolver,
}, },
component: ResourcePolicyEditComponent, component: ResourcePolicyEditComponent,
data: { title: 'resource-policies.edit.page.title' }, data: { title: 'resource-policies.edit.page.title' },

View File

@@ -1,27 +1,24 @@
import { Observable } from 'rxjs';
import { first } from 'rxjs/operators'; 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 { 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', () => { describe('resolve', () => {
let resolver: ItemTemplatePageResolver; let resolver: any;
let itemTemplateService: any; let itemTemplateService: any;
let dsoNameService: DSONameServiceMock;
const uuid = '1234-65487-12354-1235'; const uuid = '1234-65487-12354-1235';
beforeEach(() => { beforeEach(() => {
itemTemplateService = { itemTemplateService = {
findByCollectionID: (id: string) => createSuccessfulRemoteDataObject$({ id }), findByCollectionID: (id: string) => createSuccessfulRemoteDataObject$({ id }),
}; };
dsoNameService = new DSONameServiceMock(); resolver = itemTemplatePageResolver;
resolver = new ItemTemplatePageResolver(dsoNameService as DSONameService, itemTemplateService);
}); });
it('should resolve an item template with the correct id', (done) => { 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<any>)
.pipe(first()) .pipe(first())
.subscribe( .subscribe(
(resolved) => { (resolved) => {

View File

@@ -1,39 +1,23 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
Resolve, ResolveFn,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } from '@angular/router';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
import { ItemTemplateDataService } from '../../core/data/item-template-data.service'; import { ItemTemplateDataService } from '../../core/data/item-template-data.service';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { Item } from '../../core/shared/item.model'; import { Item } from '../../core/shared/item.model';
import { getFirstCompletedRemoteData } from '../../core/shared/operators'; import { getFirstCompletedRemoteData } from '../../core/shared/operators';
import { followLink } from '../../shared/utils/follow-link-config.model'; import { followLink } from '../../shared/utils/follow-link-config.model';
/** export const itemTemplatePageResolver: ResolveFn<RemoteData<Item>> = (
* This class represents a resolver that requests a specific collection's item template before the route is activated route: ActivatedRouteSnapshot,
*/ state: RouterStateSnapshot,
@Injectable({ providedIn: 'root' }) itemTemplateService: ItemTemplateDataService = inject(ItemTemplateDataService),
export class ItemTemplatePageResolver implements Resolve<RemoteData<Item>> { ): Observable<RemoteData<Item>> => {
constructor( return itemTemplateService.findByCollectionID(route.params.id, true, false, followLink('templateItemOf')).pipe(
public dsoNameService: DSONameService, getFirstCompletedRemoteData(),
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<<RemoteData<Collection>> 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<RemoteData<Item>> {
return this.itemTemplateService.findByCollectionID(route.params.id, true, false, followLink('templateItemOf')).pipe(
getFirstCompletedRemoteData(),
);
}
}

View File

@@ -1,6 +1,6 @@
import { Route } from '@angular/router'; 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'; import { ThemedCommunityListPageComponent } from './themed-community-list-page.component';
/** /**
@@ -12,7 +12,7 @@ export const ROUTES: Route[] = [
component: ThemedCommunityListPageComponent, component: ThemedCommunityListPageComponent,
pathMatch: 'full', pathMatch: 'full',
resolve: { resolve: {
breadcrumb: I18nBreadcrumbResolver, breadcrumb: i18nBreadcrumbResolver,
}, },
data: { title: 'communityList.tabTitle', breadcrumbKey: 'communityList' }, data: { title: 'communityList.tabTitle', breadcrumbKey: 'communityList' },
}, },

View File

@@ -1,6 +1,7 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
ResolveFn,
Router, Router,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } 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 { 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 { DsoPageSingleFeatureGuard } from '../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard';
import { FeatureID } from '../core/data/feature-authorization/feature-id'; import { FeatureID } from '../core/data/feature-authorization/feature-id';
import { RemoteData } from '../core/data/remote-data';
import { Community } from '../core/shared/community.model'; import { Community } from '../core/shared/community.model';
import { CommunityPageResolver } from './community-page.resolver'; import { communityPageResolver } from './community-page.resolver';
@Injectable({ @Injectable({
providedIn: 'root', 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 * Guard for preventing unauthorized access to certain {@link Community} pages requiring administrator rights
*/ */
export class CommunityPageAdministratorGuard extends DsoPageSingleFeatureGuard<Community> { export class CommunityPageAdministratorGuard extends DsoPageSingleFeatureGuard<Community> {
constructor(protected resolver: CommunityPageResolver,
protected authorizationService: AuthorizationDataService, protected resolver: ResolveFn<RemoteData<Community>> = communityPageResolver;
constructor(protected authorizationService: AuthorizationDataService,
protected router: Router, protected router: Router,
protected authService: AuthService) { protected authService: AuthService) {
super(resolver, authorizationService, router, authService); super(authorizationService, router, authService);
} }
/** /**

View File

@@ -1,23 +1,26 @@
import { Route } from '@angular/router'; import {
mapToCanActivate,
Route,
} from '@angular/router';
import { BrowseByGuard } from '../browse-by/browse-by-guard'; import { browseByGuard } from '../browse-by/browse-by-guard';
import { BrowseByI18nBreadcrumbResolver } from '../browse-by/browse-by-i18n-breadcrumb.resolver'; import { browseByI18nBreadcrumbResolver } from '../browse-by/browse-by-i18n-breadcrumb.resolver';
import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; import { authenticatedGuard } from '../core/auth/authenticated.guard';
import { CommunityBreadcrumbResolver } from '../core/breadcrumbs/community-breadcrumb.resolver'; import { communityBreadcrumbResolver } from '../core/breadcrumbs/community-breadcrumb.resolver';
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
import { ComcolBrowseByComponent } from '../shared/comcol/sections/comcol-browse-by/comcol-browse-by.component'; 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 { 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 { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
import { MenuItemType } from '../shared/menu/menu-item-type.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 { CommunityPageAdministratorGuard } from './community-page-administrator.guard';
import { import {
COMMUNITY_CREATE_PATH, COMMUNITY_CREATE_PATH,
COMMUNITY_EDIT_PATH, COMMUNITY_EDIT_PATH,
} from './community-page-routing-paths'; } from './community-page-routing-paths';
import { CreateCommunityPageComponent } from './create-community-page/create-community-page.component'; 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 { DeleteCommunityPageComponent } from './delete-community-page/delete-community-page.component';
import { SubComColSectionComponent } from './sections/sub-com-col-section/sub-com-col-section.component'; import { SubComColSectionComponent } from './sections/sub-com-col-section/sub-com-col-section.component';
import { ThemedCommunityPageComponent } from './themed-community-page.component'; import { ThemedCommunityPageComponent } from './themed-community-page.component';
@@ -30,27 +33,27 @@ export const ROUTES: Route[] = [
path: '', path: '',
component: CreateCommunityPageComponent, component: CreateCommunityPageComponent,
resolve: { resolve: {
breadcrumb: I18nBreadcrumbResolver, breadcrumb: i18nBreadcrumbResolver,
}, },
data: { data: {
breadcrumbKey: 'community.create', breadcrumbKey: 'community.create',
}, },
}, },
], ],
canActivate: [AuthenticatedGuard, CreateCommunityPageGuard], canActivate: [authenticatedGuard, createCommunityPageGuard],
data: { data: {
breadcrumbQueryParam: 'parent', breadcrumbQueryParam: 'parent',
}, },
resolve: { resolve: {
breadcrumb: CommunityBreadcrumbResolver, breadcrumb: communityBreadcrumbResolver,
}, },
}, },
{ {
path: ':id', path: ':id',
resolve: { resolve: {
dso: CommunityPageResolver, dso: communityPageResolver,
breadcrumb: CommunityBreadcrumbResolver, breadcrumb: communityBreadcrumbResolver,
menu: DSOEditMenuResolver, menu: dsoEditMenuResolver,
}, },
runGuardsAndResolvers: 'always', runGuardsAndResolvers: 'always',
children: [ children: [
@@ -58,13 +61,13 @@ export const ROUTES: Route[] = [
path: COMMUNITY_EDIT_PATH, path: COMMUNITY_EDIT_PATH,
loadChildren: () => import('./edit-community-page/edit-community-page-routes') loadChildren: () => import('./edit-community-page/edit-community-page-routes')
.then((m) => m.ROUTES), .then((m) => m.ROUTES),
canActivate: [CommunityPageAdministratorGuard], canActivate: mapToCanActivate([CommunityPageAdministratorGuard]),
}, },
{ {
path: 'delete', path: 'delete',
pathMatch: 'full', pathMatch: 'full',
component: DeleteCommunityPageComponent, component: DeleteCommunityPageComponent,
canActivate: [AuthenticatedGuard], canActivate: [authenticatedGuard],
}, },
{ {
path: '', path: '',
@@ -80,7 +83,7 @@ export const ROUTES: Route[] = [
pathMatch: 'full', pathMatch: 'full',
component: SubComColSectionComponent, component: SubComColSectionComponent,
resolve: { resolve: {
breadcrumb: I18nBreadcrumbResolver, breadcrumb: i18nBreadcrumbResolver,
}, },
data: { breadcrumbKey: 'community.subcoms-cols' }, data: { breadcrumbKey: 'community.subcoms-cols' },
}, },
@@ -88,9 +91,9 @@ export const ROUTES: Route[] = [
path: 'browse/:id', path: 'browse/:id',
pathMatch: 'full', pathMatch: 'full',
component: ComcolBrowseByComponent, component: ComcolBrowseByComponent,
canActivate: [BrowseByGuard], canActivate: [browseByGuard],
resolve: { resolve: {
breadcrumb: BrowseByI18nBreadcrumbResolver, breadcrumb: browseByI18nBreadcrumbResolver,
}, },
data: { breadcrumbKey: 'browse.metadata' }, data: { breadcrumbKey: 'browse.metadata' },
}, },

View File

@@ -1,11 +1,12 @@
import { Observable } from 'rxjs';
import { first } from 'rxjs/operators'; import { first } from 'rxjs/operators';
import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; 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', () => { describe('resolve', () => {
let resolver: CommunityPageResolver; let resolver: any;
let communityService: any; let communityService: any;
let store: any; let store: any;
const uuid = '1234-65487-12354-1235'; const uuid = '1234-65487-12354-1235';
@@ -17,11 +18,11 @@ describe('CommunityPageResolver', () => {
store = jasmine.createSpyObj('store', { store = jasmine.createSpyObj('store', {
dispatch: {}, dispatch: {},
}); });
resolver = new CommunityPageResolver(communityService, store); resolver = communityPageResolver;
}); });
it('should resolve a community with the correct id', (done) => { 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<any>)
.pipe(first()) .pipe(first())
.subscribe( .subscribe(
(resolved) => { (resolved) => {

View File

@@ -1,12 +1,13 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
Resolve, ResolveFn,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } from '@angular/router';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { AppState } from '../app.reducer';
import { CommunityDataService } from '../core/data/community-data.service'; import { CommunityDataService } from '../core/data/community-data.service';
import { RemoteData } from '../core/data/remote-data'; import { RemoteData } from '../core/data/remote-data';
import { ResolvedAction } from '../core/resolving/resolver.actions'; import { ResolvedAction } from '../core/resolving/resolver.actions';
@@ -29,37 +30,32 @@ export const COMMUNITY_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig<Community>[] = [
]; ];
/** /**
* 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<<RemoteData<Community>> Emits the found community based on the parameters in the current route,
* or an error if something went wrong
*/ */
@Injectable({ providedIn: 'root' }) export const communityPageResolver: ResolveFn<RemoteData<Community>> = (
export class CommunityPageResolver implements Resolve<RemoteData<Community>> { route: ActivatedRouteSnapshot,
constructor( state: RouterStateSnapshot,
private communityService: CommunityDataService, communityService: CommunityDataService = inject(CommunityDataService),
private store: Store<any>, store: Store<AppState> = inject(Store<AppState>),
) { ): Observable<RemoteData<Community>> => {
} const communityRD$ = communityService.findById(
route.params.id,
true,
false,
...COMMUNITY_PAGE_LINKS_TO_FOLLOW,
).pipe(
getFirstCompletedRemoteData(),
);
/** communityRD$.subscribe((communityRD: RemoteData<Community>) => {
* Method for resolving a community based on the parameters in the current route store.dispatch(new ResolvedAction(state.url, communityRD.payload));
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot });
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
* @returns Observable<<RemoteData<Community>> 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<RemoteData<Community>> {
const communityRD$ = this.communityService.findById(
route.params.id,
true,
false,
...COMMUNITY_PAGE_LINKS_TO_FOLLOW,
).pipe(
getFirstCompletedRemoteData(),
);
communityRD$.subscribe((communityRD: RemoteData<Community>) => { return communityRD$;
this.store.dispatch(new ResolvedAction(state.url, communityRD.payload)); };
});
return communityRD$;
}
}

View File

@@ -6,11 +6,11 @@ import {
createFailedRemoteDataObject$, createFailedRemoteDataObject$,
createSuccessfulRemoteDataObject$, createSuccessfulRemoteDataObject$,
} from '../../shared/remote-data.utils'; } 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', () => { describe('canActivate', () => {
let guard: CreateCommunityPageGuard; let guard: any;
let router; let router;
let communityDataServiceStub: any; let communityDataServiceStub: any;
@@ -28,11 +28,11 @@ describe('CreateCommunityPageGuard', () => {
}; };
router = new RouterMock(); router = new RouterMock();
guard = new CreateCommunityPageGuard(router, communityDataServiceStub); guard = createCommunityPageGuard;
}); });
it('should return true when the parent ID resolves to a community', () => { 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()) .pipe(first())
.subscribe( .subscribe(
(canActivate) => (canActivate) =>
@@ -41,7 +41,7 @@ describe('CreateCommunityPageGuard', () => {
}); });
it('should return true when no parent ID has been provided', () => { 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()) .pipe(first())
.subscribe( .subscribe(
(canActivate) => (canActivate) =>
@@ -50,7 +50,7 @@ describe('CreateCommunityPageGuard', () => {
}); });
it('should return false when the parent ID does not resolve to a community', () => { 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()) .pipe(first())
.subscribe( .subscribe(
(canActivate) => (canActivate) =>
@@ -59,7 +59,7 @@ describe('CreateCommunityPageGuard', () => {
}); });
it('should return false when the parent ID resolves to an error response', () => { 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()) .pipe(first())
.subscribe( .subscribe(
(canActivate) => (canActivate) =>

View File

@@ -1,7 +1,7 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
CanActivate, CanActivateFn,
Router, Router,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } from '@angular/router';
@@ -24,35 +24,29 @@ import {
} from '../../shared/empty.util'; } from '../../shared/empty.util';
/** /**
* Prevent creation of a community with an invalid parent community provided * True when either NO parent ID query parameter has been provided, or the parent ID resolves to a valid parent community
* @class CreateCommunityPageGuard * Reroutes to a 404 page when the page cannot be activated
*/ */
@Injectable({ providedIn: 'root' }) export const createCommunityPageGuard: CanActivateFn = (
export class CreateCommunityPageGuard implements CanActivate { route: ActivatedRouteSnapshot,
public constructor(private router: Router, private communityService: CommunityDataService) { state: RouterStateSnapshot,
communityService: CommunityDataService = inject(CommunityDataService),
router: Router = inject(Router),
): Observable<boolean> => {
const parentID = route.queryParams.parent;
if (hasNoValue(parentID)) {
return observableOf(true);
} }
/** return communityService.findById(parentID)
* True when either NO parent ID query parameter has been provided, or the parent ID resolves to a valid parent community .pipe(
* Reroutes to a 404 page when the page cannot be activated getFirstCompletedRemoteData(),
* @method canActivate map((communityRD: RemoteData<Community>) => hasValue(communityRD) && communityRD.hasSucceeded && hasValue(communityRD.payload)),
*/ tap((isValid: boolean) => {
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> { if (!isValid) {
const parentID = route.queryParams.parent; router.navigate(['/404']);
if (hasNoValue(parentID)) { }
return observableOf(true); },
} ),
);
return this.communityService.findById(parentID) };
.pipe(
getFirstCompletedRemoteData(),
map((communityRD: RemoteData<Community>) => hasValue(communityRD) && communityRD.hasSucceeded && hasValue(communityRD.payload)),
tap((isValid: boolean) => {
if (!isValid) {
this.router.navigate(['/404']);
}
},
),
);
}
}

View File

@@ -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 { CommunityAdministratorGuard } from '../../core/data/feature-authorization/feature-authorization-guard/community-administrator.guard';
import { ResourcePolicyCreateComponent } from '../../shared/resource-policies/create/resource-policy-create.component'; import { ResourcePolicyCreateComponent } from '../../shared/resource-policies/create/resource-policy-create.component';
import { ResourcePolicyEditComponent } from '../../shared/resource-policies/edit/resource-policy-edit.component'; import { ResourcePolicyEditComponent } from '../../shared/resource-policies/edit/resource-policy-edit.component';
import { ResourcePolicyResolver } from '../../shared/resource-policies/resolvers/resource-policy.resolver'; import { resourcePolicyResolver } from '../../shared/resource-policies/resolvers/resource-policy.resolver';
import { ResourcePolicyTargetResolver } from '../../shared/resource-policies/resolvers/resource-policy-target.resolver'; import { resourcePolicyTargetResolver } from '../../shared/resource-policies/resolvers/resource-policy-target.resolver';
import { CommunityAccessControlComponent } from './community-access-control/community-access-control.component'; import { CommunityAccessControlComponent } from './community-access-control/community-access-control.component';
import { CommunityAuthorizationsComponent } from './community-authorizations/community-authorizations.component'; import { CommunityAuthorizationsComponent } from './community-authorizations/community-authorizations.component';
import { CommunityCurateComponent } from './community-curate/community-curate.component'; import { CommunityCurateComponent } from './community-curate/community-curate.component';
@@ -21,11 +24,11 @@ export const ROUTES: Route[] = [
{ {
path: '', path: '',
resolve: { resolve: {
breadcrumb: I18nBreadcrumbResolver, breadcrumb: i18nBreadcrumbResolver,
}, },
data: { breadcrumbKey: 'community.edit' }, data: { breadcrumbKey: 'community.edit' },
component: EditCommunityPageComponent, component: EditCommunityPageComponent,
canActivate: [CommunityAdministratorGuard], canActivate: mapToCanActivate([CommunityAdministratorGuard]),
children: [ children: [
{ {
path: '', path: '',
@@ -63,7 +66,7 @@ export const ROUTES: Route[] = [
{ {
path: 'create', path: 'create',
resolve: { resolve: {
resourcePolicyTarget: ResourcePolicyTargetResolver, resourcePolicyTarget: resourcePolicyTargetResolver,
}, },
component: ResourcePolicyCreateComponent, component: ResourcePolicyCreateComponent,
data: { title: 'resource-policies.create.page.title' }, data: { title: 'resource-policies.create.page.title' },
@@ -71,7 +74,7 @@ export const ROUTES: Route[] = [
{ {
path: 'edit', path: 'edit',
resolve: { resolve: {
resourcePolicy: ResourcePolicyResolver, resourcePolicy: resourcePolicyResolver,
}, },
component: ResourcePolicyEditComponent, component: ResourcePolicyEditComponent,
data: { title: 'resource-policies.edit.page.title' }, data: { title: 'resource-policies.edit.page.title' },

View File

@@ -17,10 +17,10 @@ import {
storeModuleConfig, storeModuleConfig,
} from '../../app.reducer'; } from '../../app.reducer';
import { authReducer } from './auth.reducer'; import { authReducer } from './auth.reducer';
import { AuthBlockingGuard } from './auth-blocking.guard'; import { authBlockingGuard } from './auth-blocking.guard';
describe('AuthBlockingGuard', () => { describe('authBlockingGuard', () => {
let guard: AuthBlockingGuard; let guard: any;
let initialState; let initialState;
let store: Store<AppState>; let store: Store<AppState>;
let mockStore: MockStore<AppState>; let mockStore: MockStore<AppState>;
@@ -44,7 +44,7 @@ describe('AuthBlockingGuard', () => {
], ],
providers: [ providers: [
provideMockStore({ initialState }), provideMockStore({ initialState }),
{ provide: AuthBlockingGuard, useValue: guard }, { provide: authBlockingGuard, useValue: guard },
], ],
}).compileComponents(); }).compileComponents();
})); }));
@@ -52,14 +52,14 @@ describe('AuthBlockingGuard', () => {
beforeEach(() => { beforeEach(() => {
store = TestBed.inject(Store); store = TestBed.inject(Store);
mockStore = store as MockStore<AppState>; mockStore = store as MockStore<AppState>;
guard = new AuthBlockingGuard(store); guard = authBlockingGuard;
}); });
describe(`canActivate`, () => { describe(`canActivate`, () => {
describe(`when authState.blocking is undefined`, () => { describe(`when authState.blocking is undefined`, () => {
it(`should not emit anything`, (done) => { it(`should not emit anything`, (done) => {
expect(guard.canActivate()).toBeObservable(cold('-')); expect(guard(null, null, store)).toBeObservable(cold('-'));
done(); done();
}); });
}); });
@@ -77,7 +77,7 @@ describe('AuthBlockingGuard', () => {
}); });
it(`should not emit anything`, (done) => { it(`should not emit anything`, (done) => {
expect(guard.canActivate()).toBeObservable(cold('-')); expect(guard(null, null, store)).toBeObservable(cold('-'));
done(); done();
}); });
}); });
@@ -95,7 +95,7 @@ describe('AuthBlockingGuard', () => {
}); });
it(`should succeed`, (done) => { it(`should succeed`, (done) => {
expect(guard.canActivate()).toBeObservable(cold('(a|)', { a: true })); expect(guard(null, null, store)).toBeObservable(cold('(a|)', { a: true }));
done(); done();
}); });
}); });

View File

@@ -1,5 +1,9 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { CanActivate } from '@angular/router'; import {
ActivatedRouteSnapshot,
CanActivateFn,
RouterStateSnapshot,
} from '@angular/router';
import { import {
select, select,
Store, Store,
@@ -20,24 +24,16 @@ import { isAuthenticationBlocking } from './selectors';
* route until the authentication status has loaded. * route until the authentication status has loaded.
* To ensure all rest requests get the correct auth header. * To ensure all rest requests get the correct auth header.
*/ */
@Injectable({ export const authBlockingGuard: CanActivateFn = (
providedIn: 'root', route: ActivatedRouteSnapshot,
}) state: RouterStateSnapshot,
export class AuthBlockingGuard implements CanActivate { store: Store<AppState> = inject(Store<AppState>),
): Observable<boolean> => {
return store.pipe(select(isAuthenticationBlocking)).pipe(
map((isBlocking: boolean) => isBlocking === false),
distinctUntilChanged(),
filter((finished: boolean) => finished === true),
take(1),
);
};
constructor(private store: Store<AppState>) {
}
/**
* True when the authentication isn't blocking everything
*/
canActivate(): Observable<boolean> {
return this.store.pipe(select(isAuthenticationBlocking)).pipe(
map((isBlocking: boolean) => isBlocking === false),
distinctUntilChanged(),
filter((finished: boolean) => finished === true),
take(1),
);
}
}

View File

@@ -1,7 +1,8 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
CanActivate, CanActivateChildFn,
CanActivateFn,
Router, Router,
RouterStateSnapshot, RouterStateSnapshot,
UrlTree, UrlTree,
@@ -17,7 +18,7 @@ import {
switchMap, switchMap,
} from 'rxjs/operators'; } from 'rxjs/operators';
import { CoreState } from '../core-state.model'; import { AppState } from '../../app.reducer';
import { import {
AuthService, AuthService,
LOGIN_ROUTE, LOGIN_ROUTE,
@@ -29,49 +30,35 @@ import {
/** /**
* Prevent unauthorized activating and loading of routes * 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 const authenticatedGuard: CanActivateFn = (
export class AuthenticatedGuard implements CanActivate { route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
authService: AuthService = inject(AuthService),
router: Router = inject(Router),
store: Store<AppState> = inject(Store<AppState>),
): Observable<boolean | UrlTree> => {
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]);
}
}),
);
};
/** export const AuthenticatedGuardChild: CanActivateChildFn = (
* @constructor route: ActivatedRouteSnapshot,
*/ state: RouterStateSnapshot,
constructor(private authService: AuthService, private router: Router, private store: Store<CoreState>) {} ) => authenticatedGuard(route, state);
/**
* True when user is authenticated
* UrlTree with redirect to login page when user isn't authenticated
* @method canActivate
*/
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> {
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<boolean | UrlTree> {
return this.canActivate(route, state);
}
private handleAuth(url: string): Observable<boolean | UrlTree> {
// 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]);
}
}),
);
}
}

View File

@@ -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 { 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 { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { BitstreamDataService } from '../data/bitstream-data.service'; import { BitstreamDataService } from '../data/bitstream-data.service';
import { Bitstream } from '../shared/bitstream.model'; import { Bitstream } from '../shared/bitstream.model';
import { DSpaceObject } from '../shared/dspace-object.model';
import { BitstreamBreadcrumbsService } from './bitstream-breadcrumbs.service'; import { BitstreamBreadcrumbsService } from './bitstream-breadcrumbs.service';
import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver'; 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({ export const bitstreamBreadcrumbResolver: ResolveFn<BreadcrumbConfig<Bitstream>> = (
providedIn: 'root', route: ActivatedRouteSnapshot,
}) state: RouterStateSnapshot,
export class BitstreamBreadcrumbResolver extends DSOBreadcrumbResolver<Bitstream> { breadcrumbService: BitstreamBreadcrumbsService = inject(BitstreamBreadcrumbsService),
constructor( dataService: BitstreamDataService = inject(BitstreamDataService),
protected breadcrumbService: BitstreamBreadcrumbsService, protected dataService: BitstreamDataService) { ): Observable<BreadcrumbConfig<Bitstream>> => {
super(breadcrumbService, dataService); const linksToFollow: FollowLinkConfig<DSpaceObject>[] = BITSTREAM_PAGE_LINKS_TO_FOLLOW as FollowLinkConfig<DSpaceObject>[];
} return DSOBreadcrumbResolver(
route,
state,
breadcrumbService,
dataService,
...linksToFollow,
) as Observable<BreadcrumbConfig<Bitstream>>;
};
/**
* 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<Bitstream>[] {
return BITSTREAM_PAGE_LINKS_TO_FOLLOW;
}
}

View File

@@ -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 { COLLECTION_PAGE_LINKS_TO_FOLLOW } from '../../collection-page/collection-page.resolver';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { CollectionDataService } from '../data/collection-data.service'; import { CollectionDataService } from '../data/collection-data.service';
import { Collection } from '../shared/collection.model'; import { Collection } from '../shared/collection.model';
import { DSpaceObject } from '../shared/dspace-object.model';
import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver'; import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver';
import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; 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({ export const collectionBreadcrumbResolver: ResolveFn<BreadcrumbConfig<Collection>> = (
providedIn: 'root', route: ActivatedRouteSnapshot,
}) state: RouterStateSnapshot,
export class CollectionBreadcrumbResolver extends DSOBreadcrumbResolver<Collection> { breadcrumbService: DSOBreadcrumbsService = inject(DSOBreadcrumbsService),
constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: CollectionDataService) { dataService: CollectionDataService = inject(CollectionDataService),
super(breadcrumbService, dataService); ): Observable<BreadcrumbConfig<Collection>> => {
} const linksToFollow: FollowLinkConfig<DSpaceObject>[] = COLLECTION_PAGE_LINKS_TO_FOLLOW as FollowLinkConfig<DSpaceObject>[];
return DSOBreadcrumbResolver(
route,
state,
breadcrumbService,
dataService,
...linksToFollow,
) as Observable<BreadcrumbConfig<Collection>>;
};
/**
* 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<Collection>[] {
return COLLECTION_PAGE_LINKS_TO_FOLLOW;
}
}

View File

@@ -1,6 +1,7 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
ResolveFn,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } from '@angular/router';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
@@ -11,43 +12,39 @@ import { hasValue } from '../../shared/empty.util';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { CommunityDataService } from '../data/community-data.service'; import { CommunityDataService } from '../data/community-data.service';
import { Community } from '../shared/community.model'; import { Community } from '../shared/community.model';
import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver'; import { DSpaceObject } from '../shared/dspace-object.model';
import {
DSOBreadcrumbResolver,
DSOBreadcrumbResolverByUuid,
} from './dso-breadcrumb.resolver';
import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; 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({ export const communityBreadcrumbResolver: ResolveFn<BreadcrumbConfig<Community>> = (
providedIn: 'root', route: ActivatedRouteSnapshot,
}) state: RouterStateSnapshot,
export class CommunityBreadcrumbResolver extends DSOBreadcrumbResolver<Community> { breadcrumbService: DSOBreadcrumbsService = inject(DSOBreadcrumbsService),
constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: CommunityDataService) { dataService: CommunityDataService = inject(CommunityDataService),
super(breadcrumbService, dataService); ): Observable<BreadcrumbConfig<Community>> => {
const linksToFollow: FollowLinkConfig<DSpaceObject>[] = COMMUNITY_PAGE_LINKS_TO_FOLLOW as FollowLinkConfig<DSpaceObject>[];
if (hasValue(route.data.breadcrumbQueryParam) && hasValue(route.queryParams[route.data.breadcrumbQueryParam])) {
return DSOBreadcrumbResolverByUuid(
route,
state,
route.queryParams[route.data.breadcrumbQueryParam],
breadcrumbService,
dataService,
...linksToFollow,
) as Observable<BreadcrumbConfig<Community>>;
} else {
return DSOBreadcrumbResolver(
route,
state,
breadcrumbService,
dataService,
...linksToFollow,
) as Observable<BreadcrumbConfig<Community>>;
} }
};
/**
* Method to retrieve the breadcrumb config by the route id. It is also possible to retrieve the id through the
* query parameters. This is done by defining the name of the query parameter in the data section under the property
* breadcrumbQueryParam.
*
* @param route The current {@link ActivatedRouteSnapshot}
* @param state The current {@link RouterStateSnapshot}
* @returns BreadcrumbConfig object
*/
override resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<BreadcrumbConfig<Community>> {
if (hasValue(route.data.breadcrumbQueryParam) && hasValue(route.queryParams[route.data.breadcrumbQueryParam])) {
return this.resolveById(route.queryParams[route.data.breadcrumbQueryParam]);
} else {
return super.resolve(route, state);
}
}
/**
* 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<Community>[] {
return COMMUNITY_PAGE_LINKS_TO_FOLLOW;
}
}

View File

@@ -2,12 +2,11 @@ import { getTestScheduler } from 'jasmine-marbles';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { Collection } from '../shared/collection.model'; import { Collection } from '../shared/collection.model';
import { CollectionBreadcrumbResolver } from './collection-breadcrumb.resolver'; import { collectionBreadcrumbResolver } from './collection-breadcrumb.resolver';
import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver';
describe('DSOBreadcrumbResolver', () => { describe('DSOBreadcrumbResolver', () => {
describe('resolve', () => { describe('resolve', () => {
let resolver: DSOBreadcrumbResolver<Collection>; let resolver: any;
let collectionService: any; let collectionService: any;
let dsoBreadcrumbService: any; let dsoBreadcrumbService: any;
let testCollection: Collection; let testCollection: Collection;
@@ -17,21 +16,21 @@ describe('DSOBreadcrumbResolver', () => {
beforeEach(() => { beforeEach(() => {
uuid = '1234-65487-12354-1235'; uuid = '1234-65487-12354-1235';
breadcrumbUrl = '/collections/' + uuid; breadcrumbUrl = `/collections/${uuid}`;
currentUrl = breadcrumbUrl + '/edit'; currentUrl = `${breadcrumbUrl}/edit`;
testCollection = Object.assign(new Collection(), { testCollection = Object.assign(new Collection(), {
uuid: uuid, uuid: uuid,
type: 'collection', type: 'collection',
}); });
dsoBreadcrumbService = {}; dsoBreadcrumbService = {};
collectionService = { 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', () => { 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 }; const expectedConfig = { provider: dsoBreadcrumbService, key: testCollection, url: breadcrumbUrl };
getTestScheduler().expectObservable(resolvedConfig).toBe('(a|)', { a: expectedConfig }); getTestScheduler().expectObservable(resolvedConfig).toBe('(a|)', { a: expectedConfig });
}); });

View File

@@ -1,7 +1,5 @@
import { Injectable } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
Resolve,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } from '@angular/router';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
@@ -12,7 +10,6 @@ import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config
import { hasValue } from '../../shared/empty.util'; import { hasValue } from '../../shared/empty.util';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { IdentifiableDataService } from '../data/base/identifiable-data.service'; import { IdentifiableDataService } from '../data/base/identifiable-data.service';
import { ChildHALResource } from '../shared/child-hal-resource.model';
import { DSpaceObject } from '../shared/dspace-object.model'; import { DSpaceObject } from '../shared/dspace-object.model';
import { import {
getFirstCompletedRemoteData, getFirstCompletedRemoteData,
@@ -21,52 +18,52 @@ import {
import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; 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({ export const DSOBreadcrumbResolver: (route: ActivatedRouteSnapshot, state: RouterStateSnapshot, breadcrumbService: DSOBreadcrumbsService, dataService: IdentifiableDataService<DSpaceObject>, ...linksToFollow: FollowLinkConfig<DSpaceObject>[]) => Observable<BreadcrumbConfig<DSpaceObject>> = (
providedIn: 'root', route: ActivatedRouteSnapshot,
}) state: RouterStateSnapshot,
export abstract class DSOBreadcrumbResolver<T extends ChildHALResource & DSpaceObject> implements Resolve<BreadcrumbConfig<T>> { breadcrumbService: DSOBreadcrumbsService,
protected constructor( dataService: IdentifiableDataService<DSpaceObject>,
protected breadcrumbService: DSOBreadcrumbsService, ...linksToFollow: FollowLinkConfig<DSpaceObject>[]
protected dataService: IdentifiableDataService<T>, ): Observable<BreadcrumbConfig<DSpaceObject>> => {
) { return DSOBreadcrumbResolverByUuid(route, state, route.params.id, breadcrumbService, dataService, ...linksToFollow);
} };
/** /**
* Method for resolving a breadcrumb config object * Method for resolving a breadcrumb config object with the given UUID
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot *
* @param {RouterStateSnapshot} state The current RouterStateSnapshot * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
* @returns BreadcrumbConfig object * @param {RouterStateSnapshot} state The current RouterStateSnapshot
*/ * @param {String} uuid The uuid of the DSO object
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<BreadcrumbConfig<T>> { * @param {DSOBreadcrumbsService} breadcrumbService
return this.resolveById(route.params.id); * @param {IdentifiableDataService} dataService
} * @param linksToFollow
* @returns BreadcrumbConfig object
/** */
* Method for resolving a breadcrumb by id export const DSOBreadcrumbResolverByUuid: (route: ActivatedRouteSnapshot, state: RouterStateSnapshot, uuid: string, breadcrumbService: DSOBreadcrumbsService, dataService: IdentifiableDataService<DSpaceObject>, ...linksToFollow: FollowLinkConfig<DSpaceObject>[]) => Observable<BreadcrumbConfig<DSpaceObject>> = (
* route: ActivatedRouteSnapshot,
* @param uuid The uuid to resolve state: RouterStateSnapshot,
* @returns BreadcrumbConfig object uuid: string,
*/ breadcrumbService: DSOBreadcrumbsService,
resolveById(uuid: string): Observable<BreadcrumbConfig<T>> { dataService: IdentifiableDataService<DSpaceObject>,
return this.dataService.findById(uuid, true, false, ...this.followLinks).pipe( ...linksToFollow: FollowLinkConfig<DSpaceObject>[]
getFirstCompletedRemoteData(), ): Observable<BreadcrumbConfig<DSpaceObject>> => {
getRemoteDataPayload(), return dataService.findById(uuid, true, false, ...linksToFollow).pipe(
map((object: T) => { getFirstCompletedRemoteData(),
if (hasValue(object)) { getRemoteDataPayload(),
return { provider: this.breadcrumbService, key: object, url: getDSORoute(object) }; map((object: DSpaceObject) => {
} else { if (hasValue(object)) {
return undefined; return { provider: breadcrumbService, key: object, url: getDSORoute(object) };
} } 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<T>[];
}

View File

@@ -1,9 +1,9 @@
import { URLCombiner } from '../url-combiner/url-combiner'; 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', () => { describe('resolve', () => {
let resolver: I18nBreadcrumbResolver; let resolver: any;
let i18nBreadcrumbService: any; let i18nBreadcrumbService: any;
let i18nKey: string; let i18nKey: string;
let route: any; let route: any;
@@ -27,18 +27,18 @@ describe('I18nBreadcrumbResolver', () => {
}; };
expectedPath = new URLCombiner(parentSegment, segment).toString(); expectedPath = new URLCombiner(parentSegment, segment).toString();
i18nBreadcrumbService = {}; i18nBreadcrumbService = {};
resolver = new I18nBreadcrumbResolver(i18nBreadcrumbService); resolver = i18nBreadcrumbResolver;
}); });
it('should resolve the breadcrumb config', () => { 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 }; const expectedConfig = { provider: i18nBreadcrumbService, key: i18nKey, url: expectedPath };
expect(resolvedConfig).toEqual(expectedConfig); expect(resolvedConfig).toEqual(expectedConfig);
}); });
it('should resolve throw an error when no breadcrumbKey is defined', () => { it('should resolve throw an error when no breadcrumbKey is defined', () => {
expect(() => { expect(() => {
resolver.resolve({ data: {} } as any, undefined); resolver({ data: {} } as any, undefined, i18nBreadcrumbService);
}).toThrow(); }).toThrow();
}); });
}); });

View File

@@ -1,7 +1,7 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
Resolve, ResolveFn,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } from '@angular/router';
@@ -11,27 +11,21 @@ import { currentPathFromSnapshot } from '../../shared/utils/route.utils';
import { I18nBreadcrumbsService } from './i18n-breadcrumbs.service'; 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({ export const i18nBreadcrumbResolver: ResolveFn<BreadcrumbConfig<string>> = (
providedIn: 'root', route: ActivatedRouteSnapshot,
}) state: RouterStateSnapshot,
export class I18nBreadcrumbResolver implements Resolve<BreadcrumbConfig<string>> { breadcrumbService: I18nBreadcrumbsService = inject(I18nBreadcrumbsService),
constructor(protected breadcrumbService: I18nBreadcrumbsService) { ): BreadcrumbConfig<string> => {
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: breadcrumbService, key: key, url: fullPath };
* 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<string> {
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 };
}
}

View File

@@ -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 { ITEM_PAGE_LINKS_TO_FOLLOW } from '../../item-page/item.resolver';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { ItemDataService } from '../data/item-data.service'; import { ItemDataService } from '../data/item-data.service';
import { DSpaceObject } from '../shared/dspace-object.model';
import { Item } from '../shared/item.model'; import { Item } from '../shared/item.model';
import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver'; import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver';
import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; 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({ export const itemBreadcrumbResolver: ResolveFn<BreadcrumbConfig<Item>> = (
providedIn: 'root', route: ActivatedRouteSnapshot,
}) state: RouterStateSnapshot,
export class ItemBreadcrumbResolver extends DSOBreadcrumbResolver<Item> { breadcrumbService: DSOBreadcrumbsService = inject(DSOBreadcrumbsService),
constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: ItemDataService) { dataService: ItemDataService = inject(ItemDataService),
super(breadcrumbService, dataService); ): Observable<BreadcrumbConfig<Item>> => {
} const linksToFollow: FollowLinkConfig<DSpaceObject>[] = ITEM_PAGE_LINKS_TO_FOLLOW as FollowLinkConfig<DSpaceObject>[];
return DSOBreadcrumbResolver(
/** route,
* Method that returns the follow links to already resolve state,
* The self links defined in this list are expected to be requested somewhere in the near future breadcrumbService,
* Requesting them as embeds will limit the number of requests dataService,
*/ ...linksToFollow,
get followLinks(): FollowLinkConfig<Item>[] { ) as Observable<BreadcrumbConfig<Item>>;
return ITEM_PAGE_LINKS_TO_FOLLOW; };
}
}

View File

@@ -1,8 +1,8 @@
import { NavigationBreadcrumbResolver } from './navigation-breadcrumb.resolver'; import { navigationBreadcrumbResolver } from './navigation-breadcrumb.resolver';
describe('NavigationBreadcrumbResolver', () => { describe('navigationBreadcrumbResolver', () => {
describe('resolve', () => { describe('resolve', () => {
let resolver: NavigationBreadcrumbResolver; let resolver: any;
let NavigationBreadcrumbService: any; let NavigationBreadcrumbService: any;
let i18nKey: string; let i18nKey: string;
let relatedI18nKey: string; let relatedI18nKey: string;
@@ -40,11 +40,11 @@ describe('NavigationBreadcrumbResolver', () => {
}; };
expectedPath = '/base/example:/base'; expectedPath = '/base/example:/base';
NavigationBreadcrumbService = {}; NavigationBreadcrumbService = {};
resolver = new NavigationBreadcrumbResolver(NavigationBreadcrumbService); resolver = navigationBreadcrumbResolver;
}); });
it('should resolve the breadcrumb config', () => { 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 }; const expectedConfig = { provider: NavigationBreadcrumbService, key: `${i18nKey}:${relatedI18nKey}`, url: expectedPath };
expect(resolvedConfig).toEqual(expectedConfig); expect(resolvedConfig).toEqual(expectedConfig);
}); });

View File

@@ -1,7 +1,7 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
Resolve, ResolveFn,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } from '@angular/router';
@@ -9,49 +9,44 @@ import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config
import { NavigationBreadcrumbsService } from './navigation-breadcrumb.service'; 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({ export const navigationBreadcrumbResolver: ResolveFn<BreadcrumbConfig<string>> = (
providedIn: 'root', route: ActivatedRouteSnapshot,
}) state: RouterStateSnapshot,
export class NavigationBreadcrumbResolver implements Resolve<BreadcrumbConfig<string>> { breadcrumbService: NavigationBreadcrumbsService = inject(NavigationBreadcrumbsService),
): BreadcrumbConfig<string> => {
private parentRoutes: ActivatedRouteSnapshot[] = []; const parentRoutes: ActivatedRouteSnapshot[] = [];
constructor(protected breadcrumbService: NavigationBreadcrumbsService) { 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));
* Method to collect all parent routes snapshot from current route snapshot const baseUrlSegmentPath = route.parent.url[route.parent.url.length - 1].path;
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot const baseUrl = state.url.substring(0, state.url.lastIndexOf(baseUrlSegmentPath) + baseUrlSegmentPath.length);
*/
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<string> {
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);
const combinedParentBreadcrumbKeys = relatedParentRoutes.reduce((previous, current) => { const combinedParentBreadcrumbKeys = relatedParentRoutes.reduce((previous, current) => {
return `${previous}:${current.data.breadcrumbKey}`; return `${previous}:${current.data.breadcrumbKey}`;
}, route.data.breadcrumbKey); }, route.data.breadcrumbKey);
const combinedUrls = relatedParentRoutes.reduce((previous, current) => { const combinedUrls = relatedParentRoutes.reduce((previous, current) => {
return `${previous}:${baseUrl}${current.path}`; return `${previous}:${baseUrl}${current.path}`;
}, state.url); }, 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);
} }
} }

View File

@@ -1,8 +1,8 @@
import { PublicationClaimBreadcrumbResolver } from './publication-claim-breadcrumb.resolver'; import { publicationClaimBreadcrumbResolver } from './publication-claim-breadcrumb.resolver';
describe('PublicationClaimBreadcrumbResolver', () => { describe('publicationClaimBreadcrumbResolver', () => {
describe('resolve', () => { describe('resolve', () => {
let resolver: PublicationClaimBreadcrumbResolver; let resolver: any;
let publicationClaimBreadcrumbService: any; let publicationClaimBreadcrumbService: any;
const fullPath = '/test/publication-claim/openaire:6bee076d-4f2a-4555-a475-04a267769b2a'; const fullPath = '/test/publication-claim/openaire:6bee076d-4f2a-4555-a475-04a267769b2a';
const expectedKey = '6bee076d-4f2a-4555-a475-04a267769b2a'; const expectedKey = '6bee076d-4f2a-4555-a475-04a267769b2a';
@@ -19,11 +19,11 @@ describe('PublicationClaimBreadcrumbResolver', () => {
}, },
}; };
publicationClaimBreadcrumbService = {}; publicationClaimBreadcrumbService = {};
resolver = new PublicationClaimBreadcrumbResolver(publicationClaimBreadcrumbService); resolver = publicationClaimBreadcrumbResolver;
}); });
it('should resolve the breadcrumb config', () => { 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 }; const expectedConfig = { provider: publicationClaimBreadcrumbService, key: expectedKey };
expect(resolvedConfig).toEqual(expectedConfig); expect(resolvedConfig).toEqual(expectedConfig);
}); });

View File

@@ -1,29 +1,18 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
Resolve, ResolveFn,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } from '@angular/router';
import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config.model'; import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config.model';
import { PublicationClaimBreadcrumbService } from './publication-claim-breadcrumb.service'; import { PublicationClaimBreadcrumbService } from './publication-claim-breadcrumb.service';
@Injectable({ export const publicationClaimBreadcrumbResolver: ResolveFn<BreadcrumbConfig<string>> = (
providedIn: 'root', route: ActivatedRouteSnapshot,
}) state: RouterStateSnapshot,
export class PublicationClaimBreadcrumbResolver implements Resolve<BreadcrumbConfig<string>> { breadcrumbService: PublicationClaimBreadcrumbService = inject(PublicationClaimBreadcrumbService),
constructor(protected breadcrumbService: PublicationClaimBreadcrumbService) { ): BreadcrumbConfig<string> => {
} const targetId = route.paramMap.get('targetId').split(':')[1];
return { provider: breadcrumbService, key: targetId };
/** };
* 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<string> {
const targetId = route.paramMap.get('targetId').split(':')[1];
return { provider: this.breadcrumbService, key: targetId };
}
}

View File

@@ -1,8 +1,8 @@
import { QualityAssuranceBreadcrumbResolver } from './quality-assurance-breadcrumb.resolver'; import { qualityAssuranceBreadcrumbResolver } from './quality-assurance-breadcrumb.resolver';
describe('QualityAssuranceBreadcrumbResolver', () => { describe('qualityAssuranceBreadcrumbResolver', () => {
describe('resolve', () => { describe('resolve', () => {
let resolver: QualityAssuranceBreadcrumbResolver; let resolver: any;
let qualityAssuranceBreadcrumbService: any; let qualityAssuranceBreadcrumbService: any;
let route: any; let route: any;
const fullPath = '/test/quality-assurance/'; const fullPath = '/test/quality-assurance/';
@@ -19,11 +19,11 @@ describe('QualityAssuranceBreadcrumbResolver', () => {
}, },
}; };
qualityAssuranceBreadcrumbService = {}; qualityAssuranceBreadcrumbService = {};
resolver = new QualityAssuranceBreadcrumbResolver(qualityAssuranceBreadcrumbService); resolver = qualityAssuranceBreadcrumbResolver;
}); });
it('should resolve the breadcrumb config', () => { 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 }; const expectedConfig = { provider: qualityAssuranceBreadcrumbService, key: expectedKey, url: fullPath };
expect(resolvedConfig).toEqual(expectedConfig); expect(resolvedConfig).toEqual(expectedConfig);
}); });

View File

@@ -1,37 +1,27 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
Resolve, ResolveFn,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } from '@angular/router';
import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config.model'; import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config.model';
import { QualityAssuranceBreadcrumbService } from './quality-assurance-breadcrumb.service'; import { QualityAssuranceBreadcrumbService } from './quality-assurance-breadcrumb.service';
@Injectable({ export const qualityAssuranceBreadcrumbResolver: ResolveFn<BreadcrumbConfig<string>> = (
providedIn: 'root', route: ActivatedRouteSnapshot,
}) state: RouterStateSnapshot,
export class QualityAssuranceBreadcrumbResolver implements Resolve<BreadcrumbConfig<string>> { breadcrumbService: QualityAssuranceBreadcrumbService = inject(QualityAssuranceBreadcrumbService),
constructor(protected breadcrumbService: QualityAssuranceBreadcrumbService) {} ): BreadcrumbConfig<string> => {
const sourceId = route.paramMap.get('sourceId');
const topicId = route.paramMap.get('topicId');
let key = sourceId;
/** if (topicId) {
* Method that resolve QA item into a breadcrumb key += `:${topicId}`;
* 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<string> {
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 };
} }
} const fullPath = state.url;
const url = fullPath.substring(0, fullPath.indexOf(sourceId));
return { provider: breadcrumbService, key, url };
};

View File

@@ -37,9 +37,9 @@ class TestModel implements HALResource {
successor?: TestModel; successor?: TestModel;
} }
const mockDataServiceMap: any = { const mockDataServiceMap: any = new Map([
[TEST_MODEL.value]: () => import('../../../shared/testing/test-data-service.mock').then(m => m.TestDataService), [TEST_MODEL.value, () => import('../../../shared/testing/test-data-service.mock').then(m => m.TestDataService)],
}; ]);
let testDataService: TestDataService; let testDataService: TestDataService;

View File

@@ -1,7 +1,6 @@
import { import {
Inject, Inject,
Injectable, Injectable,
InjectionToken,
Injector, Injector,
} from '@angular/core'; } from '@angular/core';
import { import {
@@ -25,7 +24,7 @@ import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model
import { HALDataService } from '../../data/base/hal-data-service.interface'; import { HALDataService } from '../../data/base/hal-data-service.interface';
import { PaginatedList } from '../../data/paginated-list.model'; import { PaginatedList } from '../../data/paginated-list.model';
import { RemoteData } from '../../data/remote-data'; import { RemoteData } from '../../data/remote-data';
import { lazyService } from '../../lazy-service'; import { lazyDataService } from '../../lazy-data-service';
import { GenericConstructor } from '../../shared/generic-constructor'; import { GenericConstructor } from '../../shared/generic-constructor';
import { HALResource } from '../../shared/hal-resource.model'; import { HALResource } from '../../shared/hal-resource.model';
import { import {
@@ -43,7 +42,7 @@ export class LinkService {
constructor( constructor(
protected injector: Injector, protected injector: Injector,
@Inject(APP_DATA_SERVICES_MAP) private map: InjectionToken<LazyDataServicesMap>, @Inject(APP_DATA_SERVICES_MAP) private map: LazyDataServicesMap,
@Inject(LINK_DEFINITION_FACTORY) private getLinkDefinition: <T extends HALResource>(source: GenericConstructor<T>, linkName: keyof T['_links']) => LinkDefinition<T>, @Inject(LINK_DEFINITION_FACTORY) private getLinkDefinition: <T extends HALResource>(source: GenericConstructor<T>, linkName: keyof T['_links']) => LinkDefinition<T>,
@Inject(LINK_DEFINITION_MAP_FACTORY) private getLinkDefinitions: <T extends HALResource>(source: GenericConstructor<T>) => Map<keyof T['_links'], LinkDefinition<T>>, @Inject(LINK_DEFINITION_MAP_FACTORY) private getLinkDefinitions: <T extends HALResource>(source: GenericConstructor<T>) => Map<keyof T['_links'], LinkDefinition<T>>,
) { ) {
@@ -73,7 +72,7 @@ export class LinkService {
public resolveLinkWithoutAttaching<T extends HALResource, U extends HALResource>(model, linkToFollow: FollowLinkConfig<T>): Observable<RemoteData<U | PaginatedList<U>>> { public resolveLinkWithoutAttaching<T extends HALResource, U extends HALResource>(model, linkToFollow: FollowLinkConfig<T>): Observable<RemoteData<U | PaginatedList<U>>> {
const matchingLinkDef = this.getLinkDefinition(model.constructor, linkToFollow.name); const matchingLinkDef = this.getLinkDefinition(model.constructor, linkToFollow.name);
if (hasValue(matchingLinkDef)) { if (hasValue(matchingLinkDef)) {
const lazyProvider$: Observable<HALDataService<any>> = lazyService(this.map[matchingLinkDef.resourceType.value], this.injector); const lazyProvider$: Observable<HALDataService<any>> = lazyDataService(this.map, matchingLinkDef.resourceType.value, this.injector);
return lazyProvider$.pipe( return lazyProvider$.pipe(
switchMap((provider: HALDataService<any>) => { switchMap((provider: HALDataService<any>) => {
const link = model._links[matchingLinkDef.linkName]; const link = model._links[matchingLinkDef.linkName];

View File

@@ -1,36 +1,27 @@
import { TestBed } from '@angular/core/testing';
import { Router } from '@angular/router';
import { of } from 'rxjs'; import { of } from 'rxjs';
import { NotifyInfoGuard } from './notify-info.guard'; import { notifyInfoGuard } from './notify-info.guard';
import { NotifyInfoService } from './notify-info.service';
describe('NotifyInfoGuard', () => { describe('notifyInfoGuard', () => {
let guard: NotifyInfoGuard; let guard: any;
let notifyInfoServiceSpy: any; let notifyInfoServiceSpy: any;
let router: any; let router: any;
beforeEach(() => { beforeEach(() => {
notifyInfoServiceSpy = jasmine.createSpyObj('NotifyInfoService', ['isCoarConfigEnabled']); notifyInfoServiceSpy = jasmine.createSpyObj('NotifyInfoService', ['isCoarConfigEnabled']);
router = jasmine.createSpyObj('Router', ['parseUrl']); router = jasmine.createSpyObj('Router', ['parseUrl']);
TestBed.configureTestingModule({ guard = notifyInfoGuard;
providers: [
NotifyInfoGuard,
{ provide: NotifyInfoService, useValue: notifyInfoServiceSpy },
{ provide: Router, useValue: router },
],
});
guard = TestBed.inject(NotifyInfoGuard);
}); });
it('should be created', () => { 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) => { it('should return true if COAR config is enabled', (done) => {
notifyInfoServiceSpy.isCoarConfigEnabled.and.returnValue(of(true)); notifyInfoServiceSpy.isCoarConfigEnabled.and.returnValue(of(true));
guard.canActivate(null, null).subscribe((result) => { guard(null, null, notifyInfoServiceSpy, router).subscribe((result) => {
expect(result).toBe(true); expect(result).toBe(true);
done(); done();
}); });
@@ -40,7 +31,7 @@ describe('NotifyInfoGuard', () => {
notifyInfoServiceSpy.isCoarConfigEnabled.and.returnValue(of(false)); notifyInfoServiceSpy.isCoarConfigEnabled.and.returnValue(of(false));
router.parseUrl.and.returnValue(of('/404')); router.parseUrl.and.returnValue(of('/404'));
guard.canActivate(null, null).subscribe(() => { guard(null, null, notifyInfoServiceSpy, router).subscribe(() => {
expect(router.parseUrl).toHaveBeenCalledWith('/404'); expect(router.parseUrl).toHaveBeenCalledWith('/404');
done(); done();
}); });

View File

@@ -1,7 +1,7 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
CanActivate, CanActivateFn,
Router, Router,
RouterStateSnapshot, RouterStateSnapshot,
UrlTree, UrlTree,
@@ -11,27 +11,13 @@ import { map } from 'rxjs/operators';
import { NotifyInfoService } from './notify-info.service'; import { NotifyInfoService } from './notify-info.service';
@Injectable({ export const notifyInfoGuard: CanActivateFn = (
providedIn: 'root', route: ActivatedRouteSnapshot,
}) state: RouterStateSnapshot,
export class NotifyInfoGuard implements CanActivate { notifyInfoService: NotifyInfoService = inject(NotifyInfoService),
constructor( router: Router = inject(Router),
private notifyInfoService: NotifyInfoService, ): Observable<boolean | UrlTree> => {
private router: Router, return notifyInfoService.isCoarConfigEnabled().pipe(
) {} map(isEnabled => isEnabled ? true : router.parseUrl('/404')),
);
canActivate( };
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
): Observable<boolean | UrlTree> {
return this.notifyInfoService.isCoarConfigEnabled().pipe(
map(coarLdnEnabled => {
if (coarLdnEnabled) {
return true;
} else {
return this.router.parseUrl('/404');
}
}),
);
}
}

View File

@@ -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 { POOL_TASK } from './tasks/models/pool-task-object.resource-type';
import { WORKFLOW_ACTION } from './tasks/models/workflow-action-object.resource-type'; import { WORKFLOW_ACTION } from './tasks/models/workflow-action-object.resource-type';
export const LAZY_DATA_SERVICES: LazyDataServicesMap = { export const LAZY_DATA_SERVICES: LazyDataServicesMap = new Map([
[AUTHORIZATION.value]: () => import('./data/feature-authorization/authorization-data.service').then(m => m.AuthorizationDataService), [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), [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), [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), [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), [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), [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), [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), [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), [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), [ACCESS_STATUS.value, () => import('./data/access-status-data.service').then(m => m.AccessStatusDataService)],
[COLLECTION.value]: () => import('./data/collection-data.service').then(m => m.CollectionDataService), [COLLECTION.value, () => import('./data/collection-data.service').then(m => m.CollectionDataService)],
[CLAIMED_TASK.value]: () => import('./tasks/claimed-task-data.service').then(m => m.ClaimedTaskDataService), [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), [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), [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), [LICENSE.value, () => import('./data/href-only-data.service').then(m => m.HrefOnlyDataService)],
[SUBSCRIPTION.value]: () => import('../shared/subscriptions/subscriptions-data.service').then(m => m.SubscriptionsDataService), [SUBSCRIPTION.value, () => import('../shared/subscriptions/subscriptions-data.service').then(m => m.SubscriptionsDataService)],
[COMMUNITY.value]: () => import('./data/community-data.service').then(m => m.CommunityDataService), [COMMUNITY.value, () => import('./data/community-data.service').then(m => m.CommunityDataService)],
[VOCABULARY.value]: () => import('./submission/vocabularies/vocabulary.data.service').then(m => m.VocabularyDataService), [VOCABULARY.value, () => import('./submission/vocabularies/vocabulary.data.service').then(m => m.VocabularyDataService)],
[BUNDLE.value]: () => import('./data/bundle-data.service').then(m => m.BundleDataService), [BUNDLE.value, () => import('./data/bundle-data.service').then(m => m.BundleDataService)],
[CONFIG_PROPERTY.value]: () => import('./data/configuration-data.service').then(m => m.ConfigurationDataService), [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), [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), [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), [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), [WORKSPACEITEM.value, () => import('./submission/workspaceitem-data.service').then(m => m.WorkspaceitemDataService)],
[WORKFLOWITEM.value]: () => import('./submission/workflowitem-data.service').then(m => m.WorkflowItemDataService), [WORKFLOWITEM.value, () => import('./submission/workflowitem-data.service').then(m => m.WorkflowItemDataService)],
[VOCABULARY.value]: () => import('./submission/vocabularies/vocabulary.data.service').then(m => m.VocabularyDataService), [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), [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_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), [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), [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), [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), [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_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), [ORCID_HISTORY.value, () => import('./orcid/orcid-history-data.service').then(m => m.OrcidHistoryDataService)],
[FEEDBACK.value]: () => import('./feedback/feedback-data.service').then(m => m.FeedbackDataService), [FEEDBACK.value, () => import('./feedback/feedback-data.service').then(m => m.FeedbackDataService)],
[GROUP.value]: () => import('./eperson/group-data.service').then(m => m.GroupDataService), [GROUP.value, () => import('./eperson/group-data.service').then(m => m.GroupDataService)],
[EPERSON.value]: () => import('./eperson/eperson-data.service').then(m => m.EPersonDataService), [EPERSON.value, () => import('./eperson/eperson-data.service').then(m => m.EPersonDataService)],
[WORKFLOW_ACTION.value]: () => import('./data/workflow-action-data.service').then(m => m.WorkflowActionDataService), [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), [VERSION_HISTORY.value, () => import('./data/version-history-data.service').then(m => m.VersionHistoryDataService)],
[SITE.value]: () => import('./data/site-data.service').then(m => m.SiteDataService), [SITE.value, () => import('./data/site-data.service').then(m => m.SiteDataService)],
[ROOT.value]: () => import('./data/root-data.service').then(m => m.RootDataService), [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_TYPE.value, () => import('./data/relationship-type-data.service').then(m => m.RelationshipTypeDataService)],
[RELATIONSHIP.value]: () => import('./data/relationship-data.service').then(m => m.RelationshipDataService), [RELATIONSHIP.value, () => import('./data/relationship-data.service').then(m => m.RelationshipDataService)],
[SCRIPT.value]: () => import('./data/processes/script-data.service').then(m => m.ScriptDataService), [SCRIPT.value, () => import('./data/processes/script-data.service').then(m => m.ScriptDataService)],
[PROCESS.value]: () => import('./data/processes/process-data.service').then(m => m.ProcessDataService), [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), [METADATA_FIELD.value, () => import('./data/metadata-field-data.service').then(m => m.MetadataFieldDataService)],
[ITEM.value]: () => import('./data/item-data.service').then(m => m.ItemDataService), [ITEM.value, () => import('./data/item-data.service').then(m => m.ItemDataService)],
[VERSION.value]: () => import('./data/version-data.service').then(m => m.VersionDataService), [VERSION.value, () => import('./data/version-data.service').then(m => m.VersionDataService)],
[IDENTIFIERS.value]: () => import('./data/identifier-data.service').then(m => m.IdentifierDataService), [IDENTIFIERS.value, () => import('./data/identifier-data.service').then(m => m.IdentifierDataService)],
[FEATURE.value]: () => import('./data/feature-authorization/authorization-data.service').then(m => m.AuthorizationDataService), [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), [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), [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), [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_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), [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), [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), [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), [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_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_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), [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.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_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), [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), [DUPLICATE.value, () => import('./submission/submission-duplicate-data.service').then(m => m.SubmissionDuplicateDataService)],
[CorrectionType.type.value]: () => import('./submission/correctiontype-data.service').then(m => m.CorrectionTypeDataService), [CorrectionType.type.value, () => import('./submission/correctiontype-data.service').then(m => m.CorrectionTypeDataService)],
}; ]);

View File

@@ -28,7 +28,10 @@ import { Community } from '../shared/community.model';
import { ContentSource } from '../shared/content-source.model'; import { ContentSource } from '../shared/content-source.model';
import { HALEndpointService } from '../shared/hal-endpoint.service'; import { HALEndpointService } from '../shared/hal-endpoint.service';
import { Item } from '../shared/item.model'; import { Item } from '../shared/item.model';
import { getFirstCompletedRemoteData } from '../shared/operators'; import {
getAllCompletedRemoteData,
getFirstCompletedRemoteData,
} from '../shared/operators';
import { BitstreamDataService } from './bitstream-data.service'; import { BitstreamDataService } from './bitstream-data.service';
import { ComColDataService } from './comcol-data.service'; import { ComColDataService } from './comcol-data.service';
import { CommunityDataService } from './community-data.service'; import { CommunityDataService } from './community-data.service';
@@ -84,7 +87,8 @@ export class CollectionDataService extends ComColDataService<Collection> {
}); });
return this.searchBy(searchHref, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow).pipe( return this.searchBy(searchHref, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow).pipe(
filter((collections: RemoteData<PaginatedList<Collection>>) => !collections.isResponsePending)); getAllCompletedRemoteData(),
);
} }
/** /**
@@ -114,7 +118,8 @@ export class CollectionDataService extends ComColDataService<Collection> {
}); });
return this.searchBy(searchHref, options, true, reRequestOnStale, ...linksToFollow).pipe( return this.searchBy(searchHref, options, true, reRequestOnStale, ...linksToFollow).pipe(
filter((collections: RemoteData<PaginatedList<Collection>>) => !collections.isResponsePending)); getAllCompletedRemoteData(),
);
} }
/** /**
@@ -138,7 +143,8 @@ export class CollectionDataService extends ComColDataService<Collection> {
}); });
return this.searchBy(searchHref, options, reRequestOnStale).pipe( return this.searchBy(searchHref, options, reRequestOnStale).pipe(
filter((collections: RemoteData<PaginatedList<Collection>>) => !collections.isResponsePending)); getAllCompletedRemoteData(),
);
} }
/** /**
* Get all collections the user is authorized to submit to, by community and has the metadata * 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<Collection> {
}); });
return this.searchBy(searchHref, options, true, reRequestOnStale, ...linksToFollow).pipe( return this.searchBy(searchHref, options, true, reRequestOnStale, ...linksToFollow).pipe(
filter((collections: RemoteData<PaginatedList<Collection>>) => !collections.isResponsePending)); getAllCompletedRemoteData(),
);
} }
/** /**
@@ -184,9 +191,8 @@ export class CollectionDataService extends ComColDataService<Collection> {
options.elementsPerPage = 1; options.elementsPerPage = 1;
return this.searchBy(searchHref, options).pipe( return this.searchBy(searchHref, options).pipe(
filter((collections: RemoteData<PaginatedList<Collection>>) => !collections.isResponsePending), getFirstCompletedRemoteData(),
take(1), map((collections: RemoteData<PaginatedList<Collection>>) => collections?.payload?.totalElements > 0),
map((collections: RemoteData<PaginatedList<Collection>>) => collections.payload.totalElements > 0),
); );
} }

View File

@@ -91,7 +91,7 @@ export class DsoRedirectService {
/** /**
* Redirect to a DSpaceObject's path using the given identifier type and ID. * 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]). * 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 id the identifier of the object to retrieve
* @param identifierType the type of the given identifier (defaults to UUID) * @param identifierType the type of the given identifier (defaults to UUID)

View File

@@ -1,7 +1,6 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { import {
filter,
map, map,
switchMap, switchMap,
take, take,
@@ -14,6 +13,7 @@ import { HALEndpointService } from '../shared/hal-endpoint.service';
import { ItemType } from '../shared/item-relationships/item-type.model'; import { ItemType } from '../shared/item-relationships/item-type.model';
import { RelationshipType } from '../shared/item-relationships/relationship-type.model'; import { RelationshipType } from '../shared/item-relationships/relationship-type.model';
import { import {
getAllCompletedRemoteData,
getFirstSucceededRemoteData, getFirstSucceededRemoteData,
getRemoteDataPayload, getRemoteDataPayload,
} from '../shared/operators'; } from '../shared/operators';
@@ -89,8 +89,7 @@ export class EntityTypeDataService extends BaseDataService<ItemType> implements
getAllAuthorizedRelationshipType(options: FindListOptions = {}): Observable<RemoteData<PaginatedList<ItemType>>> { getAllAuthorizedRelationshipType(options: FindListOptions = {}): Observable<RemoteData<PaginatedList<ItemType>>> {
const searchHref = 'findAllByAuthorizedCollection'; const searchHref = 'findAllByAuthorizedCollection';
return this.searchBy(searchHref, options).pipe( return this.searchBy(searchHref, options).pipe(getAllCompletedRemoteData());
filter((type: RemoteData<PaginatedList<ItemType>>) => !type.isResponsePending));
} }
/** /**
@@ -123,8 +122,7 @@ export class EntityTypeDataService extends BaseDataService<ItemType> implements
getAllAuthorizedRelationshipTypeImport(options: FindListOptions = {}): Observable<RemoteData<PaginatedList<ItemType>>> { getAllAuthorizedRelationshipTypeImport(options: FindListOptions = {}): Observable<RemoteData<PaginatedList<ItemType>>> {
const searchHref = 'findAllByAuthorizedExternalSource'; const searchHref = 'findAllByAuthorizedExternalSource';
return this.searchBy(searchHref, options).pipe( return this.searchBy(searchHref, options).pipe(getAllCompletedRemoteData());
filter((type: RemoteData<PaginatedList<ItemType>>) => !type.isResponsePending));
} }
/** /**
@@ -136,15 +134,8 @@ export class EntityTypeDataService extends BaseDataService<ItemType> implements
currentPage: 1, currentPage: 1,
}; };
return this.getAllAuthorizedRelationshipTypeImport(findListOptions).pipe( return this.getAllAuthorizedRelationshipTypeImport(findListOptions).pipe(
map((result: RemoteData<PaginatedList<ItemType>>) => { take(1),
let output: boolean; map((result: RemoteData<PaginatedList<ItemType>>) => result?.payload?.totalElements > 1),
if (result.payload) {
output = ( result.payload.page.length > 1 );
} else {
output = false;
}
return output;
}),
); );
} }

View File

@@ -1,6 +1,6 @@
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
Resolve, ResolveFn,
Router, Router,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } from '@angular/router';
@@ -12,21 +12,30 @@ import {
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
import { AuthService } from '../../../auth/auth.service'; import { AuthService } from '../../../auth/auth.service';
import { DSpaceObject } from '../../../shared/dspace-object.model'; import { DSpaceObject } from '../../../shared/dspace-object.model';
import { Item } from '../../../shared/item.model';
import { RemoteData } from '../../remote-data'; import { RemoteData } from '../../remote-data';
import { AuthorizationDataService } from '../authorization-data.service'; import { AuthorizationDataService } from '../authorization-data.service';
import { FeatureID } from '../feature-id'; import { FeatureID } from '../feature-id';
import { DsoPageSingleFeatureGuard } from './dso-page-single-feature.guard'; import { DsoPageSingleFeatureGuard } from './dso-page-single-feature.guard';
const object = {
self: 'test-selflink',
} as DSpaceObject;
const testResolver: ResolveFn<RemoteData<any>> = () => createSuccessfulRemoteDataObject$(object);
/** /**
* Test implementation of abstract class DsoPageSingleFeatureGuard * Test implementation of abstract class DsoPageSingleFeatureGuard
*/ */
class DsoPageSingleFeatureGuardImpl extends DsoPageSingleFeatureGuard<any> { class DsoPageSingleFeatureGuardImpl extends DsoPageSingleFeatureGuard<any> {
constructor(protected resolver: Resolve<RemoteData<any>>,
protected authorizationService: AuthorizationDataService, protected resolver: ResolveFn<RemoteData<Item>> = testResolver;
constructor(protected authorizationService: AuthorizationDataService,
protected router: Router, protected router: Router,
protected authService: AuthService, protected authService: AuthService,
protected featureID: FeatureID) { protected featureID: FeatureID) {
super(resolver, authorizationService, router, authService); super(authorizationService, router, authService);
} }
getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> { getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
@@ -39,25 +48,16 @@ describe('DsoPageSingleFeatureGuard', () => {
let authorizationService: AuthorizationDataService; let authorizationService: AuthorizationDataService;
let router: Router; let router: Router;
let authService: AuthService; let authService: AuthService;
let resolver: Resolve<RemoteData<any>>;
let object: DSpaceObject;
let route; let route;
let parentRoute; let parentRoute;
function init() { function init() {
object = {
self: 'test-selflink',
} as DSpaceObject;
authorizationService = jasmine.createSpyObj('authorizationService', { authorizationService = jasmine.createSpyObj('authorizationService', {
isAuthorized: observableOf(true), isAuthorized: observableOf(true),
}); });
router = jasmine.createSpyObj('router', { router = jasmine.createSpyObj('router', {
parseUrl: {}, parseUrl: {},
}); });
resolver = jasmine.createSpyObj('resolver', {
resolve: createSuccessfulRemoteDataObject$(object),
});
authService = jasmine.createSpyObj('authService', { authService = jasmine.createSpyObj('authService', {
isAuthenticated: observableOf(true), isAuthenticated: observableOf(true),
}); });
@@ -71,7 +71,7 @@ describe('DsoPageSingleFeatureGuard', () => {
}, },
parent: parentRoute, parent: parentRoute,
}; };
guard = new DsoPageSingleFeatureGuardImpl(resolver, authorizationService, router, authService, undefined); guard = new DsoPageSingleFeatureGuardImpl(authorizationService, router, authService, undefined);
} }
beforeEach(() => { beforeEach(() => {

View File

@@ -1,6 +1,6 @@
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
Resolve, ResolveFn,
Router, Router,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } from '@angular/router';
@@ -12,21 +12,30 @@ import {
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
import { AuthService } from '../../../auth/auth.service'; import { AuthService } from '../../../auth/auth.service';
import { DSpaceObject } from '../../../shared/dspace-object.model'; import { DSpaceObject } from '../../../shared/dspace-object.model';
import { Item } from '../../../shared/item.model';
import { RemoteData } from '../../remote-data'; import { RemoteData } from '../../remote-data';
import { AuthorizationDataService } from '../authorization-data.service'; import { AuthorizationDataService } from '../authorization-data.service';
import { FeatureID } from '../feature-id'; import { FeatureID } from '../feature-id';
import { DsoPageSomeFeatureGuard } from './dso-page-some-feature.guard'; import { DsoPageSomeFeatureGuard } from './dso-page-some-feature.guard';
const object = {
self: 'test-selflink',
} as DSpaceObject;
const testResolver: ResolveFn<RemoteData<any>> = () => createSuccessfulRemoteDataObject$(object);
/** /**
* Test implementation of abstract class DsoPageSomeFeatureGuard * Test implementation of abstract class DsoPageSomeFeatureGuard
*/ */
class DsoPageSomeFeatureGuardImpl extends DsoPageSomeFeatureGuard<any> { class DsoPageSomeFeatureGuardImpl extends DsoPageSomeFeatureGuard<any> {
constructor(protected resolver: Resolve<RemoteData<any>>,
protected authorizationService: AuthorizationDataService, protected resolver: ResolveFn<RemoteData<Item>> = testResolver;
constructor(protected authorizationService: AuthorizationDataService,
protected router: Router, protected router: Router,
protected authService: AuthService, protected authService: AuthService,
protected featureIDs: FeatureID[]) { protected featureIDs: FeatureID[]) {
super(resolver, authorizationService, router, authService); super(authorizationService, router, authService);
} }
getFeatureIDs(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID[]> { getFeatureIDs(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID[]> {
@@ -39,25 +48,17 @@ describe('DsoPageSomeFeatureGuard', () => {
let authorizationService: AuthorizationDataService; let authorizationService: AuthorizationDataService;
let router: Router; let router: Router;
let authService: AuthService; let authService: AuthService;
let resolver: Resolve<RemoteData<any>>;
let object: DSpaceObject;
let route; let route;
let parentRoute; let parentRoute;
function init() { function init() {
object = {
self: 'test-selflink',
} as DSpaceObject;
authorizationService = jasmine.createSpyObj('authorizationService', { authorizationService = jasmine.createSpyObj('authorizationService', {
isAuthorized: observableOf(true), isAuthorized: observableOf(true),
}); });
router = jasmine.createSpyObj('router', { router = jasmine.createSpyObj('router', {
parseUrl: {}, parseUrl: {},
}); });
resolver = jasmine.createSpyObj('resolver', {
resolve: createSuccessfulRemoteDataObject$(object),
});
authService = jasmine.createSpyObj('authService', { authService = jasmine.createSpyObj('authService', {
isAuthenticated: observableOf(true), isAuthenticated: observableOf(true),
}); });
@@ -71,7 +72,7 @@ describe('DsoPageSomeFeatureGuard', () => {
}, },
parent: parentRoute, parent: parentRoute,
}; };
guard = new DsoPageSomeFeatureGuardImpl(resolver, authorizationService, router, authService, []); guard = new DsoPageSomeFeatureGuardImpl(authorizationService, router, authService, []);
} }
beforeEach(() => { beforeEach(() => {

View File

@@ -1,6 +1,6 @@
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
Resolve, ResolveFn,
Router, Router,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } 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 * This guard utilizes a resolver to retrieve the relevant object to check authorizations for
*/ */
export abstract class DsoPageSomeFeatureGuard<T extends DSpaceObject> extends SomeFeatureAuthorizationGuard { export abstract class DsoPageSomeFeatureGuard<T extends DSpaceObject> extends SomeFeatureAuthorizationGuard {
constructor(protected resolver: Resolve<RemoteData<T>>,
protected authorizationService: AuthorizationDataService, protected abstract resolver: ResolveFn<RemoteData<DSpaceObject>>;
constructor(protected authorizationService: AuthorizationDataService,
protected router: Router, protected router: Router,
protected authService: AuthService) { protected authService: AuthService) {
super(authorizationService, router, authService); super(authorizationService, router, authService);
@@ -35,14 +37,14 @@ export abstract class DsoPageSomeFeatureGuard<T extends DSpaceObject> extends So
*/ */
getObjectUrl(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<string> { getObjectUrl(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<string> {
const routeWithObjectID = this.getRouteWithDSOId(route); const routeWithObjectID = this.getRouteWithDSOId(route);
return (this.resolver.resolve(routeWithObjectID, state) as Observable<RemoteData<T>>).pipe( return (this.resolver(routeWithObjectID, state) as Observable<RemoteData<T>>).pipe(
getAllSucceededRemoteDataPayload(), getAllSucceededRemoteDataPayload(),
map((dso) => dso.self), 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 * @param route The current route
*/ */
protected getRouteWithDSOId(route: ActivatedRouteSnapshot): ActivatedRouteSnapshot { protected getRouteWithDSOId(route: ActivatedRouteSnapshot): ActivatedRouteSnapshot {

View File

@@ -1,6 +1,5 @@
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
CanActivate,
Router, Router,
RouterStateSnapshot, RouterStateSnapshot,
UrlTree, UrlTree,
@@ -22,7 +21,7 @@ import { FeatureID } from '../feature-id';
* doesn't have authorized rights on any of the specified features and/or object. * 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. * 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, constructor(protected authorizationService: AuthorizationDataService,
protected router: Router, protected router: Router,
protected authService: AuthService) { protected authService: AuthService) {

View File

@@ -1,5 +1,6 @@
import { Injector } from '@angular/core'; import { Injector } from '@angular/core';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { createMockStore } from '@ngrx/store/testing';
import { of as observableOf } from 'rxjs'; import { of as observableOf } from 'rxjs';
import { Notification } from '../../../shared/notifications/models/notification.model'; import { Notification } from '../../../shared/notifications/models/notification.model';
@@ -52,7 +53,7 @@ describe('ObjectUpdatesService', () => {
const objectEntry = { const objectEntry = {
fieldStates, fieldUpdates, lastModified: modDate, virtualMetadataSources: {}, patchOperationService, fieldStates, fieldUpdates, lastModified: modDate, virtualMetadataSources: {}, patchOperationService,
}; };
store = new Store<CoreState>(undefined, undefined, undefined); store = createMockStore({});
spyOn(store, 'dispatch'); spyOn(store, 'dispatch');
injector = jasmine.createSpyObj('injector', { injector = jasmine.createSpyObj('injector', {
get: patchOperationService, get: patchOperationService,

View File

@@ -13,6 +13,7 @@ import {
map, map,
mergeMap, mergeMap,
take, take,
withLatestFrom,
} from 'rxjs/operators'; } from 'rxjs/operators';
import { import {
@@ -25,6 +26,7 @@ import { ParsedResponse } from '../cache/response.models';
import { DSpaceSerializer } from '../dspace-rest/dspace.serializer'; import { DSpaceSerializer } from '../dspace-rest/dspace.serializer';
import { DspaceRestService } from '../dspace-rest/dspace-rest.service'; import { DspaceRestService } from '../dspace-rest/dspace-rest.service';
import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; import { RawRestResponse } from '../dspace-rest/raw-rest-response.model';
import { XSRFService } from '../xsrf/xsrf.service';
import { import {
RequestActionTypes, RequestActionTypes,
RequestErrorAction, RequestErrorAction,
@@ -35,6 +37,7 @@ import {
import { RequestService } from './request.service'; import { RequestService } from './request.service';
import { RequestEntry } from './request-entry.model'; import { RequestEntry } from './request-entry.model';
import { RequestError } from './request-error.model'; import { RequestError } from './request-error.model';
import { RestRequestMethod } from './rest-request-method';
import { RestRequestWithResponseParser } from './rest-request-with-response-parser.model'; import { RestRequestWithResponseParser } from './rest-request-with-response-parser.model';
@Injectable() @Injectable()
@@ -48,7 +51,11 @@ export class RequestEffects {
); );
}), }),
filter((entry: RequestEntry) => hasValue(entry)), 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) => { mergeMap((request: RestRequestWithResponseParser) => {
let body = request.body; let body = request.body;
if (isNotEmpty(request.body) && !request.isMultipart) { if (isNotEmpty(request.body) && !request.isMultipart) {
@@ -89,6 +96,7 @@ export class RequestEffects {
private restApi: DspaceRestService, private restApi: DspaceRestService,
private injector: Injector, private injector: Injector,
protected requestService: RequestService, protected requestService: RequestService,
protected xsrfService: XSRFService,
) { } ) { }
} }

View File

@@ -1,5 +1,6 @@
import { import {
fakeAsync, fakeAsync,
flush,
TestBed, TestBed,
waitForAsync, waitForAsync,
} from '@angular/core/testing'; } from '@angular/core/testing';
@@ -16,7 +17,6 @@ import {
getTestScheduler, getTestScheduler,
} from 'jasmine-marbles'; } from 'jasmine-marbles';
import { import {
BehaviorSubject,
EMPTY, EMPTY,
Observable, Observable,
of as observableOf, of as observableOf,
@@ -33,7 +33,6 @@ import { ObjectCacheService } from '../cache/object-cache.service';
import { coreReducers } from '../core.reducers'; import { coreReducers } from '../core.reducers';
import { CoreState } from '../core-state.model'; import { CoreState } from '../core-state.model';
import { UUIDService } from '../shared/uuid.service'; import { UUIDService } from '../shared/uuid.service';
import { XSRFService } from '../xsrf/xsrf.service';
import { import {
RequestConfigureAction, RequestConfigureAction,
RequestExecuteAction, RequestExecuteAction,
@@ -61,7 +60,6 @@ describe('RequestService', () => {
let uuidService: UUIDService; let uuidService: UUIDService;
let store: Store<CoreState>; let store: Store<CoreState>;
let mockStore: MockStore<CoreState>; let mockStore: MockStore<CoreState>;
let xsrfService: XSRFService;
const testUUID = '5f2a0d2a-effa-4d54-bd54-5663b960f9eb'; const testUUID = '5f2a0d2a-effa-4d54-bd54-5663b960f9eb';
const testHref = 'https://rest.api/endpoint/selfLink'; const testHref = 'https://rest.api/endpoint/selfLink';
@@ -107,16 +105,11 @@ describe('RequestService', () => {
store = TestBed.inject(Store); store = TestBed.inject(Store);
mockStore = store as MockStore<CoreState>; mockStore = store as MockStore<CoreState>;
mockStore.setState(initialState); mockStore.setState(initialState);
xsrfService = {
tokenInitialized$: new BehaviorSubject(false),
} as XSRFService;
service = new RequestService( service = new RequestService(
objectCache, objectCache,
uuidService, uuidService,
store, store,
xsrfService,
undefined,
); );
serviceAsAny = service as any; serviceAsAny = service as any;
}); });
@@ -509,21 +502,23 @@ describe('RequestService', () => {
dispatchSpy = spyOn(store, 'dispatch'); dispatchSpy = spyOn(store, 'dispatch');
}); });
it('should dispatch a RequestConfigureAction', () => { it('should dispatch a RequestConfigureAction', fakeAsync(() => {
const request = testGetRequest; const request = testGetRequest;
serviceAsAny.dispatchRequest(request); serviceAsAny.dispatchRequest(request);
flush();
const firstAction = dispatchSpy.calls.argsFor(0)[0]; const firstAction = dispatchSpy.calls.argsFor(0)[0];
expect(firstAction).toBeInstanceOf(RequestConfigureAction); expect(firstAction).toBeInstanceOf(RequestConfigureAction);
expect(firstAction.payload).toEqual(request); expect(firstAction.payload).toEqual(request);
}); }));
it('should dispatch a RequestExecuteAction', () => { it('should dispatch a RequestExecuteAction', fakeAsync(() => {
const request = testGetRequest; const request = testGetRequest;
serviceAsAny.dispatchRequest(request); serviceAsAny.dispatchRequest(request);
flush();
const secondAction = dispatchSpy.calls.argsFor(1)[0]; const secondAction = dispatchSpy.calls.argsFor(1)[0];
expect(secondAction).toBeInstanceOf(RequestExecuteAction); expect(secondAction).toBeInstanceOf(RequestExecuteAction);
expect(secondAction.payload).toEqual(request.uuid); expect(secondAction.payload).toEqual(request.uuid);
}); }));
describe('when it\'s not a GET request', () => { describe('when it\'s not a GET request', () => {
it('shouldn\'t track it', () => { it('shouldn\'t track it', () => {

View File

@@ -8,6 +8,7 @@ import {
} from '@ngrx/store'; } from '@ngrx/store';
import cloneDeep from 'lodash/cloneDeep'; import cloneDeep from 'lodash/cloneDeep';
import { import {
asapScheduler,
from as observableFrom, from as observableFrom,
Observable, Observable,
} from 'rxjs'; } from 'rxjs';
@@ -33,16 +34,12 @@ import { ObjectCacheService } from '../cache/object-cache.service';
import { CommitSSBAction } from '../cache/server-sync-buffer.actions'; import { CommitSSBAction } from '../cache/server-sync-buffer.actions';
import { coreSelector } from '../core.selectors'; import { coreSelector } from '../core.selectors';
import { CoreState } from '../core-state.model'; import { CoreState } from '../core-state.model';
import { import { IndexState } from '../index/index.reducer';
IndexState,
MetaIndexState,
} from '../index/index.reducer';
import { import {
getUrlWithoutEmbedParams, getUrlWithoutEmbedParams,
requestIndexSelector, requestIndexSelector,
} from '../index/index.selectors'; } from '../index/index.selectors';
import { UUIDService } from '../shared/uuid.service'; import { UUIDService } from '../shared/uuid.service';
import { XSRFService } from '../xsrf/xsrf.service';
import { import {
RequestConfigureAction, RequestConfigureAction,
RequestExecuteAction, RequestExecuteAction,
@@ -168,9 +165,7 @@ export class RequestService {
constructor(private objectCache: ObjectCacheService, constructor(private objectCache: ObjectCacheService,
private uuidService: UUIDService, private uuidService: UUIDService,
private store: Store<CoreState>, private store: Store<CoreState>) {
protected xsrfService: XSRFService,
private indexStore: Store<MetaIndexState>) {
} }
generateRequestId(): string { generateRequestId(): string {
@@ -243,7 +238,7 @@ export class RequestService {
return source.pipe( return source.pipe(
tap((entry: RequestEntry) => { tap((entry: RequestEntry) => {
if (hasValue(entry) && hasValue(entry.request) && !isStale(entry.state) && !isValid(entry)) { 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)));
} }
}), }),
); );
@@ -396,6 +391,7 @@ export class RequestService {
const requestEntry$ = this.getByHref(href); const requestEntry$ = this.getByHref(href);
requestEntry$.pipe( requestEntry$.pipe(
filter((re: RequestEntry) => isNotEmpty(re)),
map((re: RequestEntry) => re.request.uuid), map((re: RequestEntry) => re.request.uuid),
take(1), take(1),
).subscribe((uuid: string) => { ).subscribe((uuid: string) => {
@@ -451,18 +447,10 @@ export class RequestService {
* @param {RestRequest} request to dispatch * @param {RestRequest} request to dispatch
*/ */
private dispatchRequest(request: RestRequest) { private dispatchRequest(request: RestRequest) {
this.store.dispatch(new RequestConfigureAction(request)); asapScheduler.schedule(() => {
// If it's a GET request, or we have an XSRF token, dispatch it immediately this.store.dispatch(new RequestConfigureAction(request));
if (request.method === RestRequestMethod.GET || this.xsrfService.tokenInitialized$.getValue() === true) {
this.store.dispatch(new RequestExecuteAction(request.uuid)); this.store.dispatch(new RequestExecuteAction(request.uuid));
} else { });
// Otherwise wait for the XSRF token first
this.xsrfService.tokenInitialized$.pipe(
find((hasInitialized: boolean) => hasInitialized === true),
).subscribe(() => {
this.store.dispatch(new RequestExecuteAction(request.uuid));
});
}
} }
/** /**

View File

@@ -1,6 +1,5 @@
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
CanActivate,
Router, Router,
RouterStateSnapshot, RouterStateSnapshot,
UrlTree, 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 * 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 * That condition is defined by abstract method hasAccepted
*/ */
export abstract class AbstractEndUserAgreementGuard implements CanActivate { export abstract class AbstractEndUserAgreementGuard {
constructor(protected router: Router) { constructor(protected router: Router) {
} }

View File

@@ -6,6 +6,7 @@ import {
Store, Store,
StoreModule, StoreModule,
} from '@ngrx/store'; } from '@ngrx/store';
import { createMockStore } from '@ngrx/store/testing';
import { import {
TranslateLoader, TranslateLoader,
TranslateModule, TranslateModule,
@@ -104,7 +105,7 @@ describe('GroupDataService', () => {
beforeEach(() => { beforeEach(() => {
init(); init();
requestService = getMockRequestService(createRequestEntry$(groups)); requestService = getMockRequestService(createRequestEntry$(groups));
store = new Store<CoreState>(undefined, undefined, undefined); store = createMockStore({});
service = initTestService(); service = initTestService();
spyOn(store, 'dispatch'); spyOn(store, 'dispatch');
spyOn(rdbService, 'buildFromRequestUUIDAndAwait').and.callThrough(); spyOn(rdbService, 'buildFromRequestUUIDAndAwait').and.callThrough();

View File

@@ -1,7 +1,7 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
CanActivate, CanActivateFn,
RouterStateSnapshot, RouterStateSnapshot,
UrlTree, UrlTree,
} from '@angular/router'; } from '@angular/router';
@@ -11,16 +11,13 @@ import { AuthorizationDataService } from '../data/feature-authorization/authoriz
import { FeatureID } from '../data/feature-authorization/feature-id'; 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 const feedbackGuard: CanActivateFn = (
export class FeedbackGuard implements CanActivate { route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
authorizationService: AuthorizationDataService = inject(AuthorizationDataService),
): Observable<boolean | UrlTree> => {
return authorizationService.isAuthorized(FeatureID.CanSendFeedback);
};
constructor(private authorizationService: AuthorizationDataService) {
}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> {
return this.authorizationService.isAuthorized(FeatureID.CanSendFeedback);
}
}

View File

@@ -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<T>(
dataServicesMap: LazyDataServicesMap,
key: string,
injector: Injector,
): Observable<T> {
return defer(() => {
if (dataServicesMap.has(key) && typeof dataServicesMap.get(key) === 'function') {
const loader: () => Promise<Type<HALDataService<any>> | { default: HALDataService<any> }> = 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;
}
});
}

View File

@@ -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<T>(
loader: () => Promise<Type<T>> | Promise<{ default: Type<T> }>,
injector: Injector,
): Observable<T> {
return defer(() => {
return loader()
.then((serviceOrDefault) => {
if ('default' in serviceOrDefault) {
return injector!.get(serviceOrDefault.default);
}
return injector!.get(serviceOrDefault);
})
.catch((error) => {
throw error;
});
});
}

View File

@@ -10,7 +10,7 @@ import {
NavigationEnd, NavigationEnd,
Router, Router,
} from '@angular/router'; } from '@angular/router';
import { getMockStore } from '@ngrx/store/testing'; import { createMockStore } from '@ngrx/store/testing';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { import {
Observable, Observable,
@@ -106,8 +106,7 @@ describe('MetadataService', () => {
isAuthorized: observableOf(true), isAuthorized: observableOf(true),
}); });
// @ts-ignore store = createMockStore({ initialState });
store = getMockStore({ initialState });
spyOn(store, 'dispatch'); spyOn(store, 'dispatch');
appConfig = { appConfig = {

View File

@@ -2,17 +2,17 @@ import { Router } from '@angular/router';
import { AppConfig } from '../../../config/app-config.interface'; import { AppConfig } from '../../../config/app-config.interface';
import { DefaultAppConfig } from '../../../config/default-app-config'; import { DefaultAppConfig } from '../../../config/default-app-config';
import { ReloadGuard } from './reload.guard'; import { reloadGuard } from './reload.guard';
describe('ReloadGuard', () => { describe('reloadGuard', () => {
let guard: ReloadGuard; let guard: any;
let router: Router; let router: Router;
let appConfig: AppConfig; let appConfig: AppConfig;
beforeEach(() => { beforeEach(() => {
router = jasmine.createSpyObj('router', ['parseUrl', 'createUrlTree']); router = jasmine.createSpyObj('router', ['parseUrl', 'createUrlTree']);
appConfig = new DefaultAppConfig(); appConfig = new DefaultAppConfig();
guard = new ReloadGuard(router, appConfig); guard = reloadGuard;
}); });
describe('canActivate', () => { describe('canActivate', () => {
@@ -31,7 +31,7 @@ describe('ReloadGuard', () => {
}); });
it('should create a UrlTree with the redirect URL', () => { 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)); expect(router.parseUrl).toHaveBeenCalledWith(redirectUrl.substring(1));
}); });
}); });
@@ -44,7 +44,7 @@ describe('ReloadGuard', () => {
}); });
it('should create a UrlTree to home', () => { it('should create a UrlTree to home', () => {
guard.canActivate(route, undefined); guard(route, undefined, appConfig, router);
expect(router.createUrlTree).toHaveBeenCalledWith(['home']); expect(router.createUrlTree).toHaveBeenCalledWith(['home']);
}); });
}); });

View File

@@ -1,10 +1,7 @@
import { import { inject } from '@angular/core';
Inject,
Injectable,
} from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
CanActivate, CanActivateFn,
Router, Router,
RouterStateSnapshot, RouterStateSnapshot,
UrlTree, UrlTree,
@@ -14,33 +11,25 @@ import {
APP_CONFIG, APP_CONFIG,
AppConfig, AppConfig,
} from '../../../config/app-config.interface'; } from '../../../config/app-config.interface';
import { HOME_PAGE_PATH } from '../../app-routing-paths';
import { isNotEmpty } from '../../shared/empty.util'; import { isNotEmpty } from '../../shared/empty.util';
/** /**
* A guard redirecting the user to the URL provided in the route's query params * 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 * When no redirect url is found, the user is redirected to the homepage
*/ */
@Injectable({ providedIn: 'root' }) export const reloadGuard: CanActivateFn = (
export class ReloadGuard implements CanActivate { route: ActivatedRouteSnapshot,
constructor( state: RouterStateSnapshot,
private router: Router, appConfig: AppConfig = inject(APP_CONFIG),
@Inject(APP_CONFIG) private appConfig: AppConfig, 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']);
}
}
}

View File

@@ -16,7 +16,7 @@ import { ServerCheckGuard } from './server-check.guard';
import SpyObj = jasmine.SpyObj; import SpyObj = jasmine.SpyObj;
describe('ServerCheckGuard', () => { describe('ServerCheckGuard', () => {
let guard: ServerCheckGuard; let guard: any;
let router: Router; let router: Router;
let eventSubject: ReplaySubject<RouterEvent>; let eventSubject: ReplaySubject<RouterEvent>;
let rootDataServiceStub: SpyObj<RootDataService>; let rootDataServiceStub: SpyObj<RootDataService>;
@@ -39,7 +39,7 @@ describe('ServerCheckGuard', () => {
navigateByUrl: jasmine.createSpy('navigateByUrl'), navigateByUrl: jasmine.createSpy('navigateByUrl'),
parseUrl: jasmine.createSpy('parseUrl').and.returnValue(redirectUrlTree), parseUrl: jasmine.createSpy('parseUrl').and.returnValue(redirectUrlTree),
} as any; } as any;
guard = new ServerCheckGuard(router, rootDataServiceStub); guard = ServerCheckGuard;
}); });
it('should be created', () => { it('should be created', () => {
@@ -53,7 +53,7 @@ describe('ServerCheckGuard', () => {
it('should return true', () => { it('should return true', () => {
testScheduler.run(({ expectObservable }) => { 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 }); 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', () => { it('should return a UrlTree with the route to the 500 error page', () => {
testScheduler.run(({ expectObservable }) => { 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 }); expectObservable(result$).toBe('(b|)', { b: redirectUrlTree });
}); });
expect(router.parseUrl).toHaveBeenCalledWith('/500'); expect(router.parseUrl).toHaveBeenCalledWith('/500');
}); });
}); });
describe(`listenForRouteChanges`, () => { xdescribe(`listenForRouteChanges`, () => {
it(`should invalidate the root cache, when the method is first called`, () => { it(`should invalidate the root cache, when the method is first called`, () => {
testScheduler.run(() => { testScheduler.run(() => {
guard.listenForRouteChanges(); guard.listenForRouteChanges();

View File

@@ -1,15 +1,13 @@
import { Injectable } from '@angular/core'; import { inject } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
CanActivateChild, CanActivateChildFn,
NavigationStart,
Router, Router,
RouterStateSnapshot, RouterStateSnapshot,
UrlTree, UrlTree,
} from '@angular/router'; } from '@angular/router';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { import {
filter,
map, map,
take, take,
} from 'rxjs/operators'; } from 'rxjs/operators';
@@ -17,52 +15,18 @@ import {
import { getPageInternalServerErrorRoute } from '../../app-routing-paths'; import { getPageInternalServerErrorRoute } from '../../app-routing-paths';
import { RootDataService } from '../data/root-data.service'; import { RootDataService } from '../data/root-data.service';
@Injectable({
providedIn: 'root',
})
/** /**
* A guard that checks if root api endpoint is reachable. * A guard that checks if root api endpoint is reachable.
* If not redirect to 500 error page * If not redirect to 500 error page
*/ */
export class ServerCheckGuard implements CanActivateChild { export const ServerCheckGuard: CanActivateChildFn = (
constructor(private router: Router, private rootDataService: RootDataService) { route: ActivatedRouteSnapshot,
} state: RouterStateSnapshot,
rootDataService: RootDataService = inject(RootDataService),
/** router: Router = inject(Router),
* True when root api endpoint is reachable. ): Observable<boolean | UrlTree> => {
*/ return rootDataService.checkServerAvailability().pipe(
canActivateChild( take(1),
route: ActivatedRouteSnapshot, map((isAvailable: boolean) => isAvailable ? true : router.parseUrl(getPageInternalServerErrorRoute())),
state: RouterStateSnapshot, );
): Observable<boolean | UrlTree> { };
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();
});
}
}

View File

@@ -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<boolean>;
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<boolean>(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<any> {
if (environment.markdown.mathjax) {
return new Promise<void>((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<boolean> {
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]);
}
}
}

View File

@@ -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<any> {
return Promise.resolve();
}
ready(): Observable<boolean> {
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();
});
});

View File

@@ -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<any>;
abstract ready(): Observable<boolean>;
abstract render(element: HTMLElement): void;
}

View File

@@ -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<boolean>;
protected mathJaxOptions = {};
protected mathJax: MathJaxConfig = {
source: '',
id: '',
};
protected mathJaxFallback: MathJaxConfig = {
source: '',
id: '',
};
constructor() {
super();
this.isReady$ = new BehaviorSubject<boolean>(false);
this.isReady$.next(true);
}
protected async registerMathJaxAsync(config: MathJaxConfig): Promise<any> {
return Promise.resolve();
}
ready(): Observable<boolean> {
return this.isReady$;
}
render(element: HTMLElement) {
return;
}
}

View File

@@ -1,7 +1,5 @@
import { Injectable } from '@angular/core';
import { import {
ActivatedRouteSnapshot, ActivatedRouteSnapshot,
Resolve,
RouterStateSnapshot, RouterStateSnapshot,
} from '@angular/router'; } from '@angular/router';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
@@ -9,36 +7,31 @@ import { switchMap } from 'rxjs/operators';
import { IdentifiableDataService } from '../../data/base/identifiable-data.service'; import { IdentifiableDataService } from '../../data/base/identifiable-data.service';
import { RemoteData } from '../../data/remote-data'; import { RemoteData } from '../../data/remote-data';
import { Item } from '../../shared/item.model';
import { getFirstCompletedRemoteData } from '../../shared/operators'; import { getFirstCompletedRemoteData } from '../../shared/operators';
import { SubmissionObject } from '../models/submission-object.model';
import { SUBMISSION_LINKS_TO_FOLLOW } from './submission-links-to-follow'; import { SUBMISSION_LINKS_TO_FOLLOW } from './submission-links-to-follow';
/** /**
* 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<SubmissionObject> } dataService
* @returns Observable<<RemoteData<Item>> Emits the found item based on the parameters in the current route,
* or an error if something went wrong
*/ */
@Injectable({ providedIn: 'root' }) export const SubmissionObjectResolver: (route: ActivatedRouteSnapshot, state: RouterStateSnapshot, dataService: IdentifiableDataService<SubmissionObject>) => Observable<RemoteData<Item>> = (
export class SubmissionObjectResolver<T> implements Resolve<RemoteData<T>> { route: ActivatedRouteSnapshot,
constructor( state: RouterStateSnapshot,
protected dataService: IdentifiableDataService<any>, dataService: IdentifiableDataService<SubmissionObject>,
) { ): Observable<RemoteData<Item>> => {
} return dataService.findById(route.params.id,
true,
/** false,
* Method for resolving an item based on the parameters in the current route ...SUBMISSION_LINKS_TO_FOLLOW,
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot ).pipe(
* @param {RouterStateSnapshot} state The current RouterStateSnapshot getFirstCompletedRemoteData(),
* @returns Observable<<RemoteData<Item>> Emits the found item based on the parameters in the current route, switchMap((wfiRD: RemoteData<any>) => wfiRD.payload.item as Observable<RemoteData<Item>>),
* or an error if something went wrong getFirstCompletedRemoteData(),
*/ );
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<T>> { };
const itemRD$ = this.dataService.findById(route.params.id,
true,
false,
...SUBMISSION_LINKS_TO_FOLLOW,
).pipe(
getFirstCompletedRemoteData(),
switchMap((wfiRD: RemoteData<any>) => wfiRD.payload.item as Observable<RemoteData<T>>),
getFirstCompletedRemoteData(),
);
return itemRD$;
}
}

View File

@@ -38,9 +38,9 @@ const REINSTATE_BTN = 'reinstate';
const SAVE_BTN = 'save'; const SAVE_BTN = 'save';
const DISCARD_BTN = 'discard'; const DISCARD_BTN = 'discard';
const mockDataServiceMap: any = { const mockDataServiceMap: any = new Map([
[ITEM.value]: () => import('../../shared/testing/test-data-service.mock').then(m => m.TestDataService), [ITEM.value, () => import('../../shared/testing/test-data-service.mock').then(m => m.TestDataService)],
}; ]);
describe('DsoEditMetadataComponent', () => { describe('DsoEditMetadataComponent', () => {
let component: DsoEditMetadataComponent; let component: DsoEditMetadataComponent;

Some files were not shown because too many files have changed in this diff Show More