forked from hazza/dspace-angular
Merge remote-tracking branch 'origin/main' into poc-eslint-plugin-autofix-selectors
This commit is contained in:
19
.github/workflows/build.yml
vendored
19
.github/workflows/build.yml
vendored
@@ -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 }}
|
||||||
@@ -107,10 +107,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
|
||||||
|
|
||||||
@@ -141,19 +141,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)
|
||||||
@@ -203,7 +203,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.
|
||||||
@@ -213,11 +213,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
|
||||||
|
2
.github/workflows/docker.yml
vendored
2
.github/workflows/docker.yml
vendored
@@ -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:
|
||||||
|
2
.github/workflows/issue_opened.yml
vendored
2
.github/workflows/issue_opened.yml
vendored
@@ -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
|
||||||
|
2
.github/workflows/pull_request_opened.yml
vendored
2
.github/workflows/pull_request_opened.yml
vendored
@@ -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
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,4 +1,5 @@
|
|||||||
/.angular/cache
|
/.angular/cache
|
||||||
|
/.nx
|
||||||
/__build__
|
/__build__
|
||||||
/__server_build__
|
/__server_build__
|
||||||
/node_modules
|
/node_modules
|
||||||
|
18
angular.json
18
angular.json
@@ -109,22 +109,22 @@
|
|||||||
"serve": {
|
"serve": {
|
||||||
"builder": "@angular-builders/custom-webpack:dev-server",
|
"builder": "@angular-builders/custom-webpack:dev-server",
|
||||||
"options": {
|
"options": {
|
||||||
"browserTarget": "dspace-angular:build",
|
"buildTarget": "dspace-angular:build",
|
||||||
"port": 4000
|
"port": 4000
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"development": {
|
"development": {
|
||||||
"browserTarget": "dspace-angular:build:development"
|
"buildTarget": "dspace-angular:build:development"
|
||||||
},
|
},
|
||||||
"production": {
|
"production": {
|
||||||
"browserTarget": "dspace-angular:build:production"
|
"buildTarget": "dspace-angular:build:production"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"extract-i18n": {
|
"extract-i18n": {
|
||||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||||
"options": {
|
"options": {
|
||||||
"browserTarget": "dspace-angular:build"
|
"buildTarget": "dspace-angular:build"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"test": {
|
"test": {
|
||||||
@@ -217,23 +217,23 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"serve-ssr": {
|
"serve-ssr": {
|
||||||
"builder": "@nguniversal/builders:ssr-dev-server",
|
"builder": "@angular-devkit/build-angular:ssr-dev-server",
|
||||||
"options": {
|
"options": {
|
||||||
"browserTarget": "dspace-angular:build",
|
"buildTarget": "dspace-angular:build",
|
||||||
"serverTarget": "dspace-angular:server",
|
"serverTarget": "dspace-angular:server",
|
||||||
"port": 4000
|
"port": 4000
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
"browserTarget": "dspace-angular:build:production",
|
"buildTarget": "dspace-angular:build:production",
|
||||||
"serverTarget": "dspace-angular:server:production"
|
"serverTarget": "dspace-angular:server:production"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"prerender": {
|
"prerender": {
|
||||||
"builder": "@nguniversal/builders:prerender",
|
"builder": "@angular-devkit/build-angular:prerender",
|
||||||
"options": {
|
"options": {
|
||||||
"browserTarget": "dspace-angular:build:production",
|
"buildTarget": "dspace-angular:build:production",
|
||||||
"serverTarget": "dspace-angular:server:production",
|
"serverTarget": "dspace-angular:server:production",
|
||||||
"routes": [
|
"routes": [
|
||||||
"/"
|
"/"
|
||||||
|
@@ -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.
|
||||||
@@ -400,10 +407,11 @@ mediaViewer:
|
|||||||
|
|
||||||
# Whether the end user agreement is required before users use the repository.
|
# Whether the end user agreement is required before users use the repository.
|
||||||
# If enabled, the user will be required to accept the agreement before they can use the repository.
|
# If enabled, the user will be required to accept the agreement before they can use the repository.
|
||||||
# And whether the privacy statement should exist or not.
|
# And whether the privacy statement/COAR notify support page should exist or not.
|
||||||
info:
|
info:
|
||||||
enableEndUserAgreement: true
|
enableEndUserAgreement: true
|
||||||
enablePrivacyStatement: true
|
enablePrivacyStatement: true
|
||||||
|
enableCOARNotifySupport: true
|
||||||
|
|
||||||
# Whether to enable Markdown (https://commonmark.org/) and MathJax (https://www.mathjax.org/)
|
# Whether to enable Markdown (https://commonmark.org/) and MathJax (https://www.mathjax.org/)
|
||||||
# display in supported metadata fields. By default, only dc.description.abstract is supported.
|
# display in supported metadata fields. By default, only dc.description.abstract is supported.
|
||||||
|
@@ -4,10 +4,11 @@
|
|||||||
"**/*.ts"
|
"**/*.ts"
|
||||||
],
|
],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"sourceMap": false,
|
||||||
"types": [
|
"types": [
|
||||||
"cypress",
|
"cypress",
|
||||||
"cypress-axe",
|
"cypress-axe",
|
||||||
"node"
|
"node"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
58
package.json
58
package.json
@@ -60,17 +60,18 @@
|
|||||||
"ts-node": "10.2.1"
|
"ts-node": "10.2.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "^16.2.12",
|
"@angular/animations": "^17.3.4",
|
||||||
"@angular/cdk": "^16.2.12",
|
"@angular/cdk": "^17.3.4",
|
||||||
"@angular/common": "^16.2.12",
|
"@angular/common": "^17.3.4",
|
||||||
"@angular/compiler": "^16.2.12",
|
"@angular/compiler": "^17.3.4",
|
||||||
"@angular/core": "^16.2.12",
|
"@angular/core": "^17.3.4",
|
||||||
"@angular/forms": "^16.2.12",
|
"@angular/forms": "^17.3.4",
|
||||||
"@angular/localize": "16.2.12",
|
"@angular/localize": "17.3.4",
|
||||||
"@angular/platform-browser": "^16.2.12",
|
"@angular/platform-browser": "^17.3.4",
|
||||||
"@angular/platform-browser-dynamic": "^16.2.12",
|
"@angular/platform-browser-dynamic": "^17.3.4",
|
||||||
"@angular/platform-server": "^16.2.12",
|
"@angular/platform-server": "^17.3.4",
|
||||||
"@angular/router": "^16.2.12",
|
"@angular/router": "^17.3.4",
|
||||||
|
"@angular/ssr": "^17.3.0",
|
||||||
"@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",
|
||||||
@@ -78,10 +79,9 @@
|
|||||||
"@ng-bootstrap/ng-bootstrap": "^11.0.0",
|
"@ng-bootstrap/ng-bootstrap": "^11.0.0",
|
||||||
"@ng-dynamic-forms/core": "^16.0.0",
|
"@ng-dynamic-forms/core": "^16.0.0",
|
||||||
"@ng-dynamic-forms/ui-ng-bootstrap": "^16.0.0",
|
"@ng-dynamic-forms/ui-ng-bootstrap": "^16.0.0",
|
||||||
"@ngrx/effects": "^16.3.0",
|
"@ngrx/effects": "^17.1.1",
|
||||||
"@ngrx/router-store": "^16.3.0",
|
"@ngrx/router-store": "^17.1.1",
|
||||||
"@ngrx/store": "^16.3.0",
|
"@ngrx/store": "^17.1.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",
|
||||||
@@ -137,25 +137,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.13.3"
|
"zone.js": "~0.14.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-builders/custom-webpack": "~16.0.0",
|
"@angular-builders/custom-webpack": "~17.0.1",
|
||||||
"@angular-devkit/build-angular": "^16.2.12",
|
"@angular-devkit/build-angular": "^17.3.0",
|
||||||
"@angular-eslint/builder": "16.3.1",
|
"@angular-eslint/builder": "17.2.1",
|
||||||
"@angular-eslint/bundled-angular-compiler": "16.3.1",
|
"@angular-eslint/bundled-angular-compiler": "16.3.1",
|
||||||
"@angular-eslint/eslint-plugin": "16.3.1",
|
"@angular-eslint/eslint-plugin": "17.2.1",
|
||||||
"@angular-eslint/eslint-plugin-template": "16.3.1",
|
"@angular-eslint/eslint-plugin-template": "17.2.1",
|
||||||
"@angular-eslint/schematics": "16.3.1",
|
"@angular-eslint/schematics": "17.2.1",
|
||||||
"@angular-eslint/template-parser": "16.3.1",
|
"@angular-eslint/template-parser": "17.2.1",
|
||||||
"@angular/cli": "^16.2.12",
|
"@angular/cli": "^17.3.0",
|
||||||
"@angular/compiler-cli": "^16.2.12",
|
"@angular/compiler-cli": "^17.3.4",
|
||||||
"@angular/language-service": "^16.2.12",
|
"@angular/language-service": "^17.3.4",
|
||||||
"@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": "^16.3.0",
|
"@ngrx/store-devtools": "^17.1.1",
|
||||||
"@ngtools/webpack": "^16.2.12",
|
"@ngtools/webpack": "^16.2.12",
|
||||||
"@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",
|
||||||
@@ -169,6 +168,7 @@
|
|||||||
"@typescript-eslint/rule-tester": "^7.2.0",
|
"@typescript-eslint/rule-tester": "^7.2.0",
|
||||||
"@typescript-eslint/utils": "^7.2.0",
|
"@typescript-eslint/utils": "^7.2.0",
|
||||||
"axe-core": "^4.7.2",
|
"axe-core": "^4.7.2",
|
||||||
|
"browser-sync": "^3.0.0",
|
||||||
"compression-webpack-plugin": "^9.2.0",
|
"compression-webpack-plugin": "^9.2.0",
|
||||||
"copy-webpack-plugin": "^6.4.1",
|
"copy-webpack-plugin": "^6.4.1",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
@@ -211,7 +211,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.9.3",
|
"typescript": "~5.3.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",
|
||||||
|
149
server.ts
149
server.ts
@@ -17,7 +17,6 @@
|
|||||||
|
|
||||||
import 'zone.js/node';
|
import 'zone.js/node';
|
||||||
import 'reflect-metadata';
|
import 'reflect-metadata';
|
||||||
import 'rxjs';
|
|
||||||
|
|
||||||
/* eslint-disable import/no-namespace */
|
/* eslint-disable import/no-namespace */
|
||||||
import * as morgan from 'morgan';
|
import * as morgan from 'morgan';
|
||||||
@@ -39,23 +38,26 @@ import { join } from 'path';
|
|||||||
|
|
||||||
import { enableProdMode } from '@angular/core';
|
import { enableProdMode } from '@angular/core';
|
||||||
|
|
||||||
import { ngExpressEngine } from '@nguniversal/express-engine';
|
|
||||||
import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens';
|
|
||||||
|
|
||||||
import { environment } from './src/environments/environment';
|
import { environment } from './src/environments/environment';
|
||||||
import { createProxyMiddleware } from 'http-proxy-middleware';
|
import { createProxyMiddleware } from 'http-proxy-middleware';
|
||||||
import { hasNoValue, hasValue } from './src/app/shared/empty.util';
|
import { 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 bootstrap 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';
|
||||||
import { extendEnvironmentWithAppConfig } from './src/config/config.util';
|
import { extendEnvironmentWithAppConfig } from './src/config/config.util';
|
||||||
import { logStartupMessage } from './startup-message';
|
import { logStartupMessage } from './startup-message';
|
||||||
import { TOKENITEM } from './src/app/core/auth/models/auth-token-info.model';
|
import { TOKENITEM } from './src/app/core/auth/models/auth-token-info.model';
|
||||||
|
import { CommonEngine } from '@angular/ssr';
|
||||||
|
import { APP_BASE_HREF } from '@angular/common';
|
||||||
|
import {
|
||||||
|
REQUEST,
|
||||||
|
RESPONSE,
|
||||||
|
} from './src/express.tokens';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Set path for the browser application's dist folder
|
* Set path for the browser application's dist folder
|
||||||
@@ -127,27 +129,6 @@ export function app() {
|
|||||||
*/
|
*/
|
||||||
server.use(json());
|
server.use(json());
|
||||||
|
|
||||||
// Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine)
|
|
||||||
server.engine('html', (_, options, callback) =>
|
|
||||||
ngExpressEngine({
|
|
||||||
bootstrap,
|
|
||||||
providers: [
|
|
||||||
{
|
|
||||||
provide: REQUEST,
|
|
||||||
useValue: (options as any).req,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: RESPONSE,
|
|
||||||
useValue: (options as any).req.res,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: APP_CONFIG,
|
|
||||||
useValue: environment,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})(_, (options as any), callback),
|
|
||||||
);
|
|
||||||
|
|
||||||
server.engine('ejs', ejs.renderFile);
|
server.engine('ejs', ejs.renderFile);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -236,10 +217,10 @@ export function app() {
|
|||||||
/*
|
/*
|
||||||
* The callback function to serve server side angular
|
* The callback function to serve server side angular
|
||||||
*/
|
*/
|
||||||
function ngApp(req, res) {
|
function ngApp(req, res, next) {
|
||||||
if (environment.universal.preboot) {
|
if (environment.ssr.enabled) {
|
||||||
// Render the page to user via SSR (server side rendering)
|
// Render the page to user via SSR (server side rendering)
|
||||||
serverSideRender(req, res);
|
serverSideRender(req, res, next);
|
||||||
} else {
|
} else {
|
||||||
// If preboot is disabled, just serve the client
|
// If preboot is disabled, just serve the client
|
||||||
console.log('Universal off, serving for direct client-side rendering (CSR)');
|
console.log('Universal off, serving for direct client-side rendering (CSR)');
|
||||||
@@ -252,45 +233,66 @@ function ngApp(req, res) {
|
|||||||
* returned to the user.
|
* returned to the user.
|
||||||
* @param req current request
|
* @param req current request
|
||||||
* @param res current response
|
* @param res current response
|
||||||
|
* @param next the next function
|
||||||
* @param sendToUser if true (default), send the rendered content to the user.
|
* @param sendToUser if true (default), send the rendered content to the user.
|
||||||
* If false, then only save this rendered content to the in-memory cache (to refresh cache).
|
* If false, then only save this rendered content to the in-memory cache (to refresh cache).
|
||||||
*/
|
*/
|
||||||
function serverSideRender(req, res, sendToUser: boolean = true) {
|
function serverSideRender(req, res, next, sendToUser: boolean = true) {
|
||||||
|
const { protocol, originalUrl, baseUrl, headers } = req;
|
||||||
|
const commonEngine = new CommonEngine({ enablePerformanceProfiler: environment.ssr.enablePerformanceProfiler });
|
||||||
// Render the page via SSR (server side rendering)
|
// Render the page via SSR (server side rendering)
|
||||||
res.render(indexHtml, {
|
commonEngine
|
||||||
req,
|
.render({
|
||||||
res,
|
bootstrap,
|
||||||
preboot: environment.universal.preboot,
|
documentFilePath: indexHtml,
|
||||||
async: environment.universal.async,
|
inlineCriticalCss: environment.ssr.inlineCriticalCss,
|
||||||
time: environment.universal.time,
|
url: `${protocol}://${headers.host}${originalUrl}`,
|
||||||
baseUrl: environment.ui.nameSpace,
|
publicPath: DIST_FOLDER,
|
||||||
originUrl: environment.ui.baseUrl,
|
providers: [
|
||||||
requestUrl: req.originalUrl,
|
{ provide: APP_BASE_HREF, useValue: baseUrl },
|
||||||
}, (err, data) => {
|
{
|
||||||
if (hasNoValue(err) && hasValue(data)) {
|
provide: REQUEST,
|
||||||
// save server side rendered page to cache (if any are enabled)
|
useValue: req,
|
||||||
saveToCache(req, data);
|
},
|
||||||
if (sendToUser) {
|
{
|
||||||
res.locals.ssr = true; // mark response as SSR (enables text compression)
|
provide: RESPONSE,
|
||||||
// send rendered page to user
|
useValue: res,
|
||||||
res.send(data);
|
},
|
||||||
|
{
|
||||||
|
provide: APP_CONFIG,
|
||||||
|
useValue: environment,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
.then((html) => {
|
||||||
|
if (hasValue(html)) {
|
||||||
|
// save server side rendered page to cache (if any are enabled)
|
||||||
|
saveToCache(req, html);
|
||||||
|
if (sendToUser) {
|
||||||
|
res.locals.ssr = true; // mark response as SSR (enables text compression)
|
||||||
|
// send rendered page to user
|
||||||
|
res.send(html);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (hasValue(err) && err.code === 'ERR_HTTP_HEADERS_SENT') {
|
})
|
||||||
// When this error occurs we can't fall back to CSR because the response has already been
|
.catch((err) => {
|
||||||
// sent. These errors occur for various reasons in universal, not all of which are in our
|
if (hasValue(err) && err.code === 'ERR_HTTP_HEADERS_SENT') {
|
||||||
// control to solve.
|
// When this error occurs we can't fall back to CSR because the response has already been
|
||||||
console.warn('Warning [ERR_HTTP_HEADERS_SENT]: Tried to set headers after they were sent to the client');
|
// sent. These errors occur for various reasons in universal, not all of which are in our
|
||||||
} else {
|
// control to solve.
|
||||||
console.warn('Error in server-side rendering (SSR)');
|
console.warn('Warning [ERR_HTTP_HEADERS_SENT]: Tried to set headers after they were sent to the client');
|
||||||
if (hasValue(err)) {
|
} else {
|
||||||
console.warn('Error details : ', err);
|
console.warn('Error in server-side rendering (SSR)');
|
||||||
|
if (hasValue(err)) {
|
||||||
|
console.warn('Error details : ', err);
|
||||||
|
}
|
||||||
|
if (sendToUser) {
|
||||||
|
console.warn('Falling back to serving direct client-side rendering (CSR).');
|
||||||
|
clientSideRender(req, res);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (sendToUser) {
|
next(err);
|
||||||
console.warn('Falling back to serving direct client-side rendering (CSR).');
|
});
|
||||||
clientSideRender(req, res);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -348,7 +350,7 @@ function initCache() {
|
|||||||
function botCacheEnabled(): boolean {
|
function botCacheEnabled(): boolean {
|
||||||
// Caching is only enabled if SSR is enabled AND
|
// Caching is only enabled if SSR is enabled AND
|
||||||
// "max" pages to cache is greater than zero
|
// "max" pages to cache is greater than zero
|
||||||
return environment.universal.preboot && environment.cache.serverSide.botCache.max && (environment.cache.serverSide.botCache.max > 0);
|
return environment.ssr.enabled && environment.cache.serverSide.botCache.max && (environment.cache.serverSide.botCache.max > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -357,7 +359,7 @@ function botCacheEnabled(): boolean {
|
|||||||
function anonymousCacheEnabled(): boolean {
|
function anonymousCacheEnabled(): boolean {
|
||||||
// Caching is only enabled if SSR is enabled AND
|
// Caching is only enabled if SSR is enabled AND
|
||||||
// "max" pages to cache is greater than zero
|
// "max" pages to cache is greater than zero
|
||||||
return environment.universal.preboot && environment.cache.serverSide.anonymousCache.max && (environment.cache.serverSide.anonymousCache.max > 0);
|
return environment.ssr.enabled && environment.cache.serverSide.anonymousCache.max && (environment.cache.serverSide.anonymousCache.max > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -370,9 +372,9 @@ function cacheCheck(req, res, next) {
|
|||||||
|
|
||||||
// If the bot cache is enabled and this request looks like a bot, check the bot cache for a cached page.
|
// If the bot cache is enabled and this request looks like a bot, check the bot cache for a cached page.
|
||||||
if (botCacheEnabled() && isbot(req.get('user-agent'))) {
|
if (botCacheEnabled() && isbot(req.get('user-agent'))) {
|
||||||
cachedCopy = checkCacheForRequest('bot', botCache, req, res);
|
cachedCopy = checkCacheForRequest('bot', botCache, req, res, next);
|
||||||
} else if (anonymousCacheEnabled() && !isUserAuthenticated(req)) {
|
} else if (anonymousCacheEnabled() && !isUserAuthenticated(req)) {
|
||||||
cachedCopy = checkCacheForRequest('anonymous', anonymousCache, req, res);
|
cachedCopy = checkCacheForRequest('anonymous', anonymousCache, req, res, next);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If cached copy exists, return it to the user.
|
// If cached copy exists, return it to the user.
|
||||||
@@ -408,9 +410,10 @@ function cacheCheck(req, res, next) {
|
|||||||
* @param cache LRU cache to check
|
* @param cache LRU cache to check
|
||||||
* @param req current request to look for in the cache
|
* @param req current request to look for in the cache
|
||||||
* @param res current response
|
* @param res current response
|
||||||
|
* @param next the next function
|
||||||
* @returns cached copy (if found) or undefined (if not found)
|
* @returns cached copy (if found) or undefined (if not found)
|
||||||
*/
|
*/
|
||||||
function checkCacheForRequest(cacheName: string, cache: LRU<string, any>, req, res): any {
|
function checkCacheForRequest(cacheName: string, cache: LRU<string, any>, req, res, next): any {
|
||||||
// Get the cache key for this request
|
// Get the cache key for this request
|
||||||
const key = getCacheKey(req);
|
const key = getCacheKey(req);
|
||||||
|
|
||||||
@@ -426,7 +429,7 @@ function checkCacheForRequest(cacheName: string, cache: LRU<string, any>, req, r
|
|||||||
// Update cached copy by rerendering server-side
|
// Update cached copy by rerendering server-side
|
||||||
// NOTE: In this scenario the currently cached copy will be returned to the current user.
|
// NOTE: In this scenario the currently cached copy will be returned to the current user.
|
||||||
// This re-render is peformed behind the scenes to update cached copy for next user.
|
// This re-render is peformed behind the scenes to update cached copy for next user.
|
||||||
serverSideRender(req, res, false);
|
serverSideRender(req, res, next, false);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (environment.cache.serverSide.debug) { console.log(`CACHE MISS FOR ${key} in ${cacheName} cache.`); }
|
if (environment.cache.serverSide.debug) { console.log(`CACHE MISS FOR ${key} in ${cacheName} cache.`); }
|
||||||
@@ -530,7 +533,7 @@ 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();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -35,7 +35,7 @@
|
|||||||
[checked]="isSelected(bitstreamFormat) | async"
|
[checked]="isSelected(bitstreamFormat) | async"
|
||||||
(change)="selectBitStreamFormat(bitstreamFormat, $event)"
|
(change)="selectBitStreamFormat(bitstreamFormat, $event)"
|
||||||
>
|
>
|
||||||
<span class="sr-only">{{'admin.registries.bitstream-formats.select' | translate}}}</span>
|
<span class="sr-only">{{'admin.registries.bitstream-formats.select' | translate}}}</span>
|
||||||
</label>
|
</label>
|
||||||
</td>
|
</td>
|
||||||
<td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{bitstreamFormat.id}}</a></td>
|
<td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{bitstreamFormat.id}}</a></td>
|
||||||
|
@@ -1 +1 @@
|
|||||||
<ds-configuration-search-page configuration="administrativeView" [context]="context"></ds-configuration-search-page>
|
<ds-themed-configuration-search-page configuration="administrativeView" [context]="context"></ds-themed-configuration-search-page>
|
||||||
|
@@ -1 +1 @@
|
|||||||
<ds-configuration-search-page configuration="supervision" [context]="context"></ds-configuration-search-page>
|
<ds-themed-configuration-search-page configuration="supervision" [context]="context"></ds-themed-configuration-search-page>
|
||||||
|
@@ -34,7 +34,6 @@ export function getBitstreamRequestACopyRoute(item, bitstream): { routerLink: st
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
export const COAR_NOTIFY_SUPPORT = 'coar-notify-support';
|
|
||||||
|
|
||||||
export const HOME_PAGE_PATH = 'home';
|
export const HOME_PAGE_PATH = 'home';
|
||||||
|
|
||||||
|
@@ -7,7 +7,7 @@
|
|||||||
<span aria-hidden="true" class="btn btn-default invisible" cdkTreeNodeToggle>
|
<span aria-hidden="true" class="btn btn-default invisible" cdkTreeNodeToggle>
|
||||||
<span class="fa fa-chevron-right"></span>
|
<span class="fa fa-chevron-right"></span>
|
||||||
</span>
|
</span>
|
||||||
<div class="align-middle pt-2">
|
<div class="align-middle my-auto">
|
||||||
<button *ngIf="(dataSource.loading$ | async) !== true" (click)="getNextPage(node)"
|
<button *ngIf="(dataSource.loading$ | async) !== true" (click)="getNextPage(node)"
|
||||||
class="btn btn-outline-primary btn-sm" role="button">
|
class="btn btn-outline-primary btn-sm" role="button">
|
||||||
<i class="fas fa-angle-down"></i> {{ 'communityList.showMore' | translate }}
|
<i class="fas fa-angle-down"></i> {{ 'communityList.showMore' | translate }}
|
||||||
@@ -37,10 +37,10 @@
|
|||||||
<span class="fa fa-chevron-right"></span>
|
<span class="fa fa-chevron-right"></span>
|
||||||
</span>
|
</span>
|
||||||
<div class="d-flex flex-row">
|
<div class="d-flex flex-row">
|
||||||
<span class="align-middle pt-2 lead">
|
<span class="d-flex align-middle my-auto">
|
||||||
<a [routerLink]="node.route" class="lead">{{ dsoNameService.getName(node.payload) }}</a>
|
<a [routerLink]="node.route" class="lead">{{ dsoNameService.getName(node.payload) }}</a>
|
||||||
<span class="pr-2"> </span>
|
<span class="pr-2"> </span>
|
||||||
<span *ngIf="node.payload.archivedItemsCount >= 0" class="badge badge-pill badge-secondary align-top archived-items-lead">{{node.payload.archivedItemsCount}}</span>
|
<span *ngIf="node.payload.archivedItemsCount >= 0" class="badge badge-pill badge-secondary align-top archived-items-lead my-auto">{{node.payload.archivedItemsCount}}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
<span aria-hidden="true" class="btn btn-default invisible" cdkTreeNodeToggle>
|
<span aria-hidden="true" class="btn btn-default invisible" cdkTreeNodeToggle>
|
||||||
<span class="fa fa-chevron-right"></span>
|
<span class="fa fa-chevron-right"></span>
|
||||||
</span>
|
</span>
|
||||||
<h6 class="align-middle pt-2">
|
<h6 class="align-middle my-auto">
|
||||||
<a [routerLink]="node.route" class="lead">{{ dsoNameService.getName(node.payload) }}</a>
|
<a [routerLink]="node.route" class="lead">{{ dsoNameService.getName(node.payload) }}</a>
|
||||||
</h6>
|
</h6>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -12,7 +12,6 @@ import {
|
|||||||
Store,
|
Store,
|
||||||
StoreModule,
|
StoreModule,
|
||||||
} from '@ngrx/store';
|
} from '@ngrx/store';
|
||||||
import { REQUEST } from '@nguniversal/express-engine/tokens';
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { cold } from 'jasmine-marbles';
|
import { cold } from 'jasmine-marbles';
|
||||||
import {
|
import {
|
||||||
@@ -20,6 +19,7 @@ import {
|
|||||||
of as observableOf,
|
of as observableOf,
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
|
|
||||||
|
import { REQUEST } from '../../../express.tokens';
|
||||||
import { AppState } from '../../app.reducer';
|
import { AppState } from '../../app.reducer';
|
||||||
import { getMockTranslateService } from '../../shared/mocks/translate.service.mock';
|
import { getMockTranslateService } from '../../shared/mocks/translate.service.mock';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
@@ -9,10 +9,6 @@ import {
|
|||||||
select,
|
select,
|
||||||
Store,
|
Store,
|
||||||
} from '@ngrx/store';
|
} from '@ngrx/store';
|
||||||
import {
|
|
||||||
REQUEST,
|
|
||||||
RESPONSE,
|
|
||||||
} from '@nguniversal/express-engine/tokens';
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { CookieAttributes } from 'js-cookie';
|
import { CookieAttributes } from 'js-cookie';
|
||||||
import {
|
import {
|
||||||
@@ -28,6 +24,10 @@ import {
|
|||||||
} from 'rxjs/operators';
|
} from 'rxjs/operators';
|
||||||
|
|
||||||
import { environment } from '../../../environments/environment';
|
import { environment } from '../../../environments/environment';
|
||||||
|
import {
|
||||||
|
REQUEST,
|
||||||
|
RESPONSE,
|
||||||
|
} from '../../../express.tokens';
|
||||||
import { AppState } from '../../app.reducer';
|
import { AppState } from '../../app.reducer';
|
||||||
import {
|
import {
|
||||||
hasNoValue,
|
hasNoValue,
|
||||||
|
@@ -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;
|
||||||
|
|
||||||
|
7
src/app/core/cache/builders/link.service.ts
vendored
7
src/app/core/cache/builders/link.service.ts
vendored
@@ -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];
|
||||||
|
@@ -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)],
|
||||||
};
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
@@ -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,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -17,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,
|
||||||
@@ -34,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,
|
||||||
@@ -62,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';
|
||||||
@@ -108,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;
|
||||||
});
|
});
|
||||||
|
@@ -34,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,
|
||||||
@@ -169,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 {
|
||||||
@@ -455,17 +449,7 @@ export class RequestService {
|
|||||||
private dispatchRequest(request: RestRequest) {
|
private dispatchRequest(request: RestRequest) {
|
||||||
asapScheduler.schedule(() => {
|
asapScheduler.schedule(() => {
|
||||||
this.store.dispatch(new RequestConfigureAction(request));
|
this.store.dispatch(new RequestConfigureAction(request));
|
||||||
// If it's a GET request, or we have an XSRF token, dispatch it immediately
|
this.store.dispatch(new RequestExecuteAction(request.uuid));
|
||||||
if (request.method === RestRequestMethod.GET || this.xsrfService.tokenInitialized$.getValue() === true) {
|
|
||||||
this.store.dispatch(new RequestExecuteAction(request.uuid));
|
|
||||||
} else {
|
|
||||||
// Otherwise wait for the XSRF token first
|
|
||||||
this.xsrfService.tokenInitialized$.pipe(
|
|
||||||
find((hasInitialized: boolean) => hasInitialized === true),
|
|
||||||
).subscribe(() => {
|
|
||||||
this.store.dispatch(new RequestExecuteAction(request.uuid));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -4,8 +4,8 @@ import {
|
|||||||
HttpTestingController,
|
HttpTestingController,
|
||||||
} from '@angular/common/http/testing';
|
} from '@angular/common/http/testing';
|
||||||
import { TestBed } from '@angular/core/testing';
|
import { TestBed } from '@angular/core/testing';
|
||||||
import { REQUEST } from '@nguniversal/express-engine/tokens';
|
|
||||||
|
|
||||||
|
import { REQUEST } from '../../../express.tokens';
|
||||||
import { DspaceRestService } from '../dspace-rest/dspace-rest.service';
|
import { DspaceRestService } from '../dspace-rest/dspace-rest.service';
|
||||||
import { ForwardClientIpInterceptor } from './forward-client-ip.interceptor';
|
import { ForwardClientIpInterceptor } from './forward-client-ip.interceptor';
|
||||||
|
|
||||||
|
@@ -8,9 +8,10 @@ import {
|
|||||||
Inject,
|
Inject,
|
||||||
Injectable,
|
Injectable,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { REQUEST } from '@nguniversal/express-engine/tokens';
|
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
import { REQUEST } from '../../../express.tokens';
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
/**
|
/**
|
||||||
* Http Interceptor intercepting Http Requests, adding the client's IP to their X-Forwarded-For header
|
* Http Interceptor intercepting Http Requests, adding the client's IP to their X-Forwarded-For header
|
||||||
|
50
src/app/core/lazy-data-service.ts
Normal file
50
src/app/core/lazy-data-service.ts
Normal 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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
@@ -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;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
@@ -3,7 +3,6 @@ import {
|
|||||||
Inject,
|
Inject,
|
||||||
Injectable,
|
Injectable,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { REQUEST } from '@nguniversal/express-engine/tokens';
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import {
|
import {
|
||||||
combineLatest,
|
combineLatest,
|
||||||
@@ -16,6 +15,7 @@ import {
|
|||||||
take,
|
take,
|
||||||
} from 'rxjs/operators';
|
} from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { REQUEST } from '../../../express.tokens';
|
||||||
import {
|
import {
|
||||||
hasValue,
|
hasValue,
|
||||||
isEmpty,
|
isEmpty,
|
||||||
|
@@ -2,8 +2,8 @@ import {
|
|||||||
TestBed,
|
TestBed,
|
||||||
waitForAsync,
|
waitForAsync,
|
||||||
} from '@angular/core/testing';
|
} from '@angular/core/testing';
|
||||||
import { REQUEST } from '@nguniversal/express-engine/tokens';
|
|
||||||
|
|
||||||
|
import { REQUEST } from '../../../express.tokens';
|
||||||
import {
|
import {
|
||||||
CookieService,
|
CookieService,
|
||||||
ICookieService,
|
ICookieService,
|
||||||
|
@@ -2,13 +2,14 @@ import {
|
|||||||
Inject,
|
Inject,
|
||||||
Injectable,
|
Injectable,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { REQUEST } from '@nguniversal/express-engine/tokens';
|
|
||||||
import { CookieAttributes } from 'js-cookie';
|
import { CookieAttributes } from 'js-cookie';
|
||||||
import {
|
import {
|
||||||
Observable,
|
Observable,
|
||||||
Subject,
|
Subject,
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
|
|
||||||
|
import { REQUEST } from '../../../express.tokens';
|
||||||
|
|
||||||
export interface ICookieService {
|
export interface ICookieService {
|
||||||
readonly cookies$: Observable<{ readonly [key: string]: any }>;
|
readonly cookies$: Observable<{ readonly [key: string]: any }>;
|
||||||
|
|
||||||
|
@@ -2,15 +2,15 @@ import {
|
|||||||
Inject,
|
Inject,
|
||||||
Injectable,
|
Injectable,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import {
|
|
||||||
REQUEST,
|
|
||||||
RESPONSE,
|
|
||||||
} from '@nguniversal/express-engine/tokens';
|
|
||||||
import {
|
import {
|
||||||
Request,
|
Request,
|
||||||
Response,
|
Response,
|
||||||
} from 'express';
|
} from 'express';
|
||||||
|
|
||||||
|
import {
|
||||||
|
REQUEST,
|
||||||
|
RESPONSE,
|
||||||
|
} from '../../../express.tokens';
|
||||||
import { HardRedirectService } from './hard-redirect.service';
|
import { HardRedirectService } from './hard-redirect.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -3,9 +3,10 @@ import {
|
|||||||
Injectable,
|
Injectable,
|
||||||
Optional,
|
Optional,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { RESPONSE } from '@nguniversal/express-engine/tokens';
|
|
||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
|
|
||||||
|
import { RESPONSE } from '../../../express.tokens';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service responsible to provide method to manage the response object
|
* Service responsible to provide method to manage the response object
|
||||||
*/
|
*/
|
||||||
|
@@ -2,12 +2,12 @@ import {
|
|||||||
Inject,
|
Inject,
|
||||||
Injectable,
|
Injectable,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { REQUEST } from '@nguniversal/express-engine/tokens';
|
|
||||||
import {
|
import {
|
||||||
Observable,
|
Observable,
|
||||||
of as observableOf,
|
of as observableOf,
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
|
|
||||||
|
import { REQUEST } from '../../../express.tokens';
|
||||||
import { ReferrerService } from './referrer.service';
|
import { ReferrerService } from './referrer.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -48,7 +48,7 @@ export const DEBOUNCE_TIME_OPERATOR = new InjectionToken<<T>(dueTime: number) =>
|
|||||||
|
|
||||||
export const getRemoteDataPayload = <T>() =>
|
export const getRemoteDataPayload = <T>() =>
|
||||||
(source: Observable<RemoteData<T>>): Observable<T> =>
|
(source: Observable<RemoteData<T>>): Observable<T> =>
|
||||||
source.pipe(map((remoteData: RemoteData<T>) => remoteData.payload));
|
source.pipe(map((remoteData: RemoteData<T>) => remoteData?.payload));
|
||||||
|
|
||||||
export const getPaginatedListPayload = <T>() =>
|
export const getPaginatedListPayload = <T>() =>
|
||||||
(source: Observable<PaginatedList<T>>): Observable<T[]> =>
|
(source: Observable<PaginatedList<T>>): Observable<T[]> =>
|
||||||
|
@@ -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;
|
||||||
|
@@ -7,7 +7,6 @@ import {
|
|||||||
ChangeDetectorRef,
|
ChangeDetectorRef,
|
||||||
Component,
|
Component,
|
||||||
Inject,
|
Inject,
|
||||||
InjectionToken,
|
|
||||||
Injector,
|
Injector,
|
||||||
Input,
|
Input,
|
||||||
OnDestroy,
|
OnDestroy,
|
||||||
@@ -42,7 +41,7 @@ import {
|
|||||||
import { ArrayMoveChangeAnalyzer } from '../../core/data/array-move-change-analyzer.service';
|
import { ArrayMoveChangeAnalyzer } from '../../core/data/array-move-change-analyzer.service';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
import { UpdateDataService } from '../../core/data/update-data.service';
|
import { UpdateDataService } from '../../core/data/update-data.service';
|
||||||
import { lazyService } from '../../core/lazy-service';
|
import { lazyDataService } from '../../core/lazy-data-service';
|
||||||
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||||
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
|
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
|
||||||
import { ResourceType } from '../../core/shared/resource-type';
|
import { ResourceType } from '../../core/shared/resource-type';
|
||||||
@@ -152,7 +151,7 @@ export class DsoEditMetadataComponent implements OnInit, OnDestroy {
|
|||||||
protected parentInjector: Injector,
|
protected parentInjector: Injector,
|
||||||
protected arrayMoveChangeAnalyser: ArrayMoveChangeAnalyzer<number>,
|
protected arrayMoveChangeAnalyser: ArrayMoveChangeAnalyzer<number>,
|
||||||
protected cdr: ChangeDetectorRef,
|
protected cdr: ChangeDetectorRef,
|
||||||
@Inject(APP_DATA_SERVICES_MAP) private dataServiceMap: InjectionToken<LazyDataServicesMap>) {
|
@Inject(APP_DATA_SERVICES_MAP) private dataServiceMap: LazyDataServicesMap) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -186,7 +185,7 @@ export class DsoEditMetadataComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
retrieveDataService(): Observable<UpdateDataService<DSpaceObject>> {
|
retrieveDataService(): Observable<UpdateDataService<DSpaceObject>> {
|
||||||
if (hasNoValue(this.updateDataService)) {
|
if (hasNoValue(this.updateDataService)) {
|
||||||
const lazyProvider$: Observable<UpdateDataService<DSpaceObject>> = lazyService(this.dataServiceMap[this.dsoType], this.parentInjector);
|
const lazyProvider$: Observable<UpdateDataService<DSpaceObject>> = lazyDataService(this.dataServiceMap, this.dsoType, this.parentInjector);
|
||||||
return lazyProvider$;
|
return lazyProvider$;
|
||||||
} else {
|
} else {
|
||||||
return EMPTY;
|
return EMPTY;
|
||||||
|
@@ -3,10 +3,9 @@ form {
|
|||||||
&:before {
|
&:before {
|
||||||
pointer-events: none; // prevent the icon from ‘catching‘ the click
|
pointer-events: none; // prevent the icon from ‘catching‘ the click
|
||||||
position: absolute;
|
position: absolute;
|
||||||
font-weight: 900;
|
font: var(--fa-font-solid);
|
||||||
font-family: "Font Awesome 5 Free";
|
|
||||||
content: "\f0d7";
|
content: "\f0d7";
|
||||||
top: 7px;
|
top: 10px;
|
||||||
right: 0;
|
right: 0;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
|
@@ -13,7 +13,6 @@ import {
|
|||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { REQUEST } from '@nguniversal/express-engine/tokens';
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import {
|
import {
|
||||||
Observable,
|
Observable,
|
||||||
@@ -21,6 +20,7 @@ import {
|
|||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
|
|
||||||
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
|
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
|
||||||
|
import { REQUEST } from '../../../../../../express.tokens';
|
||||||
import { AuthService } from '../../../../../core/auth/auth.service';
|
import { AuthService } from '../../../../../core/auth/auth.service';
|
||||||
import { RemoteDataBuildService } from '../../../../../core/cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../../../../../core/cache/builders/remote-data-build.service';
|
||||||
import { ObjectCacheService } from '../../../../../core/cache/object-cache.service';
|
import { ObjectCacheService } from '../../../../../core/cache/object-cache.service';
|
||||||
|
@@ -2,10 +2,9 @@ form {
|
|||||||
&:before {
|
&:before {
|
||||||
pointer-events: none; // prevent the icon from ‘catching‘ the click
|
pointer-events: none; // prevent the icon from ‘catching‘ the click
|
||||||
position: absolute;
|
position: absolute;
|
||||||
font-weight: 900;
|
font: var(--fa-font-solid);
|
||||||
font-family: "Font Awesome 5 Free";
|
|
||||||
content: "\f0d7";
|
content: "\f0d7";
|
||||||
top: 7px;
|
top: 10px;
|
||||||
right: 0;
|
right: 0;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
|
@@ -82,10 +82,10 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="notify-enabled text-white" [hidden]="!coarLdnEnabled">
|
<div *ngIf="coarLdnEnabled$ | async" class="notify-enabled text-white">
|
||||||
<a class="coar-notify-support-route" routerLink="info/coar-notify-support">
|
<a class="coar-notify-support-route" routerLink="info/coar-notify-support">
|
||||||
<img class="n-coar" src="assets/images/n-coar.png" [attr.alt]="'menu.header.image.logo' | translate" />
|
<img class="n-coar" src="assets/images/n-coar.png" [attr.alt]="'menu.header.image.logo' | translate" />
|
||||||
COAR Notify
|
{{ 'footer.link.coar-notify-support' | translate }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,9 +1,3 @@
|
|||||||
// ... test imports
|
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import {
|
|
||||||
CUSTOM_ELEMENTS_SCHEMA,
|
|
||||||
DebugElement,
|
|
||||||
} from '@angular/core';
|
|
||||||
import {
|
import {
|
||||||
ComponentFixture,
|
ComponentFixture,
|
||||||
fakeAsync,
|
fakeAsync,
|
||||||
@@ -13,28 +7,19 @@ import {
|
|||||||
} from '@angular/core/testing';
|
} from '@angular/core/testing';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { StoreModule } from '@ngrx/store';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import {
|
|
||||||
TranslateLoader,
|
|
||||||
TranslateModule,
|
|
||||||
} from '@ngx-translate/core';
|
|
||||||
import { of } from 'rxjs';
|
import { of } from 'rxjs';
|
||||||
import { environment } from 'src/environments/environment';
|
|
||||||
|
|
||||||
import { storeModuleConfig } from '../app.reducer';
|
import { APP_CONFIG } from '../../config/app-config.interface';
|
||||||
|
import { environment } from '../../environments/environment.test';
|
||||||
import { NotifyInfoService } from '../core/coar-notify/notify-info/notify-info.service';
|
import { NotifyInfoService } from '../core/coar-notify/notify-info/notify-info.service';
|
||||||
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
|
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
|
||||||
import { TranslateLoaderMock } from '../shared/mocks/translate-loader.mock';
|
|
||||||
import { ActivatedRouteStub } from '../shared/testing/active-router.stub';
|
import { ActivatedRouteStub } from '../shared/testing/active-router.stub';
|
||||||
import { AuthorizationDataServiceStub } from '../shared/testing/authorization-service.stub';
|
import { AuthorizationDataServiceStub } from '../shared/testing/authorization-service.stub';
|
||||||
// Load the implementations that should be tested
|
|
||||||
import { FooterComponent } from './footer.component';
|
import { FooterComponent } from './footer.component';
|
||||||
|
|
||||||
let comp: FooterComponent;
|
let comp: FooterComponent;
|
||||||
let compAny: any;
|
|
||||||
let fixture: ComponentFixture<FooterComponent>;
|
let fixture: ComponentFixture<FooterComponent>;
|
||||||
let de: DebugElement;
|
|
||||||
let el: HTMLElement;
|
|
||||||
|
|
||||||
let notifyInfoService = {
|
let notifyInfoService = {
|
||||||
isCoarConfigEnabled: () => of(true),
|
isCoarConfigEnabled: () => of(true),
|
||||||
@@ -43,19 +28,16 @@ let notifyInfoService = {
|
|||||||
describe('Footer component', () => {
|
describe('Footer component', () => {
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
return TestBed.configureTestingModule({
|
return TestBed.configureTestingModule({
|
||||||
imports: [CommonModule, StoreModule.forRoot({}, storeModuleConfig), TranslateModule.forRoot({
|
imports: [
|
||||||
loader: {
|
TranslateModule.forRoot(),
|
||||||
provide: TranslateLoader,
|
],
|
||||||
useClass: TranslateLoaderMock,
|
|
||||||
},
|
|
||||||
}), FooterComponent],
|
|
||||||
providers: [
|
providers: [
|
||||||
FooterComponent,
|
FooterComponent,
|
||||||
{ provide: AuthorizationDataService, useClass: AuthorizationDataServiceStub },
|
{ provide: AuthorizationDataService, useClass: AuthorizationDataServiceStub },
|
||||||
{ provide: NotifyInfoService, useValue: notifyInfoService },
|
{ provide: NotifyInfoService, useValue: notifyInfoService },
|
||||||
{ provide: ActivatedRoute, useValue: new ActivatedRouteStub() },
|
{ provide: ActivatedRoute, useValue: new ActivatedRouteStub() },
|
||||||
|
{ provide: APP_CONFIG, useValue: environment },
|
||||||
],
|
],
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -63,10 +45,6 @@ describe('Footer component', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(FooterComponent);
|
fixture = TestBed.createComponent(FooterComponent);
|
||||||
comp = fixture.componentInstance;
|
comp = fixture.componentInstance;
|
||||||
compAny = comp as any;
|
|
||||||
// query for the title <p> by CSS element selector
|
|
||||||
de = fixture.debugElement.query(By.css('p'));
|
|
||||||
el = de.nativeElement;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create footer', inject([FooterComponent], (app: FooterComponent) => {
|
it('should create footer', inject([FooterComponent], (app: FooterComponent) => {
|
||||||
@@ -76,23 +54,25 @@ describe('Footer component', () => {
|
|||||||
|
|
||||||
|
|
||||||
it('should set showPrivacyPolicy to the value of environment.info.enablePrivacyStatement', () => {
|
it('should set showPrivacyPolicy to the value of environment.info.enablePrivacyStatement', () => {
|
||||||
|
comp.ngOnInit();
|
||||||
expect(comp.showPrivacyPolicy).toBe(environment.info.enablePrivacyStatement);
|
expect(comp.showPrivacyPolicy).toBe(environment.info.enablePrivacyStatement);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set showEndUserAgreement to the value of environment.info.enableEndUserAgreement', () => {
|
it('should set showEndUserAgreement to the value of environment.info.enableEndUserAgreement', () => {
|
||||||
|
comp.ngOnInit();
|
||||||
expect(comp.showEndUserAgreement).toBe(environment.info.enableEndUserAgreement);
|
expect(comp.showEndUserAgreement).toBe(environment.info.enableEndUserAgreement);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('showCookieSettings', () => {
|
describe('showCookieSettings', () => {
|
||||||
it('should call cookies.showSettings() if cookies is defined', () => {
|
it('should call cookies.showSettings() if cookies is defined', () => {
|
||||||
const cookies = jasmine.createSpyObj('cookies', ['showSettings']);
|
const cookies = jasmine.createSpyObj('cookies', ['showSettings']);
|
||||||
compAny.cookies = cookies;
|
comp.cookies = cookies;
|
||||||
comp.showCookieSettings();
|
comp.showCookieSettings();
|
||||||
expect(cookies.showSettings).toHaveBeenCalled();
|
expect(cookies.showSettings).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not call cookies.showSettings() if cookies is undefined', () => {
|
it('should not call cookies.showSettings() if cookies is undefined', () => {
|
||||||
compAny.cookies = undefined;
|
comp.cookies = undefined;
|
||||||
expect(() => comp.showCookieSettings()).not.toThrow();
|
expect(() => comp.showCookieSettings()).not.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -107,9 +87,7 @@ describe('Footer component', () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set coarLdnEnabled based on notifyInfoService', () => {
|
it('should render COAR notify support link', () => {
|
||||||
expect(comp.coarLdnEnabled).toBeTruthy();
|
|
||||||
// Check if COAR Notify section is rendered
|
|
||||||
const notifySection = fixture.debugElement.query(By.css('.notify-enabled'));
|
const notifySection = fixture.debugElement.query(By.css('.notify-enabled'));
|
||||||
expect(notifySection).toBeTruthy();
|
expect(notifySection).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
@@ -5,13 +5,21 @@ import {
|
|||||||
} from '@angular/common';
|
} from '@angular/common';
|
||||||
import {
|
import {
|
||||||
Component,
|
Component,
|
||||||
|
Inject,
|
||||||
|
OnInit,
|
||||||
Optional,
|
Optional,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { RouterLink } from '@angular/router';
|
import { RouterLink } from '@angular/router';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { Observable } from 'rxjs';
|
import {
|
||||||
|
Observable,
|
||||||
|
of as observableOf,
|
||||||
|
} from 'rxjs';
|
||||||
|
|
||||||
import { environment } from '../../environments/environment';
|
import {
|
||||||
|
APP_CONFIG,
|
||||||
|
AppConfig,
|
||||||
|
} from '../../config/app-config.interface';
|
||||||
import { NotifyInfoService } from '../core/coar-notify/notify-info/notify-info.service';
|
import { NotifyInfoService } from '../core/coar-notify/notify-info/notify-info.service';
|
||||||
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
|
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
|
||||||
import { FeatureID } from '../core/data/feature-authorization/feature-id';
|
import { FeatureID } from '../core/data/feature-authorization/feature-id';
|
||||||
@@ -25,27 +33,31 @@ import { hasValue } from '../shared/empty.util';
|
|||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [NgIf, RouterLink, AsyncPipe, DatePipe, TranslateModule],
|
imports: [NgIf, RouterLink, AsyncPipe, DatePipe, TranslateModule],
|
||||||
})
|
})
|
||||||
export class FooterComponent {
|
export class FooterComponent implements OnInit {
|
||||||
dateObj: number = Date.now();
|
dateObj: number = Date.now();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A boolean representing if to show or not the top footer container
|
* A boolean representing if to show or not the top footer container
|
||||||
*/
|
*/
|
||||||
showTopFooter = false;
|
showTopFooter = false;
|
||||||
showPrivacyPolicy = environment.info.enablePrivacyStatement;
|
showPrivacyPolicy: boolean;
|
||||||
showEndUserAgreement = environment.info.enableEndUserAgreement;
|
showEndUserAgreement: boolean;
|
||||||
showSendFeedback$: Observable<boolean>;
|
showSendFeedback$: Observable<boolean>;
|
||||||
coarLdnEnabled: boolean;
|
coarLdnEnabled$: Observable<boolean>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Optional() private cookies: KlaroService,
|
@Optional() public cookies: KlaroService,
|
||||||
private authorizationService: AuthorizationDataService,
|
protected authorizationService: AuthorizationDataService,
|
||||||
private notifyInfoService: NotifyInfoService,
|
protected notifyInfoService: NotifyInfoService,
|
||||||
|
@Inject(APP_CONFIG) protected appConfig: AppConfig,
|
||||||
) {
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.showPrivacyPolicy = this.appConfig.info.enablePrivacyStatement;
|
||||||
|
this.showEndUserAgreement = this.appConfig.info.enableEndUserAgreement;
|
||||||
|
this.coarLdnEnabled$ = this.appConfig.info.enableCOARNotifySupport ? this.notifyInfoService.isCoarConfigEnabled() : observableOf(false);
|
||||||
this.showSendFeedback$ = this.authorizationService.isAuthorized(FeatureID.CanSendFeedback);
|
this.showSendFeedback$ = this.authorizationService.isAuthorized(FeatureID.CanSendFeedback);
|
||||||
this.notifyInfoService.isCoarConfigEnabled().subscribe(coarLdnEnabled => {
|
|
||||||
this.coarLdnEnabled = coarLdnEnabled;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
showCookieSettings() {
|
showCookieSettings() {
|
||||||
|
92
src/app/home-page/home-coar/home-coar.component.ts
Normal file
92
src/app/home-page/home-coar/home-coar.component.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import { isPlatformServer } from '@angular/common';
|
||||||
|
import {
|
||||||
|
Component,
|
||||||
|
Inject,
|
||||||
|
OnDestroy,
|
||||||
|
OnInit,
|
||||||
|
PLATFORM_ID,
|
||||||
|
} from '@angular/core';
|
||||||
|
import {
|
||||||
|
of as observableOf,
|
||||||
|
Subscription,
|
||||||
|
} from 'rxjs';
|
||||||
|
import { switchMap } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { NotifyInfoService } from '../../core/coar-notify/notify-info/notify-info.service';
|
||||||
|
import {
|
||||||
|
LinkDefinition,
|
||||||
|
LinkHeadService,
|
||||||
|
} from '../../core/services/link-head.service';
|
||||||
|
import { ServerResponseService } from '../../core/services/server-response.service';
|
||||||
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-home-coar',
|
||||||
|
template: '',
|
||||||
|
standalone: true,
|
||||||
|
})
|
||||||
|
export class HomeCoarComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array of LinkDefinition objects representing inbox links for the home page.
|
||||||
|
*/
|
||||||
|
inboxLinks: LinkDefinition[] = [];
|
||||||
|
|
||||||
|
subs: Subscription[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected linkHeadService: LinkHeadService,
|
||||||
|
protected notifyInfoService: NotifyInfoService,
|
||||||
|
protected responseService: ServerResponseService,
|
||||||
|
@Inject(PLATFORM_ID) protected platformId: string,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
// Get COAR REST API URLs from REST configuration
|
||||||
|
// only if COAR configuration is enabled
|
||||||
|
this.subs.push(this.notifyInfoService.isCoarConfigEnabled().pipe(
|
||||||
|
switchMap((coarLdnEnabled: boolean) => coarLdnEnabled ? this.notifyInfoService.getCoarLdnLocalInboxUrls() : observableOf([])),
|
||||||
|
).subscribe((coarRestApiUrls: string[]) => {
|
||||||
|
if (coarRestApiUrls.length > 0) {
|
||||||
|
this.initPageLinks(coarRestApiUrls);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* It removes the inbox links from the head of the html.
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.subs.forEach((sub: Subscription) => sub.unsubscribe());
|
||||||
|
this.inboxLinks.forEach((link: LinkDefinition) => {
|
||||||
|
this.linkHeadService.removeTag(`href='${link.href}'`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes page links for COAR REST API URLs.
|
||||||
|
* @param coarRestApiUrls An array of COAR REST API URLs.
|
||||||
|
*/
|
||||||
|
private initPageLinks(coarRestApiUrls: string[]): void {
|
||||||
|
const rel = this.notifyInfoService.getInboxRelationLink();
|
||||||
|
let links = '';
|
||||||
|
coarRestApiUrls.forEach((coarRestApiUrl: string) => {
|
||||||
|
// Add link to head
|
||||||
|
const tag: LinkDefinition = {
|
||||||
|
href: coarRestApiUrl,
|
||||||
|
rel: rel,
|
||||||
|
};
|
||||||
|
this.inboxLinks.push(tag);
|
||||||
|
this.linkHeadService.addTag(tag);
|
||||||
|
|
||||||
|
links = links + (isNotEmpty(links) ? ', ' : '') + `<${coarRestApiUrl}> ; rel="${rel}"`;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isPlatformServer(this.platformId)) {
|
||||||
|
// Add link to response header
|
||||||
|
this.responseService.setHeader('Link', links);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -1,19 +1,29 @@
|
|||||||
|
<ds-home-coar></ds-home-coar>
|
||||||
<ds-home-news></ds-home-news>
|
<ds-home-news></ds-home-news>
|
||||||
<div [ngClass]="appConfig.homePage.showDiscoverFilters ? 'container-fluid' : 'container'">
|
<div [ngClass]="showDiscoverFilters ? 'container-fluid' : 'container'">
|
||||||
<div class="row m-5">
|
<ds-page-with-sidebar [sidebarContent]="sidebar" [sideBarWidth]="showDiscoverFilters ? 3 : 0" [class]="showDiscoverFilters ? 'row mx-3' : ''">
|
||||||
<div class="col-sm-3" *ngIf="appConfig.homePage.showDiscoverFilters">
|
<div [class.col-sm-12]="showDiscoverFilters">
|
||||||
<ds-configuration-search-page [sideBarWidth]="12" [showViewModes]="false" [searchEnabled]="false"
|
<button *ngIf="showDiscoverFilters && (isXsOrSm$ | async) && sidebarService.isCollapsed" (click)="sidebarService.expand()"
|
||||||
[inPlaceSearch]="false" [showScopeSelector]="false"></ds-configuration-search-page>
|
class="btn btn-outline-primary d-block ml-auto mb-3">
|
||||||
</div>
|
<i class="fas fa-sliders"></i> {{ 'search.sidebar.open' | translate }}
|
||||||
<div [ngClass]="appConfig.homePage.showDiscoverFilters ? 'col-sm-9' : 'col-sm-12'">
|
</button>
|
||||||
<ng-container *ngIf="(site$ | async) as site">
|
<ng-container *ngIf="(site$ | async) as site">
|
||||||
<ds-view-tracker [object]="site"></ds-view-tracker>
|
<ds-view-tracker [object]="site"></ds-view-tracker>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ds-search-form [inPlaceSearch]="false"
|
<ds-search-form [inPlaceSearch]="false"
|
||||||
[searchPlaceholder]="'home.search-form.placeholder' | translate"></ds-search-form>
|
[searchPlaceholder]="'home.search-form.placeholder' | translate">
|
||||||
|
</ds-search-form>
|
||||||
<ds-top-level-community-list></ds-top-level-community-list>
|
<ds-top-level-community-list></ds-top-level-community-list>
|
||||||
<ds-recent-item-list *ngIf="recentSubmissionspageSize>0"></ds-recent-item-list>
|
<ds-recent-item-list *ngIf="recentSubmissionspageSize>0"></ds-recent-item-list>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</ds-page-with-sidebar>
|
||||||
</div>
|
</div>
|
||||||
<ds-suggestions-popup></ds-suggestions-popup>
|
<ds-suggestions-popup></ds-suggestions-popup>
|
||||||
|
|
||||||
|
<ng-template #sidebar>
|
||||||
|
<div *ngIf="showDiscoverFilters">
|
||||||
|
<ds-configuration-search-page [sideBarWidth]="12" [showViewModes]="false" [searchEnabled]="false"
|
||||||
|
[inPlaceSearch]="false" [showScopeSelector]="false">
|
||||||
|
</ds-configuration-search-page>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
@@ -1,2 +1,5 @@
|
|||||||
:host {
|
:host ::ng-deep {
|
||||||
}
|
.container-fluid .container {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -1,44 +1,31 @@
|
|||||||
import {
|
import {
|
||||||
AsyncPipe,
|
AsyncPipe,
|
||||||
isPlatformServer,
|
|
||||||
NgClass,
|
NgClass,
|
||||||
NgIf,
|
NgIf,
|
||||||
} from '@angular/common';
|
} from '@angular/common';
|
||||||
import {
|
import {
|
||||||
Component,
|
Component,
|
||||||
Inject,
|
Inject,
|
||||||
OnDestroy,
|
|
||||||
OnInit,
|
OnInit,
|
||||||
PLATFORM_ID,
|
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import {
|
import { Observable } from 'rxjs';
|
||||||
EMPTY,
|
import { map } from 'rxjs/operators';
|
||||||
Observable,
|
|
||||||
} from 'rxjs';
|
|
||||||
import {
|
|
||||||
map,
|
|
||||||
switchMap,
|
|
||||||
} from 'rxjs/operators';
|
|
||||||
import {
|
import {
|
||||||
APP_CONFIG,
|
APP_CONFIG,
|
||||||
AppConfig,
|
AppConfig,
|
||||||
} from 'src/config/app-config.interface';
|
} from 'src/config/app-config.interface';
|
||||||
|
|
||||||
import { environment } from '../../environments/environment';
|
|
||||||
import { NotifyInfoService } from '../core/coar-notify/notify-info/notify-info.service';
|
|
||||||
import {
|
|
||||||
LinkDefinition,
|
|
||||||
LinkHeadService,
|
|
||||||
} from '../core/services/link-head.service';
|
|
||||||
import { ServerResponseService } from '../core/services/server-response.service';
|
|
||||||
import { Site } from '../core/shared/site.model';
|
import { Site } from '../core/shared/site.model';
|
||||||
import { SuggestionsPopupComponent } from '../notifications/suggestions-popup/suggestions-popup.component';
|
import { SuggestionsPopupComponent } from '../notifications/suggestions-popup/suggestions-popup.component';
|
||||||
import { ThemedConfigurationSearchPageComponent } from '../search-page/themed-configuration-search-page.component';
|
import { ThemedConfigurationSearchPageComponent } from '../search-page/themed-configuration-search-page.component';
|
||||||
import { isNotEmpty } from '../shared/empty.util';
|
import { HostWindowService } from '../shared/host-window.service';
|
||||||
import { ThemedSearchFormComponent } from '../shared/search-form/themed-search-form.component';
|
import { ThemedSearchFormComponent } from '../shared/search-form/themed-search-form.component';
|
||||||
|
import { PageWithSidebarComponent } from '../shared/sidebar/page-with-sidebar.component';
|
||||||
|
import { SidebarService } from '../shared/sidebar/sidebar.service';
|
||||||
import { ViewTrackerComponent } from '../statistics/angulartics/dspace/view-tracker.component';
|
import { ViewTrackerComponent } from '../statistics/angulartics/dspace/view-tracker.component';
|
||||||
|
import { HomeCoarComponent } from './home-coar/home-coar.component';
|
||||||
import { ThemedHomeNewsComponent } from './home-news/themed-home-news.component';
|
import { ThemedHomeNewsComponent } from './home-news/themed-home-news.component';
|
||||||
import { RecentItemListComponent } from './recent-item-list/recent-item-list.component';
|
import { RecentItemListComponent } from './recent-item-list/recent-item-list.component';
|
||||||
import { ThemedTopLevelCommunityListComponent } from './top-level-community-list/themed-top-level-community-list.component';
|
import { ThemedTopLevelCommunityListComponent } from './top-level-community-list/themed-top-level-community-list.component';
|
||||||
@@ -48,80 +35,30 @@ import { ThemedTopLevelCommunityListComponent } from './top-level-community-list
|
|||||||
styleUrls: ['./home-page.component.scss'],
|
styleUrls: ['./home-page.component.scss'],
|
||||||
templateUrl: './home-page.component.html',
|
templateUrl: './home-page.component.html',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [ThemedHomeNewsComponent, NgIf, ViewTrackerComponent, ThemedSearchFormComponent, ThemedTopLevelCommunityListComponent, RecentItemListComponent, AsyncPipe, TranslateModule, NgClass, ThemedConfigurationSearchPageComponent, SuggestionsPopupComponent],
|
imports: [ThemedHomeNewsComponent, NgIf, ViewTrackerComponent, ThemedSearchFormComponent, ThemedTopLevelCommunityListComponent, RecentItemListComponent, AsyncPipe, TranslateModule, NgClass, ThemedConfigurationSearchPageComponent, SuggestionsPopupComponent, PageWithSidebarComponent, HomeCoarComponent],
|
||||||
})
|
})
|
||||||
export class HomePageComponent implements OnInit, OnDestroy {
|
export class HomePageComponent implements OnInit {
|
||||||
|
|
||||||
site$: Observable<Site>;
|
site$: Observable<Site>;
|
||||||
|
isXsOrSm$: Observable<boolean>;
|
||||||
recentSubmissionspageSize: number;
|
recentSubmissionspageSize: number;
|
||||||
/**
|
showDiscoverFilters: boolean;
|
||||||
* An array of LinkDefinition objects representing inbox links for the home page.
|
|
||||||
*/
|
|
||||||
inboxLinks: LinkDefinition[] = [];
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(APP_CONFIG) protected appConfig: AppConfig,
|
@Inject(APP_CONFIG) protected appConfig: AppConfig,
|
||||||
private route: ActivatedRoute,
|
protected route: ActivatedRoute,
|
||||||
private responseService: ServerResponseService,
|
protected sidebarService: SidebarService,
|
||||||
private notifyInfoService: NotifyInfoService,
|
protected windowService: HostWindowService,
|
||||||
protected linkHeadService: LinkHeadService,
|
|
||||||
@Inject(PLATFORM_ID) private platformId: string,
|
|
||||||
) {
|
) {
|
||||||
this.recentSubmissionspageSize = environment.homePage.recentSubmissions.pageSize;
|
this.recentSubmissionspageSize = this.appConfig.homePage.recentSubmissions.pageSize;
|
||||||
// Get COAR REST API URLs from REST configuration
|
this.showDiscoverFilters = this.appConfig.homePage.showDiscoverFilters;
|
||||||
// only if COAR configuration is enabled
|
|
||||||
this.notifyInfoService.isCoarConfigEnabled().pipe(
|
|
||||||
switchMap((coarLdnEnabled: boolean) => coarLdnEnabled ? this.notifyInfoService.getCoarLdnLocalInboxUrls() : EMPTY, /*{
|
|
||||||
if (coarLdnEnabled) {
|
|
||||||
return this.notifyInfoService.getCoarLdnLocalInboxUrls();
|
|
||||||
} else {
|
|
||||||
return of([]);
|
|
||||||
}
|
|
||||||
}*/),
|
|
||||||
).subscribe((coarRestApiUrls: string[]) => {
|
|
||||||
if (coarRestApiUrls?.length > 0) {
|
|
||||||
this.initPageLinks(coarRestApiUrls);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
this.isXsOrSm$ = this.windowService.isXsOrSm();
|
||||||
this.site$ = this.route.data.pipe(
|
this.site$ = this.route.data.pipe(
|
||||||
map((data) => data.site as Site),
|
map((data) => data.site as Site),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes page links for COAR REST API URLs.
|
|
||||||
* @param coarRestApiUrls An array of COAR REST API URLs.
|
|
||||||
*/
|
|
||||||
private initPageLinks(coarRestApiUrls: string[]): void {
|
|
||||||
const rel = this.notifyInfoService.getInboxRelationLink();
|
|
||||||
let links = '';
|
|
||||||
coarRestApiUrls.forEach((coarRestApiUrl: string) => {
|
|
||||||
// Add link to head
|
|
||||||
const tag: LinkDefinition = {
|
|
||||||
href: coarRestApiUrl,
|
|
||||||
rel: rel,
|
|
||||||
};
|
|
||||||
this.inboxLinks.push(tag);
|
|
||||||
this.linkHeadService.addTag(tag);
|
|
||||||
|
|
||||||
links = links + (isNotEmpty(links) ? ', ' : '') + `<${coarRestApiUrl}> ; rel="${rel}"`;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isPlatformServer(this.platformId)) {
|
|
||||||
// Add link to response header
|
|
||||||
this.responseService.setHeader('Link', links);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* It removes the inbox links from the head of the html.
|
|
||||||
*/
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
this.inboxLinks.forEach((link: LinkDefinition) => {
|
|
||||||
this.linkHeadService.removeTag(`href='${link.href}'`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -1,17 +1,26 @@
|
|||||||
|
import {
|
||||||
|
Route,
|
||||||
|
Routes,
|
||||||
|
} from '@angular/router';
|
||||||
|
|
||||||
import { environment } from '../../environments/environment';
|
import { environment } from '../../environments/environment';
|
||||||
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 { feedbackGuard } from '../core/feedback/feedback.guard';
|
import { feedbackGuard } from '../core/feedback/feedback.guard';
|
||||||
|
import { hasValue } from '../shared/empty.util';
|
||||||
import { ThemedEndUserAgreementComponent } from './end-user-agreement/themed-end-user-agreement.component';
|
import { ThemedEndUserAgreementComponent } from './end-user-agreement/themed-end-user-agreement.component';
|
||||||
import { ThemedFeedbackComponent } from './feedback/themed-feedback.component';
|
import { ThemedFeedbackComponent } from './feedback/themed-feedback.component';
|
||||||
import {
|
import {
|
||||||
|
COAR_NOTIFY_SUPPORT,
|
||||||
END_USER_AGREEMENT_PATH,
|
END_USER_AGREEMENT_PATH,
|
||||||
FEEDBACK_PATH,
|
FEEDBACK_PATH,
|
||||||
PRIVACY_PATH,
|
PRIVACY_PATH,
|
||||||
} from './info-routing-paths';
|
} from './info-routing-paths';
|
||||||
|
import { NotifyInfoComponent } from './notify-info/notify-info.component';
|
||||||
import { ThemedPrivacyComponent } from './privacy/themed-privacy.component';
|
import { ThemedPrivacyComponent } from './privacy/themed-privacy.component';
|
||||||
|
|
||||||
|
|
||||||
export const ROUTES = [
|
export const ROUTES: Routes = [
|
||||||
{
|
{
|
||||||
path: FEEDBACK_PATH,
|
path: FEEDBACK_PATH,
|
||||||
component: ThemedFeedbackComponent,
|
component: ThemedFeedbackComponent,
|
||||||
@@ -31,4 +40,16 @@ export const ROUTES = [
|
|||||||
resolve: { breadcrumb: i18nBreadcrumbResolver },
|
resolve: { breadcrumb: i18nBreadcrumbResolver },
|
||||||
data: { title: 'info.privacy.title', breadcrumbKey: 'info.privacy' },
|
data: { title: 'info.privacy.title', breadcrumbKey: 'info.privacy' },
|
||||||
} : undefined,
|
} : undefined,
|
||||||
];
|
environment.info.enableCOARNotifySupport ? {
|
||||||
|
path: COAR_NOTIFY_SUPPORT,
|
||||||
|
component: NotifyInfoComponent,
|
||||||
|
canActivate: [notifyInfoGuard],
|
||||||
|
resolve: {
|
||||||
|
breadcrumb: i18nBreadcrumbResolver,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
title: 'info.coar-notify-support.title',
|
||||||
|
breadcrumbKey: 'info.coar-notify-support',
|
||||||
|
},
|
||||||
|
} : undefined,
|
||||||
|
].filter((route: Route) => hasValue(route));
|
||||||
|
@@ -3,6 +3,7 @@ import { getInfoModulePath } from '../app-routing-paths';
|
|||||||
export const END_USER_AGREEMENT_PATH = 'end-user-agreement';
|
export const END_USER_AGREEMENT_PATH = 'end-user-agreement';
|
||||||
export const PRIVACY_PATH = 'privacy';
|
export const PRIVACY_PATH = 'privacy';
|
||||||
export const FEEDBACK_PATH = 'feedback';
|
export const FEEDBACK_PATH = 'feedback';
|
||||||
|
export const COAR_NOTIFY_SUPPORT = 'coar-notify-support';
|
||||||
|
|
||||||
export function getEndUserAgreementPath() {
|
export function getEndUserAgreementPath() {
|
||||||
return getSubPath(END_USER_AGREEMENT_PATH);
|
return getSubPath(END_USER_AGREEMENT_PATH);
|
||||||
@@ -16,6 +17,10 @@ export function getFeedbackPath() {
|
|||||||
return getSubPath(FEEDBACK_PATH);
|
return getSubPath(FEEDBACK_PATH);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getCOARNotifySupportPath(): string {
|
||||||
|
return getSubPath(COAR_NOTIFY_SUPPORT);
|
||||||
|
}
|
||||||
|
|
||||||
function getSubPath(path: string) {
|
function getSubPath(path: string) {
|
||||||
return `${getInfoModulePath()}/${path}`;
|
return `${getInfoModulePath()}/${path}`;
|
||||||
}
|
}
|
||||||
|
@@ -6,9 +6,9 @@ import { ActivatedRoute } from '@angular/router';
|
|||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { of } from 'rxjs';
|
import { of } from 'rxjs';
|
||||||
|
|
||||||
import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub';
|
import { NotifyInfoService } from '../../core/coar-notify/notify-info/notify-info.service';
|
||||||
|
import { ActivatedRouteStub } from '../../shared/testing/active-router.stub';
|
||||||
import { NotifyInfoComponent } from './notify-info.component';
|
import { NotifyInfoComponent } from './notify-info.component';
|
||||||
import { NotifyInfoService } from './notify-info.service';
|
|
||||||
|
|
||||||
describe('NotifyInfoComponent', () => {
|
describe('NotifyInfoComponent', () => {
|
||||||
let component: NotifyInfoComponent;
|
let component: NotifyInfoComponent;
|
@@ -11,7 +11,7 @@ import {
|
|||||||
of,
|
of,
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
|
|
||||||
import { NotifyInfoService } from './notify-info.service';
|
import { NotifyInfoService } from '../../core/coar-notify/notify-info/notify-info.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-notify-info',
|
selector: 'ds-notify-info',
|
@@ -13,7 +13,6 @@ import {
|
|||||||
Router,
|
Router,
|
||||||
} from '@angular/router';
|
} from '@angular/router';
|
||||||
import { provideMockStore } from '@ngrx/store/testing';
|
import { provideMockStore } from '@ngrx/store/testing';
|
||||||
import { REQUEST } from '@nguniversal/express-engine/tokens';
|
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
import { AuthRequestService } from 'src/app/core/auth/auth-request.service';
|
import { AuthRequestService } from 'src/app/core/auth/auth-request.service';
|
||||||
@@ -23,6 +22,7 @@ import { ActivatedRouteStub } from 'src/app/shared/testing/active-router.stub';
|
|||||||
import { AuthRequestServiceStub } from 'src/app/shared/testing/auth-request-service.stub';
|
import { AuthRequestServiceStub } from 'src/app/shared/testing/auth-request-service.stub';
|
||||||
|
|
||||||
import { APP_CONFIG } from '../../../../../config/app-config.interface';
|
import { APP_CONFIG } from '../../../../../config/app-config.interface';
|
||||||
|
import { REQUEST } from '../../../../../express.tokens';
|
||||||
import { LinkService } from '../../../../core/cache/builders/link.service';
|
import { LinkService } from '../../../../core/cache/builders/link.service';
|
||||||
import { ConfigurationDataService } from '../../../../core/data/configuration-data.service';
|
import { ConfigurationDataService } from '../../../../core/data/configuration-data.service';
|
||||||
import { FieldChangeType } from '../../../../core/data/object-updates/field-change-type.model';
|
import { FieldChangeType } from '../../../../core/data/object-updates/field-change-type.model';
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
<ds-configuration-search-page
|
<ds-themed-configuration-search-page
|
||||||
[fixedFilterQuery]="fixedFilter"
|
[fixedFilterQuery]="fixedFilter"
|
||||||
[configuration]="configuration"
|
[configuration]="configuration"
|
||||||
[searchEnabled]="searchEnabled"
|
[searchEnabled]="searchEnabled"
|
||||||
[sideBarWidth]="sideBarWidth"
|
[sideBarWidth]="sideBarWidth"
|
||||||
[showCsvExport]="true">
|
[showCsvExport]="true">
|
||||||
</ds-configuration-search-page>
|
</ds-themed-configuration-search-page>
|
||||||
|
@@ -0,0 +1,53 @@
|
|||||||
|
<div class="left-column">
|
||||||
|
<span *ngIf="(workspaceId$ | async) || (workflowId$ | async); then versionNumberWithoutLink else versionNumberWithLink"></span>
|
||||||
|
<ng-template #versionNumberWithLink>
|
||||||
|
<a [routerLink]="getVersionRoute(version.id)">{{version.version}}</a>
|
||||||
|
</ng-template>
|
||||||
|
<ng-template #versionNumberWithoutLink>
|
||||||
|
{{version.version}}
|
||||||
|
</ng-template>
|
||||||
|
<span *ngIf="version?.id === itemVersion?.id">*</span>
|
||||||
|
|
||||||
|
<span *ngIf="workspaceId$ | async" class="text-light badge badge-primary ml-3">
|
||||||
|
{{ "item.version.history.table.workspaceItem" | translate }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span *ngIf="workflowId$ | async" class="text-light badge badge-info ml-3">
|
||||||
|
{{ "item.version.history.table.workflowItem" | translate }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="right-column">
|
||||||
|
|
||||||
|
<div class="btn-group edit-field space-children-mr" *ngIf="displayActions">
|
||||||
|
<!--EDIT WORKSPACE ITEM-->
|
||||||
|
<button class="btn btn-outline-primary btn-sm version-row-element-edit"
|
||||||
|
*ngIf="workspaceId$ | async"
|
||||||
|
(click)="editWorkspaceItem(workspaceId$)"
|
||||||
|
title="{{'item.version.history.table.action.editWorkspaceItem' | translate }}">
|
||||||
|
<i class="fas fa-pencil-alt fa-fw"></i>
|
||||||
|
</button>
|
||||||
|
<!--CREATE-->
|
||||||
|
<ng-container *ngIf="canCreateVersion$ | async">
|
||||||
|
<button class="btn btn-outline-primary btn-sm version-row-element-create"
|
||||||
|
[disabled]="isAnyBeingEdited() || hasDraftVersion"
|
||||||
|
(click)="createNewVersion(version)"
|
||||||
|
title="{{createVersionTitle | translate }}">
|
||||||
|
<i class="fas fa-code-branch fa-fw"></i>
|
||||||
|
</button>
|
||||||
|
</ng-container>
|
||||||
|
<!--DELETE-->
|
||||||
|
<ng-container *ngIf="canDeleteVersion$ | async">
|
||||||
|
<button class="btn btn-sm version-row-element-delete"
|
||||||
|
[ngClass]="isAnyBeingEdited() ? 'btn-outline-primary' : 'btn-outline-danger'"
|
||||||
|
[disabled]="isAnyBeingEdited()"
|
||||||
|
(click)="deleteVersion(version, version.id === itemVersion.id)"
|
||||||
|
title="{{'item.version.history.table.action.deleteVersion' | translate}}">
|
||||||
|
<i class="fas fa-trash fa-fw"></i>
|
||||||
|
</button>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
@@ -0,0 +1,9 @@
|
|||||||
|
.left-column {
|
||||||
|
float: left;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-column {
|
||||||
|
float: right;
|
||||||
|
text-align: right;
|
||||||
|
}
|
@@ -0,0 +1,188 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import {
|
||||||
|
ComponentFixture,
|
||||||
|
TestBed,
|
||||||
|
waitForAsync,
|
||||||
|
} from '@angular/core/testing';
|
||||||
|
import {
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
} from '@angular/forms';
|
||||||
|
import {
|
||||||
|
BrowserModule,
|
||||||
|
By,
|
||||||
|
} from '@angular/platform-browser';
|
||||||
|
import {
|
||||||
|
ActivatedRoute,
|
||||||
|
RouterModule,
|
||||||
|
} from '@angular/router';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import {
|
||||||
|
EMPTY,
|
||||||
|
of as observableOf,
|
||||||
|
of,
|
||||||
|
} from 'rxjs';
|
||||||
|
|
||||||
|
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||||
|
import { VersionDataService } from '../../../core/data/version-data.service';
|
||||||
|
import { VersionHistoryDataService } from '../../../core/data/version-history-data.service';
|
||||||
|
import { Item } from '../../../core/shared/item.model';
|
||||||
|
import { Version } from '../../../core/shared/version.model';
|
||||||
|
import { VersionHistory } from '../../../core/shared/version-history.model';
|
||||||
|
import { WorkflowItemDataService } from '../../../core/submission/workflowitem-data.service';
|
||||||
|
import { WorkspaceitemDataService } from '../../../core/submission/workspaceitem-data.service';
|
||||||
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||||
|
import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub';
|
||||||
|
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
|
||||||
|
import { createPaginatedList } from '../../../shared/testing/utils.test';
|
||||||
|
import { ItemVersionsComponent } from '../item-versions.component';
|
||||||
|
import { ItemVersionsRowElementVersionComponent } from './item-versions-row-element-version.component';
|
||||||
|
|
||||||
|
describe('ItemVersionsRowElementVersionComponent', () => {
|
||||||
|
let component: ItemVersionsRowElementVersionComponent;
|
||||||
|
let fixture: ComponentFixture<ItemVersionsRowElementVersionComponent>;
|
||||||
|
|
||||||
|
const versionHistory = Object.assign(new VersionHistory(), {
|
||||||
|
id: '1',
|
||||||
|
draftVersion: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const version = Object.assign(new Version(), {
|
||||||
|
id: '1',
|
||||||
|
version: 1,
|
||||||
|
created: new Date(2020, 1, 1),
|
||||||
|
summary: 'first version',
|
||||||
|
versionhistory: createSuccessfulRemoteDataObject$(versionHistory),
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: 'version2-url',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
versionHistory.versions = createSuccessfulRemoteDataObject$(createPaginatedList([version]));
|
||||||
|
|
||||||
|
|
||||||
|
const item = Object.assign(new Item(), { // is a workspace item
|
||||||
|
id: 'item-identifier-1',
|
||||||
|
uuid: 'item-identifier-1',
|
||||||
|
handle: '123456789/1',
|
||||||
|
version: createSuccessfulRemoteDataObject$(version),
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: '/items/item-identifier-1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
version.item = createSuccessfulRemoteDataObject$(item);
|
||||||
|
|
||||||
|
const versionHistoryServiceSpy = jasmine.createSpyObj('versionHistoryService', {
|
||||||
|
getVersions: createSuccessfulRemoteDataObject$(createPaginatedList([version])),
|
||||||
|
getVersionHistoryFromVersion$: of(versionHistory),
|
||||||
|
getLatestVersionItemFromHistory$: of(item),
|
||||||
|
});
|
||||||
|
const authorizationServiceSpy = jasmine.createSpyObj('authorizationService', {
|
||||||
|
isAuthorized: observableOf(true),
|
||||||
|
});
|
||||||
|
const workspaceItemDataServiceSpy = jasmine.createSpyObj('workspaceItemDataService', {
|
||||||
|
findByItem: EMPTY,
|
||||||
|
});
|
||||||
|
const workflowItemDataServiceSpy = jasmine.createSpyObj('workflowItemDataService', {
|
||||||
|
findByItem: EMPTY,
|
||||||
|
});
|
||||||
|
const versionServiceSpy = jasmine.createSpyObj('versionService', {
|
||||||
|
findById: EMPTY,
|
||||||
|
});
|
||||||
|
const itemDataServiceSpy = jasmine.createSpyObj('itemDataService', {
|
||||||
|
delete: createSuccessfulRemoteDataObject$({}),
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot(), RouterModule.forRoot([
|
||||||
|
{ path: 'items/:id/edit/versionhistory', component: {} as any },
|
||||||
|
]), CommonModule, FormsModule, ReactiveFormsModule, BrowserModule, ItemVersionsComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
|
||||||
|
{ provide: AuthorizationDataService, useValue: authorizationServiceSpy },
|
||||||
|
{ provide: VersionHistoryDataService, useValue: versionHistoryServiceSpy },
|
||||||
|
{ provide: ItemDataService, useValue: itemDataServiceSpy },
|
||||||
|
{ provide: VersionDataService, useValue: versionServiceSpy },
|
||||||
|
{ provide: WorkspaceitemDataService, useValue: workspaceItemDataServiceSpy },
|
||||||
|
{ provide: WorkflowItemDataService, useValue: workflowItemDataServiceSpy },
|
||||||
|
{ provide: ActivatedRoute, useValue: new ActivatedRouteStub() },
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA],
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ItemVersionsRowElementVersionComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
|
||||||
|
component.version = version;
|
||||||
|
component.itemVersion = version;
|
||||||
|
component.item = item;
|
||||||
|
component.displayActions = true;
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should display version ${version.version} in the correct column for version ${version.id}`, () => {
|
||||||
|
const id = fixture.debugElement.query(By.css(`.left-column`));
|
||||||
|
expect(id.nativeElement.textContent).toContain(version.version.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should displau an asterisk in the correct column for current version`, () => {
|
||||||
|
const draft = fixture.debugElement.query(By.css(`.left-column`));
|
||||||
|
expect(draft.nativeElement.textContent).toContain('*');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display action buttons in the correct column if displayActions is true', () => {
|
||||||
|
fixture.detectChanges();
|
||||||
|
const actions = fixture.debugElement.query(By.css(`.right-column`));
|
||||||
|
expect(actions).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when deleting a version', () => {
|
||||||
|
let deleteButton;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
deleteButton = fixture.debugElement.queryAll(By.css('.version-row-element-delete'))[0].nativeElement;
|
||||||
|
|
||||||
|
itemDataServiceSpy.delete.calls.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if confirmed via modal', () => {
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
deleteButton.click();
|
||||||
|
fixture.detectChanges();
|
||||||
|
(document as any).querySelector('.modal-footer .confirm').click();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should call ItemService.delete', () => {
|
||||||
|
expect(itemDataServiceSpy.delete).toHaveBeenCalledWith(item.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if canceled via modal', () => {
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
deleteButton.click();
|
||||||
|
fixture.detectChanges();
|
||||||
|
(document as any).querySelector('.modal-footer .cancel').click();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should not call ItemService.delete', () => {
|
||||||
|
expect(itemDataServiceSpy.delete).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@@ -0,0 +1,302 @@
|
|||||||
|
import {
|
||||||
|
AsyncPipe,
|
||||||
|
NgClass,
|
||||||
|
NgIf,
|
||||||
|
} from '@angular/common';
|
||||||
|
import {
|
||||||
|
Component,
|
||||||
|
EventEmitter,
|
||||||
|
Input,
|
||||||
|
OnInit,
|
||||||
|
Output,
|
||||||
|
} from '@angular/core';
|
||||||
|
import {
|
||||||
|
Router,
|
||||||
|
RouterLink,
|
||||||
|
} from '@angular/router';
|
||||||
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import {
|
||||||
|
TranslateModule,
|
||||||
|
TranslateService,
|
||||||
|
} from '@ngx-translate/core';
|
||||||
|
import {
|
||||||
|
combineLatest,
|
||||||
|
concatMap,
|
||||||
|
Observable,
|
||||||
|
of,
|
||||||
|
} from 'rxjs';
|
||||||
|
import {
|
||||||
|
map,
|
||||||
|
mergeMap,
|
||||||
|
switchMap,
|
||||||
|
take,
|
||||||
|
tap,
|
||||||
|
} from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
|
||||||
|
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||||
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
|
import { VersionDataService } from '../../../core/data/version-data.service';
|
||||||
|
import { VersionHistoryDataService } from '../../../core/data/version-history-data.service';
|
||||||
|
import { Item } from '../../../core/shared/item.model';
|
||||||
|
import {
|
||||||
|
getFirstCompletedRemoteData,
|
||||||
|
getFirstSucceededRemoteDataPayload,
|
||||||
|
} from '../../../core/shared/operators';
|
||||||
|
import { Version } from '../../../core/shared/version.model';
|
||||||
|
import { VersionHistory } from '../../../core/shared/version-history.model';
|
||||||
|
import { WorkspaceItem } from '../../../core/submission/models/workspaceitem.model';
|
||||||
|
import { WorkflowItemDataService } from '../../../core/submission/workflowitem-data.service';
|
||||||
|
import { WorkspaceitemDataService } from '../../../core/submission/workspaceitem-data.service';
|
||||||
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
|
import {
|
||||||
|
getItemEditVersionhistoryRoute,
|
||||||
|
getItemVersionRoute,
|
||||||
|
} from '../../item-page-routing-paths';
|
||||||
|
import { ItemVersionsDeleteModalComponent } from '../item-versions-delete-modal/item-versions-delete-modal.component';
|
||||||
|
import { ItemVersionsSharedService } from '../item-versions-shared.service';
|
||||||
|
import { ItemVersionsSummaryModalComponent } from '../item-versions-summary-modal/item-versions-summary-modal.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-item-versions-row-element-version',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
AsyncPipe,
|
||||||
|
RouterLink,
|
||||||
|
TranslateModule,
|
||||||
|
NgClass,
|
||||||
|
NgIf,
|
||||||
|
],
|
||||||
|
templateUrl: './item-versions-row-element-version.component.html',
|
||||||
|
styleUrl: './item-versions-row-element-version.component.scss',
|
||||||
|
})
|
||||||
|
export class ItemVersionsRowElementVersionComponent implements OnInit {
|
||||||
|
@Input() hasDraftVersion: boolean | null;
|
||||||
|
@Input() version: Version;
|
||||||
|
@Input() itemVersion: Version;
|
||||||
|
@Input() item: Item;
|
||||||
|
@Input() displayActions: boolean;
|
||||||
|
@Input() versionBeingEditedNumber: number;
|
||||||
|
|
||||||
|
@Output() versionsHistoryChange = new EventEmitter<Observable<VersionHistory>>();
|
||||||
|
|
||||||
|
workspaceId$: Observable<string>;
|
||||||
|
workflowId$: Observable<string>;
|
||||||
|
canDeleteVersion$: Observable<boolean>;
|
||||||
|
canCreateVersion$: Observable<boolean>;
|
||||||
|
|
||||||
|
createVersionTitle: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private workspaceItemDataService: WorkspaceitemDataService,
|
||||||
|
private workflowItemDataService: WorkflowItemDataService,
|
||||||
|
private router: Router,
|
||||||
|
private itemService: ItemDataService,
|
||||||
|
private authorizationService: AuthorizationDataService,
|
||||||
|
private itemVersionShared: ItemVersionsSharedService,
|
||||||
|
private versionHistoryService: VersionHistoryDataService,
|
||||||
|
private versionService: VersionDataService,
|
||||||
|
private notificationsService: NotificationsService,
|
||||||
|
private translateService: TranslateService,
|
||||||
|
private modalService: NgbModal,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.workspaceId$ = this.getWorkspaceId(this.version.item);
|
||||||
|
this.workflowId$ = this.getWorkflowId(this.version.item);
|
||||||
|
this.canDeleteVersion$ = this.canDeleteVersion(this.version);
|
||||||
|
this.canCreateVersion$ = this.authorizationService.isAuthorized(FeatureID.CanCreateVersion, this.item.self);
|
||||||
|
|
||||||
|
this.createVersionTitle = this.hasDraftVersion ? 'item.version.history.table.action.hasDraft' : 'item.version.history.table.action.newVersion';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the ID of the workspace item, if present, otherwise return undefined
|
||||||
|
* @param versionItem the item for which retrieve the workspace item id
|
||||||
|
*/
|
||||||
|
getWorkspaceId(versionItem: Observable<RemoteData<Item>>): Observable<string> {
|
||||||
|
if (!this.hasDraftVersion) {
|
||||||
|
return of(undefined);
|
||||||
|
}
|
||||||
|
return versionItem.pipe(
|
||||||
|
getFirstSucceededRemoteDataPayload(),
|
||||||
|
map((item: Item) => item.uuid),
|
||||||
|
switchMap((itemUuid: string) => this.workspaceItemDataService.findByItem(itemUuid, true)),
|
||||||
|
getFirstCompletedRemoteData<WorkspaceItem>(),
|
||||||
|
map((res: RemoteData<WorkspaceItem>) => res?.payload?.id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the ID of the workflow item, if present, otherwise return undefined
|
||||||
|
* @param versionItem the item for which retrieve the workspace item id
|
||||||
|
*/
|
||||||
|
getWorkflowId(versionItem: Observable<RemoteData<Item>>): Observable<string> {
|
||||||
|
return this.getWorkspaceId(versionItem).pipe(
|
||||||
|
concatMap((workspaceId: string) => {
|
||||||
|
if (workspaceId) {
|
||||||
|
return of(undefined);
|
||||||
|
}
|
||||||
|
return versionItem.pipe(
|
||||||
|
getFirstSucceededRemoteDataPayload(),
|
||||||
|
map((item: Item) => item.uuid),
|
||||||
|
switchMap((itemUuid: string) => this.workflowItemDataService.findByItem(itemUuid, true)),
|
||||||
|
getFirstCompletedRemoteData<WorkspaceItem>(),
|
||||||
|
map((res: RemoteData<WorkspaceItem>) => res?.payload?.id),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* redirect to the edit page of the workspace item
|
||||||
|
* @param id$ the id of the workspace item
|
||||||
|
*/
|
||||||
|
editWorkspaceItem(id$: Observable<string>) {
|
||||||
|
id$.subscribe((id) => {
|
||||||
|
void this.router.navigateByUrl('workspaceitems/' + id + '/edit');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the current user can delete the version
|
||||||
|
* @param version
|
||||||
|
*/
|
||||||
|
canDeleteVersion(version: Version): Observable<boolean> {
|
||||||
|
return this.authorizationService.isAuthorized(FeatureID.CanDeleteVersion, version.self);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the route to the specified version
|
||||||
|
* @param versionId the ID of the version for which the route will be retrieved
|
||||||
|
*/
|
||||||
|
getVersionRoute(versionId: string) {
|
||||||
|
return getItemVersionRoute(versionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new version starting from the specified one
|
||||||
|
* @param version the version from which a new one will be created
|
||||||
|
*/
|
||||||
|
createNewVersion(version: Version) {
|
||||||
|
const versionNumber = version.version;
|
||||||
|
|
||||||
|
// Open modal and set current version number
|
||||||
|
const activeModal = this.modalService.open(ItemVersionsSummaryModalComponent);
|
||||||
|
activeModal.componentInstance.versionNumber = versionNumber;
|
||||||
|
|
||||||
|
// On createVersionEvent emitted create new version and notify
|
||||||
|
activeModal.componentInstance.createVersionEvent.pipe(
|
||||||
|
mergeMap((summary: string) => combineLatest([
|
||||||
|
of(summary),
|
||||||
|
version.item.pipe(getFirstSucceededRemoteDataPayload()),
|
||||||
|
])),
|
||||||
|
mergeMap(([summary, item]: [string, Item]) => this.versionHistoryService.createVersion(item._links.self.href, summary)),
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
// close model (should be displaying loading/waiting indicator) when version creation failed/succeeded
|
||||||
|
tap(() => activeModal.close()),
|
||||||
|
// show success/failure notification
|
||||||
|
tap((newVersionRD: RemoteData<Version>) => {
|
||||||
|
this.itemVersionShared.notifyCreateNewVersion(newVersionRD);
|
||||||
|
if (newVersionRD.hasSucceeded) {
|
||||||
|
const versionHistory$ = this.versionService.getHistoryFromVersion(version).pipe(
|
||||||
|
tap((versionHistory: VersionHistory) => {
|
||||||
|
this.itemService.invalidateItemCache(this.item.uuid);
|
||||||
|
this.versionHistoryService.invalidateVersionHistoryCache(versionHistory.id);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
this.versionsHistoryChange.emit(versionHistory$);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
// get workspace item
|
||||||
|
getFirstSucceededRemoteDataPayload<Version>(),
|
||||||
|
switchMap((newVersion: Version) => this.itemService.findByHref(newVersion._links.item.href)),
|
||||||
|
getFirstSucceededRemoteDataPayload<Item>(),
|
||||||
|
switchMap((newVersionItem: Item) => this.workspaceItemDataService.findByItem(newVersionItem.uuid, true, false)),
|
||||||
|
getFirstSucceededRemoteDataPayload<WorkspaceItem>(),
|
||||||
|
).subscribe((wsItem) => {
|
||||||
|
const wsiId = wsItem.id;
|
||||||
|
const route = 'workspaceitems/' + wsiId + '/edit';
|
||||||
|
this.router.navigateByUrl(route);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the specified version, notify the success/failure and redirect to latest version
|
||||||
|
* @param version the version to be deleted
|
||||||
|
* @param redirectToLatest force the redirect to the latest version in the history
|
||||||
|
*/
|
||||||
|
deleteVersion(version: Version, redirectToLatest: boolean): void {
|
||||||
|
const successMessageKey = 'item.version.delete.notification.success';
|
||||||
|
const failureMessageKey = 'item.version.delete.notification.failure';
|
||||||
|
const versionNumber = version.version;
|
||||||
|
const versionItem$ = version.item;
|
||||||
|
|
||||||
|
// Open modal
|
||||||
|
const activeModal = this.modalService.open(ItemVersionsDeleteModalComponent);
|
||||||
|
activeModal.componentInstance.versionNumber = version.version;
|
||||||
|
activeModal.componentInstance.firstVersion = false;
|
||||||
|
|
||||||
|
// On modal submit/dismiss
|
||||||
|
activeModal.componentInstance.response.pipe(take(1)).subscribe((ok) => {
|
||||||
|
if (ok) {
|
||||||
|
versionItem$.pipe(
|
||||||
|
getFirstSucceededRemoteDataPayload<Item>(),
|
||||||
|
// Retrieve version history
|
||||||
|
mergeMap((item: Item) => combineLatest([
|
||||||
|
of(item),
|
||||||
|
this.versionHistoryService.getVersionHistoryFromVersion$(version),
|
||||||
|
])),
|
||||||
|
// Delete item
|
||||||
|
mergeMap(([item, versionHistory]: [Item, VersionHistory]) => combineLatest([
|
||||||
|
this.deleteItemAndGetResult$(item),
|
||||||
|
of(versionHistory),
|
||||||
|
])),
|
||||||
|
// Retrieve new latest version
|
||||||
|
mergeMap(([deleteItemResult, versionHistory]: [boolean, VersionHistory]) => combineLatest([
|
||||||
|
of(deleteItemResult),
|
||||||
|
this.versionHistoryService.getLatestVersionItemFromHistory$(versionHistory).pipe(
|
||||||
|
tap(() => {
|
||||||
|
this.versionsHistoryChange.emit(of(versionHistory));
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
])),
|
||||||
|
).subscribe(([deleteHasSucceeded, newLatestVersionItem]: [boolean, Item]) => {
|
||||||
|
// Notify operation result and redirect to latest item
|
||||||
|
if (deleteHasSucceeded) {
|
||||||
|
this.notificationsService.success(null, this.translateService.get(successMessageKey, { 'version': versionNumber }));
|
||||||
|
} else {
|
||||||
|
this.notificationsService.error(null, this.translateService.get(failureMessageKey, { 'version': versionNumber }));
|
||||||
|
}
|
||||||
|
if (redirectToLatest) {
|
||||||
|
const path = getItemEditVersionhistoryRoute(newLatestVersionItem);
|
||||||
|
this.router.navigateByUrl(path);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete the item and get the result of the operation
|
||||||
|
* @param item
|
||||||
|
*/
|
||||||
|
deleteItemAndGetResult$(item: Item): Observable<boolean> {
|
||||||
|
return this.itemService.delete(item.id).pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
map((deleteItemRes) => deleteItemRes.hasSucceeded),
|
||||||
|
take(1),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when a version is being edited
|
||||||
|
* (used to disable buttons for other versions)
|
||||||
|
*/
|
||||||
|
isAnyBeingEdited(): boolean {
|
||||||
|
return this.versionBeingEditedNumber != null;
|
||||||
|
}
|
||||||
|
}
|
@@ -17,7 +17,7 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">{{"item.version.history.table.version" | translate}}</th>
|
<th scope="col">{{"item.version.history.table.version" | translate}}</th>
|
||||||
<th scope="col" *ngIf="(showSubmitter() | async)">{{"item.version.history.table.editor" | translate}}</th>
|
<th scope="col" *ngIf="(showSubmitter$ | async)">{{"item.version.history.table.editor" | translate}}</th>
|
||||||
<th scope="col">{{"item.version.history.table.date" | translate}}</th>
|
<th scope="col">{{"item.version.history.table.date" | translate}}</th>
|
||||||
<th scope="col">{{"item.version.history.table.summary" | translate}}</th>
|
<th scope="col">{{"item.version.history.table.summary" | translate}}</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -25,69 +25,15 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let version of versions?.page" [id]="'version-row-' + version.id">
|
<tr *ngFor="let version of versions?.page" [id]="'version-row-' + version.id">
|
||||||
<td class="version-row-element-version">
|
<td class="version-row-element-version">
|
||||||
<!-- Get the ID of the workspace/workflow item (`undefined` if they don't exist).
|
<ds-item-versions-row-element-version [hasDraftVersion]="hasDraftVersion$ | async"
|
||||||
Conditionals inside *ngVar are needed in order to avoid useless calls. -->
|
[version]="version"
|
||||||
<ng-container *ngVar="((hasDraftVersion$ | async) ? getWorkspaceId(version?.item) : undefined) as workspaceId$">
|
[item]="item" [displayActions]="displayActions"
|
||||||
<ng-container *ngVar=" ((workspaceId$ | async) ? undefined : getWorkflowId(version?.item)) as workflowId$">
|
[itemVersion]="itemVersion"
|
||||||
|
[versionBeingEditedNumber]="versionBeingEditedNumber"
|
||||||
<div class="left-column">
|
(versionsHistoryChange)="getAllVersions($event)"
|
||||||
|
></ds-item-versions-row-element-version>
|
||||||
<span *ngIf="(workspaceId$ | async) || (workflowId$ | async); then versionNumberWithoutLink else versionNumberWithLink"></span>
|
|
||||||
<ng-template #versionNumberWithLink>
|
|
||||||
<a [routerLink]="getVersionRoute(version.id)">{{version.version}}</a>
|
|
||||||
</ng-template>
|
|
||||||
<ng-template #versionNumberWithoutLink>
|
|
||||||
{{version.version}}
|
|
||||||
</ng-template>
|
|
||||||
<span *ngIf="version?.id === itemVersion?.id">*</span>
|
|
||||||
|
|
||||||
<span *ngIf="workspaceId$ | async" class="text-light badge badge-primary ml-3">
|
|
||||||
{{ "item.version.history.table.workspaceItem" | translate }}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span *ngIf="workflowId$ | async" class="text-light badge badge-info ml-3">
|
|
||||||
{{ "item.version.history.table.workflowItem" | translate }}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="right-column">
|
|
||||||
|
|
||||||
<div class="btn-group edit-field space-children-mr" *ngIf="displayActions">
|
|
||||||
<!--EDIT WORKSPACE ITEM-->
|
|
||||||
<button class="btn btn-outline-primary btn-sm version-row-element-edit"
|
|
||||||
*ngIf="workspaceId$ | async"
|
|
||||||
(click)="editWorkspaceItem(workspaceId$)"
|
|
||||||
title="{{'item.version.history.table.action.editWorkspaceItem' | translate }}">
|
|
||||||
<i class="fas fa-pencil-alt fa-fw"></i>
|
|
||||||
</button>
|
|
||||||
<!--CREATE-->
|
|
||||||
<ng-container *ngIf="canCreateVersion$ | async">
|
|
||||||
<button class="btn btn-outline-primary btn-sm version-row-element-create"
|
|
||||||
[disabled]="isAnyBeingEdited() || (hasDraftVersion$ | async)"
|
|
||||||
(click)="createNewVersion(version)"
|
|
||||||
title="{{createVersionTitle$ | async | translate }}">
|
|
||||||
<i class="fas fa-code-branch fa-fw"></i>
|
|
||||||
</button>
|
|
||||||
</ng-container>
|
|
||||||
<!--DELETE-->
|
|
||||||
<ng-container *ngIf="canDeleteVersion$(version) | async">
|
|
||||||
<button class="btn btn-sm version-row-element-delete"
|
|
||||||
[ngClass]="isAnyBeingEdited() ? 'btn-outline-primary' : 'btn-outline-danger'"
|
|
||||||
[disabled]="isAnyBeingEdited()"
|
|
||||||
(click)="deleteVersion(version, version.id===itemVersion.id)"
|
|
||||||
title="{{'item.version.history.table.action.deleteVersion' | translate}}">
|
|
||||||
<i class="fas fa-trash fa-fw"></i>
|
|
||||||
</button>
|
|
||||||
</ng-container>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</ng-container>
|
|
||||||
</ng-container>
|
|
||||||
</td>
|
</td>
|
||||||
<td class="version-row-element-editor" *ngIf="(showSubmitter() | async)">
|
<td class="version-row-element-editor" *ngIf="(showSubmitter$ | async)">
|
||||||
{{version?.submitterName}}
|
{{version?.submitterName}}
|
||||||
</td>
|
</td>
|
||||||
<td class="version-row-element-date">
|
<td class="version-row-element-date">
|
||||||
|
@@ -1,9 +0,0 @@
|
|||||||
.left-column {
|
|
||||||
float: left;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.right-column {
|
|
||||||
float: right;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
@@ -206,19 +206,6 @@ describe('ItemVersionsComponent', () => {
|
|||||||
versions.forEach((version: Version, index: number) => {
|
versions.forEach((version: Version, index: number) => {
|
||||||
const versionItem = items[index];
|
const versionItem = items[index];
|
||||||
|
|
||||||
it(`should display version ${version.version} in the correct column for version ${version.id}`, () => {
|
|
||||||
const id = fixture.debugElement.query(By.css(`#version-row-${version.id} .version-row-element-version`));
|
|
||||||
expect(id.nativeElement.textContent).toContain(version.version.toString());
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check if the current version contains an asterisk
|
|
||||||
if (item1.uuid === versionItem.uuid) {
|
|
||||||
it('should add an asterisk to the version of the selected item', () => {
|
|
||||||
const item = fixture.debugElement.query(By.css(`#version-row-${version.id} .version-row-element-version`));
|
|
||||||
expect(item.nativeElement.textContent).toContain('*');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
it(`should display date ${version.created} in the correct column for version ${version.id}`, () => {
|
it(`should display date ${version.created} in the correct column for version ${version.id}`, () => {
|
||||||
const date = fixture.debugElement.query(By.css(`#version-row-${version.id} .version-row-element-date`));
|
const date = fixture.debugElement.query(By.css(`#version-row-${version.id} .version-row-element-date`));
|
||||||
switch (versionItem.uuid) {
|
switch (versionItem.uuid) {
|
||||||
@@ -319,44 +306,4 @@ describe('ItemVersionsComponent', () => {
|
|||||||
expect(component.isThisBeingEdited(version2)).toBeFalse();
|
expect(component.isThisBeingEdited(version2)).toBeFalse();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when deleting a version', () => {
|
|
||||||
let deleteButton;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
const canDelete = (featureID: FeatureID, url: string ) => of(featureID === FeatureID.CanDeleteVersion);
|
|
||||||
authorizationServiceSpy.isAuthorized.and.callFake(canDelete);
|
|
||||||
|
|
||||||
fixture.detectChanges();
|
|
||||||
|
|
||||||
// delete the last version in the table (version2 → item2)
|
|
||||||
deleteButton = fixture.debugElement.queryAll(By.css('.version-row-element-delete'))[1].nativeElement;
|
|
||||||
|
|
||||||
itemDataServiceSpy.delete.calls.reset();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('if confirmed via modal', () => {
|
|
||||||
beforeEach(waitForAsync(() => {
|
|
||||||
deleteButton.click();
|
|
||||||
fixture.detectChanges();
|
|
||||||
(document as any).querySelector('.modal-footer .confirm').click();
|
|
||||||
}));
|
|
||||||
|
|
||||||
it('should call ItemService.delete', () => {
|
|
||||||
expect(itemDataServiceSpy.delete).toHaveBeenCalledWith(item2.id);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('if canceled via modal', () => {
|
|
||||||
beforeEach(waitForAsync(() => {
|
|
||||||
deleteButton.click();
|
|
||||||
fixture.detectChanges();
|
|
||||||
(document as any).querySelector('.modal-footer .cancel').click();
|
|
||||||
}));
|
|
||||||
|
|
||||||
it('should not call ItemService.delete', () => {
|
|
||||||
expect(itemDataServiceSpy.delete).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@@ -11,15 +11,8 @@ import {
|
|||||||
OnDestroy,
|
OnDestroy,
|
||||||
OnInit,
|
OnInit,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import {
|
import { FormsModule } from '@angular/forms';
|
||||||
FormsModule,
|
import { RouterLink } from '@angular/router';
|
||||||
UntypedFormBuilder,
|
|
||||||
} from '@angular/forms';
|
|
||||||
import {
|
|
||||||
Router,
|
|
||||||
RouterLink,
|
|
||||||
} from '@angular/router';
|
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
|
||||||
import {
|
import {
|
||||||
TranslateModule,
|
TranslateModule,
|
||||||
TranslateService,
|
TranslateService,
|
||||||
@@ -28,22 +21,18 @@ import {
|
|||||||
BehaviorSubject,
|
BehaviorSubject,
|
||||||
combineLatest,
|
combineLatest,
|
||||||
Observable,
|
Observable,
|
||||||
of,
|
|
||||||
Subscription,
|
Subscription,
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import {
|
import {
|
||||||
map,
|
map,
|
||||||
mergeMap,
|
|
||||||
startWith,
|
startWith,
|
||||||
switchMap,
|
switchMap,
|
||||||
take,
|
take,
|
||||||
tap,
|
|
||||||
} from 'rxjs/operators';
|
} from 'rxjs/operators';
|
||||||
|
|
||||||
import { ConfigurationDataService } from '../../core/data/configuration-data.service';
|
import { ConfigurationDataService } from '../../core/data/configuration-data.service';
|
||||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||||
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||||
import { ItemDataService } from '../../core/data/item-data.service';
|
|
||||||
import { PaginatedList } from '../../core/data/paginated-list.model';
|
import { PaginatedList } from '../../core/data/paginated-list.model';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
import { VersionDataService } from '../../core/data/version-data.service';
|
import { VersionDataService } from '../../core/data/version-data.service';
|
||||||
@@ -60,9 +49,6 @@ import {
|
|||||||
} from '../../core/shared/operators';
|
} from '../../core/shared/operators';
|
||||||
import { Version } from '../../core/shared/version.model';
|
import { Version } from '../../core/shared/version.model';
|
||||||
import { VersionHistory } from '../../core/shared/version-history.model';
|
import { VersionHistory } from '../../core/shared/version-history.model';
|
||||||
import { WorkspaceItem } from '../../core/submission/models/workspaceitem.model';
|
|
||||||
import { WorkflowItemDataService } from '../../core/submission/workflowitem-data.service';
|
|
||||||
import { WorkspaceitemDataService } from '../../core/submission/workspaceitem-data.service';
|
|
||||||
import { AlertComponent } from '../../shared/alert/alert.component';
|
import { AlertComponent } from '../../shared/alert/alert.component';
|
||||||
import { AlertType } from '../../shared/alert/alert-type';
|
import { AlertType } from '../../shared/alert/alert-type';
|
||||||
import {
|
import {
|
||||||
@@ -75,21 +61,15 @@ import { PaginationComponentOptions } from '../../shared/pagination/pagination-c
|
|||||||
import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model';
|
import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model';
|
||||||
import { followLink } from '../../shared/utils/follow-link-config.model';
|
import { followLink } from '../../shared/utils/follow-link-config.model';
|
||||||
import { VarDirective } from '../../shared/utils/var.directive';
|
import { VarDirective } from '../../shared/utils/var.directive';
|
||||||
import {
|
import { getItemPageRoute } from '../item-page-routing-paths';
|
||||||
getItemEditVersionhistoryRoute,
|
import { ItemVersionsRowElementVersionComponent } from './item-versions-row-element-version/item-versions-row-element-version.component';
|
||||||
getItemPageRoute,
|
|
||||||
getItemVersionRoute,
|
|
||||||
} from '../item-page-routing-paths';
|
|
||||||
import { ItemVersionsDeleteModalComponent } from './item-versions-delete-modal/item-versions-delete-modal.component';
|
|
||||||
import { ItemVersionsSharedService } from './item-versions-shared.service';
|
|
||||||
import { ItemVersionsSummaryModalComponent } from './item-versions-summary-modal/item-versions-summary-modal.component';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-item-versions',
|
selector: 'ds-item-versions',
|
||||||
templateUrl: './item-versions.component.html',
|
templateUrl: './item-versions.component.html',
|
||||||
styleUrls: ['./item-versions.component.scss'],
|
styleUrls: ['./item-versions.component.scss'],
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [VarDirective, NgIf, AlertComponent, PaginationComponent, NgFor, RouterLink, NgClass, FormsModule, AsyncPipe, DatePipe, TranslateModule],
|
imports: [VarDirective, NgIf, AlertComponent, PaginationComponent, NgFor, RouterLink, NgClass, FormsModule, AsyncPipe, DatePipe, TranslateModule, ItemVersionsRowElementVersionComponent],
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -162,6 +142,11 @@ export class ItemVersionsComponent implements OnDestroy, OnInit {
|
|||||||
*/
|
*/
|
||||||
hasDraftVersion$: Observable<boolean>;
|
hasDraftVersion$: Observable<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show submitter in version history table
|
||||||
|
*/
|
||||||
|
showSubmitter$: Observable<boolean> = this.showSubmitter();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The amount of versions to display per page
|
* The amount of versions to display per page
|
||||||
*/
|
*/
|
||||||
@@ -206,17 +191,10 @@ export class ItemVersionsComponent implements OnDestroy, OnInit {
|
|||||||
|
|
||||||
constructor(private versionHistoryService: VersionHistoryDataService,
|
constructor(private versionHistoryService: VersionHistoryDataService,
|
||||||
private versionService: VersionDataService,
|
private versionService: VersionDataService,
|
||||||
private itemService: ItemDataService,
|
|
||||||
private paginationService: PaginationService,
|
private paginationService: PaginationService,
|
||||||
private formBuilder: UntypedFormBuilder,
|
|
||||||
private modalService: NgbModal,
|
|
||||||
private notificationsService: NotificationsService,
|
private notificationsService: NotificationsService,
|
||||||
private translateService: TranslateService,
|
private translateService: TranslateService,
|
||||||
private router: Router,
|
|
||||||
private itemVersionShared: ItemVersionsSharedService,
|
|
||||||
private authorizationService: AuthorizationDataService,
|
private authorizationService: AuthorizationDataService,
|
||||||
private workspaceItemDataService: WorkspaceitemDataService,
|
|
||||||
private workflowItemDataService: WorkflowItemDataService,
|
|
||||||
private configurationService: ConfigurationDataService,
|
private configurationService: ConfigurationDataService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
@@ -255,14 +233,6 @@ export class ItemVersionsComponent implements OnDestroy, OnInit {
|
|||||||
this.versionBeingEditedId = undefined;
|
this.versionBeingEditedId = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the route to the specified version
|
|
||||||
* @param versionId the ID of the version for which the route will be retrieved
|
|
||||||
*/
|
|
||||||
getVersionRoute(versionId: string) {
|
|
||||||
return getItemVersionRoute(versionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Applies changes to version currently being edited
|
* Applies changes to version currently being edited
|
||||||
*/
|
*/
|
||||||
@@ -291,121 +261,6 @@ export class ItemVersionsComponent implements OnDestroy, OnInit {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete the item and get the result of the operation
|
|
||||||
* @param item
|
|
||||||
*/
|
|
||||||
deleteItemAndGetResult$(item: Item): Observable<boolean> {
|
|
||||||
return this.itemService.delete(item.id).pipe(
|
|
||||||
getFirstCompletedRemoteData(),
|
|
||||||
map((deleteItemRes) => deleteItemRes.hasSucceeded),
|
|
||||||
take(1),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes the specified version, notify the success/failure and redirect to latest version
|
|
||||||
* @param version the version to be deleted
|
|
||||||
* @param redirectToLatest force the redirect to the latest version in the history
|
|
||||||
*/
|
|
||||||
deleteVersion(version: Version, redirectToLatest: boolean): void {
|
|
||||||
const successMessageKey = 'item.version.delete.notification.success';
|
|
||||||
const failureMessageKey = 'item.version.delete.notification.failure';
|
|
||||||
const versionNumber = version.version;
|
|
||||||
const versionItem$ = version.item;
|
|
||||||
|
|
||||||
// Open modal
|
|
||||||
const activeModal = this.modalService.open(ItemVersionsDeleteModalComponent);
|
|
||||||
activeModal.componentInstance.versionNumber = version.version;
|
|
||||||
activeModal.componentInstance.firstVersion = false;
|
|
||||||
|
|
||||||
// On modal submit/dismiss
|
|
||||||
activeModal.componentInstance.response.pipe(take(1)).subscribe((ok) => {
|
|
||||||
if (ok) {
|
|
||||||
versionItem$.pipe(
|
|
||||||
getFirstSucceededRemoteDataPayload<Item>(),
|
|
||||||
// Retrieve version history
|
|
||||||
mergeMap((item: Item) => combineLatest([
|
|
||||||
of(item),
|
|
||||||
this.versionHistoryService.getVersionHistoryFromVersion$(version),
|
|
||||||
])),
|
|
||||||
// Delete item
|
|
||||||
mergeMap(([item, versionHistory]: [Item, VersionHistory]) => combineLatest([
|
|
||||||
this.deleteItemAndGetResult$(item),
|
|
||||||
of(versionHistory),
|
|
||||||
])),
|
|
||||||
// Retrieve new latest version
|
|
||||||
mergeMap(([deleteItemResult, versionHistory]: [boolean, VersionHistory]) => combineLatest([
|
|
||||||
of(deleteItemResult),
|
|
||||||
this.versionHistoryService.getLatestVersionItemFromHistory$(versionHistory).pipe(
|
|
||||||
tap(() => {
|
|
||||||
this.getAllVersions(of(versionHistory));
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
])),
|
|
||||||
).subscribe(([deleteHasSucceeded, newLatestVersionItem]: [boolean, Item]) => {
|
|
||||||
// Notify operation result and redirect to latest item
|
|
||||||
if (deleteHasSucceeded) {
|
|
||||||
this.notificationsService.success(null, this.translateService.get(successMessageKey, { 'version': versionNumber }));
|
|
||||||
} else {
|
|
||||||
this.notificationsService.error(null, this.translateService.get(failureMessageKey, { 'version': versionNumber }));
|
|
||||||
}
|
|
||||||
if (redirectToLatest) {
|
|
||||||
const path = getItemEditVersionhistoryRoute(newLatestVersionItem);
|
|
||||||
this.router.navigateByUrl(path);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new version starting from the specified one
|
|
||||||
* @param version the version from which a new one will be created
|
|
||||||
*/
|
|
||||||
createNewVersion(version: Version) {
|
|
||||||
const versionNumber = version.version;
|
|
||||||
|
|
||||||
// Open modal and set current version number
|
|
||||||
const activeModal = this.modalService.open(ItemVersionsSummaryModalComponent);
|
|
||||||
activeModal.componentInstance.versionNumber = versionNumber;
|
|
||||||
|
|
||||||
// On createVersionEvent emitted create new version and notify
|
|
||||||
activeModal.componentInstance.createVersionEvent.pipe(
|
|
||||||
mergeMap((summary: string) => combineLatest([
|
|
||||||
of(summary),
|
|
||||||
version.item.pipe(getFirstSucceededRemoteDataPayload()),
|
|
||||||
])),
|
|
||||||
mergeMap(([summary, item]: [string, Item]) => this.versionHistoryService.createVersion(item._links.self.href, summary)),
|
|
||||||
getFirstCompletedRemoteData(),
|
|
||||||
// close model (should be displaying loading/waiting indicator) when version creation failed/succeeded
|
|
||||||
tap(() => activeModal.close()),
|
|
||||||
// show success/failure notification
|
|
||||||
tap((newVersionRD: RemoteData<Version>) => {
|
|
||||||
this.itemVersionShared.notifyCreateNewVersion(newVersionRD);
|
|
||||||
if (newVersionRD.hasSucceeded) {
|
|
||||||
const versionHistory$ = this.versionService.getHistoryFromVersion(version).pipe(
|
|
||||||
tap((versionHistory: VersionHistory) => {
|
|
||||||
this.itemService.invalidateItemCache(this.item.uuid);
|
|
||||||
this.versionHistoryService.invalidateVersionHistoryCache(versionHistory.id);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
this.getAllVersions(versionHistory$);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
// get workspace item
|
|
||||||
getFirstSucceededRemoteDataPayload<Version>(),
|
|
||||||
switchMap((newVersion: Version) => this.itemService.findByHref(newVersion._links.item.href)),
|
|
||||||
getFirstSucceededRemoteDataPayload<Item>(),
|
|
||||||
switchMap((newVersionItem: Item) => this.workspaceItemDataService.findByItem(newVersionItem.uuid, true, false)),
|
|
||||||
getFirstSucceededRemoteDataPayload<WorkspaceItem>(),
|
|
||||||
).subscribe((wsItem) => {
|
|
||||||
const wsiId = wsItem.id;
|
|
||||||
const route = 'workspaceitems/' + wsiId + '/edit';
|
|
||||||
this.router.navigateByUrl(route);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check is the current user can edit the version summary
|
* Check is the current user can edit the version summary
|
||||||
* @param version
|
* @param version
|
||||||
@@ -444,14 +299,6 @@ export class ItemVersionsComponent implements OnDestroy, OnInit {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the current user can delete the version
|
|
||||||
* @param version
|
|
||||||
*/
|
|
||||||
canDeleteVersion$(version: Version): Observable<boolean> {
|
|
||||||
return this.authorizationService.isAuthorized(FeatureID.CanDeleteVersion, version.self);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all versions for the given version history and store them in versionRD$
|
* Get all versions for the given version history and store them in versionRD$
|
||||||
* @param versionHistory$
|
* @param versionHistory$
|
||||||
@@ -477,44 +324,6 @@ export class ItemVersionsComponent implements OnDestroy, OnInit {
|
|||||||
this.getAllVersions(this.versionHistory$);
|
this.getAllVersions(this.versionHistory$);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the ID of the workspace item, if present, otherwise return undefined
|
|
||||||
* @param versionItem the item for which retrieve the workspace item id
|
|
||||||
*/
|
|
||||||
getWorkspaceId(versionItem): Observable<string> {
|
|
||||||
return versionItem.pipe(
|
|
||||||
getFirstSucceededRemoteDataPayload(),
|
|
||||||
map((item: Item) => item.uuid),
|
|
||||||
switchMap((itemUuid: string) => this.workspaceItemDataService.findByItem(itemUuid, true)),
|
|
||||||
getFirstCompletedRemoteData<WorkspaceItem>(),
|
|
||||||
map((res: RemoteData<WorkspaceItem>) => res?.payload?.id ),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the ID of the workflow item, if present, otherwise return undefined
|
|
||||||
* @param versionItem the item for which retrieve the workspace item id
|
|
||||||
*/
|
|
||||||
getWorkflowId(versionItem): Observable<string> {
|
|
||||||
return versionItem.pipe(
|
|
||||||
getFirstSucceededRemoteDataPayload(),
|
|
||||||
map((item: Item) => item.uuid),
|
|
||||||
switchMap((itemUuid: string) => this.workflowItemDataService.findByItem(itemUuid, true)),
|
|
||||||
getFirstCompletedRemoteData<WorkspaceItem>(),
|
|
||||||
map((res: RemoteData<WorkspaceItem>) => res?.payload?.id ),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* redirect to the edit page of the workspace item
|
|
||||||
* @param id$ the id of the workspace item
|
|
||||||
*/
|
|
||||||
editWorkspaceItem(id$: Observable<string>) {
|
|
||||||
id$.subscribe((id) => {
|
|
||||||
this.router.navigateByUrl('workspaceitems/' + id + '/edit');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize all observables
|
* Initialize all observables
|
||||||
*/
|
*/
|
||||||
@@ -532,19 +341,12 @@ export class ItemVersionsComponent implements OnDestroy, OnInit {
|
|||||||
hasValueOperator(),
|
hasValueOperator(),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.canCreateVersion$ = this.authorizationService.isAuthorized(FeatureID.CanCreateVersion, this.item.self);
|
|
||||||
|
|
||||||
// If there is a draft item in the version history the 'Create version' button is disabled and a different tooltip message is shown
|
// If there is a draft item in the version history the 'Create version' button is disabled and a different tooltip message is shown
|
||||||
this.hasDraftVersion$ = this.versionHistoryRD$.pipe(
|
this.hasDraftVersion$ = this.versionHistoryRD$.pipe(
|
||||||
getFirstSucceededRemoteDataPayload(),
|
getFirstSucceededRemoteDataPayload(),
|
||||||
map((res) => Boolean(res?.draftVersion)),
|
map((res) => Boolean(res?.draftVersion)),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.createVersionTitle$ = this.hasDraftVersion$.pipe(
|
|
||||||
take(1),
|
|
||||||
switchMap((res) => of(res ? 'item.version.history.table.action.hasDraft' : 'item.version.history.table.action.newVersion')),
|
|
||||||
);
|
|
||||||
|
|
||||||
this.getAllVersions(this.versionHistory$);
|
this.getAllVersions(this.versionHistory$);
|
||||||
this.hasEpersons$ = this.versionsRD$.pipe(
|
this.hasEpersons$ = this.versionsRD$.pipe(
|
||||||
getAllSucceededRemoteData(),
|
getAllSucceededRemoteData(),
|
||||||
|
@@ -38,6 +38,7 @@ import {
|
|||||||
import { WorkspaceItem } from '../core/submission/models/workspaceitem.model';
|
import { WorkspaceItem } from '../core/submission/models/workspaceitem.model';
|
||||||
import { WorkspaceitemDataService } from '../core/submission/workspaceitem-data.service';
|
import { WorkspaceitemDataService } from '../core/submission/workspaceitem-data.service';
|
||||||
import {
|
import {
|
||||||
|
hasNoValue,
|
||||||
hasValue,
|
hasValue,
|
||||||
isNotEmpty,
|
isNotEmpty,
|
||||||
} from '../shared/empty.util';
|
} from '../shared/empty.util';
|
||||||
@@ -165,6 +166,9 @@ export class SuggestionsService {
|
|||||||
* The EPerson id for which to retrieve suggestion targets
|
* The EPerson id for which to retrieve suggestion targets
|
||||||
*/
|
*/
|
||||||
public retrieveCurrentUserSuggestions(userUuid: string): Observable<SuggestionTarget[]> {
|
public retrieveCurrentUserSuggestions(userUuid: string): Observable<SuggestionTarget[]> {
|
||||||
|
if (hasNoValue(userUuid)) {
|
||||||
|
return of([]);
|
||||||
|
}
|
||||||
return this.researcherProfileService.findById(userUuid, true).pipe(
|
return this.researcherProfileService.findById(userUuid, true).pipe(
|
||||||
getFirstCompletedRemoteData(),
|
getFirstCompletedRemoteData(),
|
||||||
mergeMap((profile: RemoteData<ResearcherProfile> ) => {
|
mergeMap((profile: RemoteData<ResearcherProfile> ) => {
|
||||||
|
@@ -47,7 +47,7 @@
|
|||||||
(showNotification)="showNotification($event)"></ds-google-recaptcha>
|
(showNotification)="showNotification($event)"></ds-google-recaptcha>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ng-container *ngIf="((googleRecaptchaService.captchaVersion() | async) !== 'v2' && (googleRecaptchaService.captchaMode() | async) === 'invisible'); else v2Invisible">
|
<ng-container *ngIf="(!registrationVerification || ((googleRecaptchaService.captchaVersion() | async) !== 'v2' && (googleRecaptchaService.captchaMode() | async) === 'invisible')); else v2Invisible">
|
||||||
<button class="btn btn-primary" [disabled]="form.invalid || registrationVerification && !isRecaptchaCookieAccepted() || disableUntilChecked" (click)="register()">
|
<button class="btn btn-primary" [disabled]="form.invalid || registrationVerification && !isRecaptchaCookieAccepted() || disableUntilChecked" (click)="register()">
|
||||||
{{ MESSAGE_PREFIX + '.submit' | translate }}
|
{{ MESSAGE_PREFIX + '.submit' | translate }}
|
||||||
</button>
|
</button>
|
||||||
|
@@ -16,7 +16,6 @@ import { ConfigurationSearchPageComponent } from './configuration-search-page.co
|
|||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-configuration-search-page',
|
selector: 'ds-configuration-search-page',
|
||||||
styleUrls: [],
|
|
||||||
templateUrl: '../shared/theme-support/themed.component.html',
|
templateUrl: '../shared/theme-support/themed.component.html',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [ConfigurationSearchPageComponent],
|
imports: [ConfigurationSearchPageComponent],
|
||||||
@@ -37,7 +36,7 @@ export class ThemedConfigurationSearchPageComponent extends ThemedComponent<Conf
|
|||||||
* The configuration to use for the search options
|
* The configuration to use for the search options
|
||||||
* If empty, 'default' is used
|
* If empty, 'default' is used
|
||||||
*/
|
*/
|
||||||
@Input() configuration;
|
@Input() configuration: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The actual query for the fixed filter.
|
* The actual query for the fixed filter.
|
||||||
@@ -49,12 +48,12 @@ export class ThemedConfigurationSearchPageComponent extends ThemedComponent<Conf
|
|||||||
* If this is true, the request will only be sent if there's
|
* If this is true, the request will only be sent if there's
|
||||||
* no valid cached version. Defaults to true
|
* no valid cached version. Defaults to true
|
||||||
*/
|
*/
|
||||||
@Input() useCachedVersionIfAvailable;
|
@Input() useCachedVersionIfAvailable: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* True when the search component should show results on the current page
|
* True when the search component should show results on the current page
|
||||||
*/
|
*/
|
||||||
@Input() inPlaceSearch;
|
@Input() inPlaceSearch: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The link type of the listed search results
|
* The link type of the listed search results
|
||||||
@@ -64,27 +63,27 @@ export class ThemedConfigurationSearchPageComponent extends ThemedComponent<Conf
|
|||||||
/**
|
/**
|
||||||
* The pagination id used in the search
|
* The pagination id used in the search
|
||||||
*/
|
*/
|
||||||
@Input() paginationId;
|
@Input() paginationId: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether or not the search bar should be visible
|
* Whether or not the search bar should be visible
|
||||||
*/
|
*/
|
||||||
@Input() searchEnabled;
|
@Input() searchEnabled: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The width of the sidebar (bootstrap columns)
|
* The width of the sidebar (bootstrap columns)
|
||||||
*/
|
*/
|
||||||
@Input() sideBarWidth;
|
@Input() sideBarWidth: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The placeholder of the search form input
|
* The placeholder of the search form input
|
||||||
*/
|
*/
|
||||||
@Input() searchFormPlaceholder;
|
@Input() searchFormPlaceholder: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A boolean representing if result entries are selectable
|
* A boolean representing if result entries are selectable
|
||||||
*/
|
*/
|
||||||
@Input() selectable;
|
@Input() selectable: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The config option used for selection functionality
|
* The config option used for selection functionality
|
||||||
@@ -94,22 +93,22 @@ export class ThemedConfigurationSearchPageComponent extends ThemedComponent<Conf
|
|||||||
/**
|
/**
|
||||||
* A boolean representing if show csv export button
|
* A boolean representing if show csv export button
|
||||||
*/
|
*/
|
||||||
@Input() showCsvExport;
|
@Input() showCsvExport: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A boolean representing if show search sidebar button
|
* A boolean representing if show search sidebar button
|
||||||
*/
|
*/
|
||||||
@Input() showSidebar;
|
@Input() showSidebar: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether to show the thumbnail preview
|
* Whether to show the thumbnail preview
|
||||||
*/
|
*/
|
||||||
@Input() showThumbnails;
|
@Input() showThumbnails: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether to show the view mode switch
|
* Whether to show the view mode switch
|
||||||
*/
|
*/
|
||||||
@Input() showViewModes;
|
@Input() showViewModes: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of available view mode
|
* List of available view mode
|
||||||
@@ -124,12 +123,12 @@ export class ThemedConfigurationSearchPageComponent extends ThemedComponent<Conf
|
|||||||
/**
|
/**
|
||||||
* Defines whether or not to show the scope selector
|
* Defines whether or not to show the scope selector
|
||||||
*/
|
*/
|
||||||
@Input() showScopeSelector;
|
@Input() showScopeSelector: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether or not to track search statistics by sending updates to the rest api
|
* Whether or not to track search statistics by sending updates to the rest api
|
||||||
*/
|
*/
|
||||||
@Input() trackStatistics;
|
@Input() trackStatistics: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The default value for the search query when none is already defined in the {@link SearchConfigurationService}
|
* The default value for the search query when none is already defined in the {@link SearchConfigurationService}
|
||||||
@@ -146,8 +145,32 @@ export class ThemedConfigurationSearchPageComponent extends ThemedComponent<Conf
|
|||||||
*/
|
*/
|
||||||
@Input() hideScopeInUrl: boolean;
|
@Input() hideScopeInUrl: boolean;
|
||||||
|
|
||||||
protected inAndOutputNames: (keyof ConfigurationSearchPageComponent & keyof this)[] =
|
protected inAndOutputNames: (keyof ConfigurationSearchPageComponent & keyof this)[] = [
|
||||||
['context', 'configuration', 'fixedFilterQuery', 'inPlaceSearch', 'searchEnabled', 'sideBarWidth'];
|
'configurationList',
|
||||||
|
'context',
|
||||||
|
'configuration',
|
||||||
|
'fixedFilterQuery',
|
||||||
|
'useCachedVersionIfAvailable',
|
||||||
|
'inPlaceSearch',
|
||||||
|
'linkType',
|
||||||
|
'paginationId',
|
||||||
|
'searchEnabled',
|
||||||
|
'sideBarWidth',
|
||||||
|
'searchFormPlaceholder',
|
||||||
|
'selectable',
|
||||||
|
'selectionConfig',
|
||||||
|
'showCsvExport',
|
||||||
|
'showSidebar',
|
||||||
|
'showThumbnails',
|
||||||
|
'showViewModes',
|
||||||
|
'useUniquePageId',
|
||||||
|
'viewModeList',
|
||||||
|
'showScopeSelector',
|
||||||
|
'trackStatistics',
|
||||||
|
'query',
|
||||||
|
'scope',
|
||||||
|
'hideScopeInUrl',
|
||||||
|
];
|
||||||
|
|
||||||
protected getComponentName(): string {
|
protected getComponentName(): string {
|
||||||
return 'ConfigurationSearchPageComponent';
|
return 'ConfigurationSearchPageComponent';
|
||||||
|
@@ -16,7 +16,7 @@
|
|||||||
<ds-loading *ngIf="groupRD$ | async | dsHasNoValue"></ds-loading>
|
<ds-loading *ngIf="groupRD$ | async | dsHasNoValue"></ds-loading>
|
||||||
<div *ngIf="hasNoGroup$ | async">
|
<div *ngIf="hasNoGroup$ | async">
|
||||||
{{'comcol-role.edit.no-group' | translate}}
|
{{'comcol-role.edit.no-group' | translate}}
|
||||||
</div>V
|
</div>
|
||||||
<div *ngIf="hasAnonymousGroup$ | async">
|
<div *ngIf="hasAnonymousGroup$ | async">
|
||||||
{{'comcol-role.edit.' + (comcolRole$ | async)?.name + '.anonymous-group' | translate}}
|
{{'comcol-role.edit.' + (comcolRole$ | async)?.name + '.anonymous-group' | translate}}
|
||||||
</div>
|
</div>
|
||||||
|
@@ -17,7 +17,10 @@ import { cold } from 'jasmine-marbles';
|
|||||||
import uniqueId from 'lodash/uniqueId';
|
import uniqueId from 'lodash/uniqueId';
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
import { APP_DATA_SERVICES_MAP } from '../../../config/app-config.interface';
|
import {
|
||||||
|
APP_DATA_SERVICES_MAP,
|
||||||
|
LazyDataServicesMap,
|
||||||
|
} from '../../../config/app-config.interface';
|
||||||
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||||
import { buildPaginatedList } from '../../core/data/paginated-list.model';
|
import { buildPaginatedList } from '../../core/data/paginated-list.model';
|
||||||
import { RequestService } from '../../core/data/request.service';
|
import { RequestService } from '../../core/data/request.service';
|
||||||
@@ -41,10 +44,10 @@ import { SearchEvent } from './eperson-group-list-event-type';
|
|||||||
import { EpersonSearchBoxComponent } from './eperson-search-box/eperson-search-box.component';
|
import { EpersonSearchBoxComponent } from './eperson-search-box/eperson-search-box.component';
|
||||||
import { GroupSearchBoxComponent } from './group-search-box/group-search-box.component';
|
import { GroupSearchBoxComponent } from './group-search-box/group-search-box.component';
|
||||||
|
|
||||||
const mockDataServiceMap: any = {
|
const mockDataServiceMap: LazyDataServicesMap = new Map([
|
||||||
[EPERSON.value]: () => import('../../core/eperson/eperson-data.service').then(m => m.EPersonDataService),
|
[EPERSON.value, () => import('../../core/eperson/eperson-data.service').then(m => m.EPersonDataService)],
|
||||||
[GROUP.value]: () => import('../../core/eperson/group-data.service').then(m => m.GroupDataService),
|
[GROUP.value, () => import('../../core/eperson/group-data.service').then(m => m.GroupDataService)],
|
||||||
};
|
]);
|
||||||
|
|
||||||
describe('EpersonGroupListComponent test suite', () => {
|
describe('EpersonGroupListComponent test suite', () => {
|
||||||
let comp: EpersonGroupListComponent;
|
let comp: EpersonGroupListComponent;
|
||||||
|
@@ -7,7 +7,6 @@ import {
|
|||||||
Component,
|
Component,
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
Inject,
|
Inject,
|
||||||
InjectionToken,
|
|
||||||
Injector,
|
Injector,
|
||||||
Input,
|
Input,
|
||||||
OnDestroy,
|
OnDestroy,
|
||||||
@@ -35,7 +34,7 @@ import { EPersonDataService } from '../../core/eperson/eperson-data.service';
|
|||||||
import { GroupDataService } from '../../core/eperson/group-data.service';
|
import { GroupDataService } from '../../core/eperson/group-data.service';
|
||||||
import { EPERSON } from '../../core/eperson/models/eperson.resource-type';
|
import { EPERSON } from '../../core/eperson/models/eperson.resource-type';
|
||||||
import { GROUP } from '../../core/eperson/models/group.resource-type';
|
import { GROUP } from '../../core/eperson/models/group.resource-type';
|
||||||
import { lazyService } from '../../core/lazy-service';
|
import { lazyDataService } from '../../core/lazy-data-service';
|
||||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||||
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||||
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
|
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
|
||||||
@@ -133,7 +132,7 @@ export class EpersonGroupListComponent implements OnInit, OnDestroy {
|
|||||||
constructor(public dsoNameService: DSONameService,
|
constructor(public dsoNameService: DSONameService,
|
||||||
private parentInjector: Injector,
|
private parentInjector: Injector,
|
||||||
private paginationService: PaginationService,
|
private paginationService: PaginationService,
|
||||||
@Inject(APP_DATA_SERVICES_MAP) private dataServiceMap: InjectionToken<LazyDataServicesMap>) {
|
@Inject(APP_DATA_SERVICES_MAP) private dataServiceMap: LazyDataServicesMap) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -141,7 +140,7 @@ export class EpersonGroupListComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
const resourceType: ResourceType = (this.isListOfEPerson) ? EPERSON : GROUP;
|
const resourceType: ResourceType = (this.isListOfEPerson) ? EPERSON : GROUP;
|
||||||
const lazyProvider$: Observable<EPersonDataService | GroupDataService> = lazyService(this.dataServiceMap[resourceType.value], this.parentInjector);
|
const lazyProvider$: Observable<EPersonDataService | GroupDataService> = lazyDataService(this.dataServiceMap, resourceType.value, this.parentInjector);
|
||||||
lazyProvider$.subscribe((dataService: EPersonDataService | GroupDataService) => {
|
lazyProvider$.subscribe((dataService: EPersonDataService | GroupDataService) => {
|
||||||
this.dataService = dataService;
|
this.dataService = dataService;
|
||||||
console.log(dataService);
|
console.log(dataService);
|
||||||
|
@@ -12,7 +12,6 @@ import {
|
|||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { provideMockStore } from '@ngrx/store/testing';
|
import { provideMockStore } from '@ngrx/store/testing';
|
||||||
import { REQUEST } from '@nguniversal/express-engine/tokens';
|
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { of } from 'rxjs';
|
import { of } from 'rxjs';
|
||||||
import { AuthRequestService } from 'src/app/core/auth/auth-request.service';
|
import { AuthRequestService } from 'src/app/core/auth/auth-request.service';
|
||||||
@@ -27,6 +26,7 @@ import {
|
|||||||
APP_CONFIG,
|
APP_CONFIG,
|
||||||
APP_DATA_SERVICES_MAP,
|
APP_DATA_SERVICES_MAP,
|
||||||
} from '../../../../../config/app-config.interface';
|
} from '../../../../../config/app-config.interface';
|
||||||
|
import { REQUEST } from '../../../../../express.tokens';
|
||||||
import { Context } from '../../../../core/shared/context.model';
|
import { Context } from '../../../../core/shared/context.model';
|
||||||
import { GenericConstructor } from '../../../../core/shared/generic-constructor';
|
import { GenericConstructor } from '../../../../core/shared/generic-constructor';
|
||||||
import { ListableModule } from '../../../../core/shared/listable.module';
|
import { ListableModule } from '../../../../core/shared/listable.module';
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
inject,
|
inject,
|
||||||
InjectionToken,
|
|
||||||
Injector,
|
Injector,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import {
|
import {
|
||||||
@@ -12,10 +11,13 @@ import {
|
|||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { switchMap } from 'rxjs/operators';
|
import { switchMap } from 'rxjs/operators';
|
||||||
|
|
||||||
import { LazyDataServicesMap } from '../../../../config/app-config.interface';
|
import {
|
||||||
|
APP_DATA_SERVICES_MAP,
|
||||||
|
LazyDataServicesMap,
|
||||||
|
} from '../../../../config/app-config.interface';
|
||||||
import { IdentifiableDataService } from '../../../core/data/base/identifiable-data.service';
|
import { IdentifiableDataService } from '../../../core/data/base/identifiable-data.service';
|
||||||
import { RemoteData } from '../../../core/data/remote-data';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
import { lazyService } from '../../../core/lazy-service';
|
import { lazyDataService } from '../../../core/lazy-data-service';
|
||||||
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||||
import { getFirstCompletedRemoteData } from '../../../core/shared/operators';
|
import { getFirstCompletedRemoteData } from '../../../core/shared/operators';
|
||||||
import { ResourceType } from '../../../core/shared/resource-type';
|
import { ResourceType } from '../../../core/shared/resource-type';
|
||||||
@@ -34,7 +36,7 @@ import { isEmpty } from '../../empty.util';
|
|||||||
export const resourcePolicyTargetResolver: ResolveFn<RemoteData<DSpaceObject>> = (
|
export const resourcePolicyTargetResolver: ResolveFn<RemoteData<DSpaceObject>> = (
|
||||||
route: ActivatedRouteSnapshot,
|
route: ActivatedRouteSnapshot,
|
||||||
state: RouterStateSnapshot,
|
state: RouterStateSnapshot,
|
||||||
dataServiceMap: InjectionToken<LazyDataServicesMap> = inject(InjectionToken<LazyDataServicesMap>),
|
dataServiceMap: LazyDataServicesMap = inject(APP_DATA_SERVICES_MAP),
|
||||||
parentInjector: Injector = inject(Injector),
|
parentInjector: Injector = inject(Injector),
|
||||||
router: Router = inject(Router),
|
router: Router = inject(Router),
|
||||||
): Observable<RemoteData<DSpaceObject>> => {
|
): Observable<RemoteData<DSpaceObject>> => {
|
||||||
@@ -46,7 +48,7 @@ export const resourcePolicyTargetResolver: ResolveFn<RemoteData<DSpaceObject>> =
|
|||||||
}
|
}
|
||||||
|
|
||||||
const resourceType: ResourceType = new ResourceType(targetType);
|
const resourceType: ResourceType = new ResourceType(targetType);
|
||||||
const lazyProvider$: Observable<IdentifiableDataService<DSpaceObject>> = lazyService(dataServiceMap[resourceType.value], parentInjector);
|
const lazyProvider$: Observable<IdentifiableDataService<DSpaceObject>> = lazyDataService(dataServiceMap, resourceType.value, parentInjector);
|
||||||
|
|
||||||
return lazyProvider$.pipe(
|
return lazyProvider$.pipe(
|
||||||
switchMap((dataService: IdentifiableDataService<DSpaceObject>) => {
|
switchMap((dataService: IdentifiableDataService<DSpaceObject>) => {
|
||||||
|
@@ -7,4 +7,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<ds-advanced-search *ngIf="appConfig.search.advancedFilters.enabled"
|
<ds-advanced-search *ngIf="appConfig.search.advancedFilters.enabled"
|
||||||
[inPlaceSearch]="inPlaceSearch"></ds-advanced-search>
|
[inPlaceSearch]="inPlaceSearch"></ds-advanced-search>
|
||||||
<a class="btn btn-primary" [routerLink]="[searchLink]" [queryParams]="clearParams | async" queryParamsHandling="merge" role="button"><i class="fas fa-undo"></i> {{"search.filters.reset" | translate}}</a>
|
<a *ngIf="inPlaceSearch" class="btn btn-primary" [routerLink]="[searchLink]" [queryParams]="clearParams | async" queryParamsHandling="merge" role="button">
|
||||||
|
<i class="fas fa-undo"></i> {{"search.filters.reset" | translate}}
|
||||||
|
</a>
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
<div class="container" *ngIf="(isXsOrSm$ | async)">
|
<div class="container" *ngIf="searchEnabled && (isXsOrSm$ | async)">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<ng-template *ngTemplateOutlet="searchForm"></ng-template>
|
<ng-template *ngTemplateOutlet="searchForm"></ng-template>
|
||||||
@@ -18,7 +18,9 @@
|
|||||||
<ng-template #searchContent>
|
<ng-template #searchContent>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12" *ngIf="(isXsOrSm$ | async) !== true">
|
<div class="col-12" *ngIf="(isXsOrSm$ | async) !== true">
|
||||||
<ng-template *ngTemplateOutlet="searchForm"></ng-template>
|
<ng-container *ngIf="searchEnabled">
|
||||||
|
<ng-template *ngTemplateOutlet="searchForm"></ng-template>
|
||||||
|
</ng-container>
|
||||||
<ng-content select="[additionalSearchOptions]"></ng-content>
|
<ng-content select="[additionalSearchOptions]"></ng-content>
|
||||||
</div>
|
</div>
|
||||||
<div id="search-content" class="col-12">
|
<div id="search-content" class="col-12">
|
||||||
@@ -84,7 +86,7 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ng-template #searchForm>
|
<ng-template #searchForm>
|
||||||
<ds-search-form *ngIf="searchEnabled" id="search-form"
|
<ds-search-form id="search-form"
|
||||||
[query]="(searchOptions$ | async)?.query"
|
[query]="(searchOptions$ | async)?.query"
|
||||||
[scope]="(searchOptions$ | async)?.scope"
|
[scope]="(searchOptions$ | async)?.scope"
|
||||||
[hideScopeInUrl]="hideScopeInUrl"
|
[hideScopeInUrl]="hideScopeInUrl"
|
||||||
@@ -95,7 +97,7 @@
|
|||||||
</ds-search-form>
|
</ds-search-form>
|
||||||
<div class="row mb-3 mb-md-1">
|
<div class="row mb-3 mb-md-1">
|
||||||
<div class="labels col-sm-9">
|
<div class="labels col-sm-9">
|
||||||
<ds-search-labels *ngIf="searchEnabled" [inPlaceSearch]="inPlaceSearch"></ds-search-labels>
|
<ds-search-labels [inPlaceSearch]="inPlaceSearch"></ds-search-labels>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
@@ -2,12 +2,13 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="row-with-sidebar row-offcanvas row-offcanvas-left"
|
<div class="row-with-sidebar row-offcanvas row-offcanvas-left"
|
||||||
[@pushInOut]="(isSidebarCollapsed$ | async) ? 'collapsed' : 'expanded'">
|
[@pushInOut]="(isSidebarCollapsed$ | async) ? 'collapsed' : 'expanded'">
|
||||||
<div id="{{id}}-sidebar-content"
|
<div *ngIf="sideBarWidth > 0" id="{{id}}-sidebar-content"
|
||||||
[class.invisible]="(isSidebarCollapsed$ | async) === true && (isXsOrSm$ | async) === true"
|
[class.invisible]="(isSidebarCollapsed$ | async) === true && (isXsOrSm$ | async) === true"
|
||||||
class="col-12 col-md-{{sideBarWidth}} sidebar-content {{sidebarClasses | async}}">
|
class="col-12 col-md-{{sideBarWidth}} sidebar-content {{sidebarClasses | async}}">
|
||||||
<ng-container *ngTemplateOutlet="sidebarContent"></ng-container>
|
<ng-container *ngTemplateOutlet="sidebarContent"></ng-container>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-{{12 - sideBarWidth}}"
|
<div class="col-12 col-md-{{12 - sideBarWidth}}"
|
||||||
|
[class.px-0]="sideBarWidth === 0"
|
||||||
[class.invisible]="(isSidebarCollapsed$ | async) !== true && (isXsOrSm$ | async) === true">
|
[class.invisible]="(isSidebarCollapsed$ | async) !== true && (isXsOrSm$ | async) === true">
|
||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
AsyncPipe,
|
AsyncPipe,
|
||||||
|
NgIf,
|
||||||
NgTemplateOutlet,
|
NgTemplateOutlet,
|
||||||
} from '@angular/common';
|
} from '@angular/common';
|
||||||
import {
|
import {
|
||||||
@@ -21,7 +22,11 @@ import { SidebarService } from './sidebar.service';
|
|||||||
templateUrl: './page-with-sidebar.component.html',
|
templateUrl: './page-with-sidebar.component.html',
|
||||||
animations: [pushInOut],
|
animations: [pushInOut],
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [NgTemplateOutlet, AsyncPipe],
|
imports: [
|
||||||
|
AsyncPipe,
|
||||||
|
NgTemplateOutlet,
|
||||||
|
NgIf,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
/**
|
/**
|
||||||
* This component takes care of displaying the sidebar properly on all viewports. It does not
|
* This component takes care of displaying the sidebar properly on all viewports. It does not
|
||||||
|
48
src/app/shared/utils/browser-only.directive.ts
Normal file
48
src/app/shared/utils/browser-only.directive.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { isPlatformBrowser } from '@angular/common';
|
||||||
|
import {
|
||||||
|
ChangeDetectorRef,
|
||||||
|
Directive,
|
||||||
|
Inject,
|
||||||
|
OnInit,
|
||||||
|
PLATFORM_ID,
|
||||||
|
TemplateRef,
|
||||||
|
ViewContainerRef,
|
||||||
|
} from '@angular/core';
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
selector: '[dsRenderOnlyForBrowser]',
|
||||||
|
standalone: true,
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Structural Directive for rendering a template reference on client side only
|
||||||
|
*/
|
||||||
|
export class BrowserOnlyDirective implements OnInit {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(PLATFORM_ID) protected platformId: string,
|
||||||
|
private viewContainer: ViewContainerRef,
|
||||||
|
private changeDetector: ChangeDetectorRef,
|
||||||
|
private templateRef: TemplateRef<any>,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.showTemplateBlockInView();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show template in view container according to platform
|
||||||
|
*/
|
||||||
|
private showTemplateBlockInView(): void {
|
||||||
|
if (!this.templateRef) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.viewContainer.clear();
|
||||||
|
|
||||||
|
if (isPlatformBrowser(this.platformId)) {
|
||||||
|
this.viewContainer.createEmbeddedView(this.templateRef);
|
||||||
|
this.changeDetector.markForCheck();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -282,7 +282,7 @@ describe('SubmissionSectionFormComponent test suite', () => {
|
|||||||
expect(comp.sectionData.errorsToShow).toEqual([]);
|
expect(comp.sectionData.errorsToShow).toEqual([]);
|
||||||
expect(comp.sectionData.data).toEqual(sectionData);
|
expect(comp.sectionData.data).toEqual(sectionData);
|
||||||
expect(comp.isLoading).toBeFalsy();
|
expect(comp.isLoading).toBeFalsy();
|
||||||
expect(comp.initForm).toHaveBeenCalledWith(sectionData);
|
expect(comp.initForm).toHaveBeenCalledWith(sectionData, [], []);
|
||||||
expect(comp.subscriptions).toHaveBeenCalled();
|
expect(comp.subscriptions).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -290,7 +290,7 @@ describe('SubmissionSectionFormComponent test suite', () => {
|
|||||||
formBuilderService.modelFromConfiguration.and.returnValue(testFormModel);
|
formBuilderService.modelFromConfiguration.and.returnValue(testFormModel);
|
||||||
const sectionData = {};
|
const sectionData = {};
|
||||||
|
|
||||||
comp.initForm(sectionData);
|
comp.initForm(sectionData, [], []);
|
||||||
|
|
||||||
expect(comp.formModel).toEqual(testFormModel);
|
expect(comp.formModel).toEqual(testFormModel);
|
||||||
|
|
||||||
@@ -305,7 +305,7 @@ describe('SubmissionSectionFormComponent test suite', () => {
|
|||||||
path: '/sections/' + sectionObject.id,
|
path: '/sections/' + sectionObject.id,
|
||||||
};
|
};
|
||||||
|
|
||||||
comp.initForm(sectionData);
|
comp.initForm(sectionData, [], []);
|
||||||
|
|
||||||
expect(comp.formModel).toBeUndefined();
|
expect(comp.formModel).toBeUndefined();
|
||||||
expect(sectionsServiceStub.setSectionError).toHaveBeenCalledWith(submissionId, sectionObject.id, sectionError);
|
expect(sectionsServiceStub.setSectionError).toHaveBeenCalledWith(submissionId, sectionObject.id, sectionError);
|
||||||
@@ -464,7 +464,7 @@ describe('SubmissionSectionFormComponent test suite', () => {
|
|||||||
compAsAny.formData = {};
|
compAsAny.formData = {};
|
||||||
compAsAny.sectionMetadata = ['dc.title'];
|
compAsAny.sectionMetadata = ['dc.title'];
|
||||||
|
|
||||||
comp.updateForm(sectionData, sectionError);
|
comp.updateForm({ data: sectionData, errorsToShow: sectionError } as any);
|
||||||
|
|
||||||
expect(comp.isUpdating).toBeFalsy();
|
expect(comp.isUpdating).toBeFalsy();
|
||||||
expect(comp.initForm).toHaveBeenCalled();
|
expect(comp.initForm).toHaveBeenCalled();
|
||||||
@@ -476,15 +476,19 @@ describe('SubmissionSectionFormComponent test suite', () => {
|
|||||||
it('should update form error properly', () => {
|
it('should update form error properly', () => {
|
||||||
spyOn(comp, 'initForm');
|
spyOn(comp, 'initForm');
|
||||||
spyOn(comp, 'checksForErrors');
|
spyOn(comp, 'checksForErrors');
|
||||||
const sectionData: any = {
|
const sectionData = {
|
||||||
'dc.title': [new FormFieldMetadataValueObject('test')],
|
'dc.title': [new FormFieldMetadataValueObject('test')],
|
||||||
};
|
};
|
||||||
|
const sectionState = {
|
||||||
|
data: sectionData,
|
||||||
|
errorsToShow: [{ path: '/test', message: 'test' }],
|
||||||
|
} as any;
|
||||||
comp.sectionData.data = {};
|
comp.sectionData.data = {};
|
||||||
comp.sectionData.errorsToShow = [];
|
comp.sectionData.errorsToShow = [];
|
||||||
compAsAny.formData = sectionData;
|
compAsAny.formData = sectionData;
|
||||||
compAsAny.sectionMetadata = ['dc.title'];
|
compAsAny.sectionMetadata = ['dc.title'];
|
||||||
|
|
||||||
comp.updateForm(sectionData, parsedSectionErrors);
|
comp.updateForm(sectionState);
|
||||||
|
|
||||||
expect(comp.initForm).not.toHaveBeenCalled();
|
expect(comp.initForm).not.toHaveBeenCalled();
|
||||||
expect(comp.checksForErrors).toHaveBeenCalled();
|
expect(comp.checksForErrors).toHaveBeenCalled();
|
||||||
@@ -495,8 +499,9 @@ describe('SubmissionSectionFormComponent test suite', () => {
|
|||||||
spyOn(comp, 'initForm');
|
spyOn(comp, 'initForm');
|
||||||
spyOn(comp, 'checksForErrors');
|
spyOn(comp, 'checksForErrors');
|
||||||
const sectionData: any = {};
|
const sectionData: any = {};
|
||||||
|
const sectionErrors: any = [{ path: '/test', message: 'test' }];
|
||||||
|
|
||||||
comp.updateForm(sectionData, parsedSectionErrors);
|
comp.updateForm({ data: sectionData, errorsToShow: sectionErrors } as any);
|
||||||
|
|
||||||
expect(comp.initForm).not.toHaveBeenCalled();
|
expect(comp.initForm).not.toHaveBeenCalled();
|
||||||
expect(comp.checksForErrors).toHaveBeenCalled();
|
expect(comp.checksForErrors).toHaveBeenCalled();
|
||||||
@@ -562,7 +567,7 @@ describe('SubmissionSectionFormComponent test suite', () => {
|
|||||||
const sectionState = {
|
const sectionState = {
|
||||||
data: sectionData,
|
data: sectionData,
|
||||||
errorsToShow: parsedSectionErrors,
|
errorsToShow: parsedSectionErrors,
|
||||||
};
|
} as any;
|
||||||
|
|
||||||
formService.getFormData.and.returnValue(observableOf(formData));
|
formService.getFormData.and.returnValue(observableOf(formData));
|
||||||
sectionsServiceStub.getSectionState.and.returnValue(observableOf(sectionState));
|
sectionsServiceStub.getSectionState.and.returnValue(observableOf(sectionState));
|
||||||
@@ -571,7 +576,7 @@ describe('SubmissionSectionFormComponent test suite', () => {
|
|||||||
|
|
||||||
expect(compAsAny.subs.length).toBe(2);
|
expect(compAsAny.subs.length).toBe(2);
|
||||||
expect(compAsAny.formData).toEqual(formData);
|
expect(compAsAny.formData).toEqual(formData);
|
||||||
expect(comp.updateForm).toHaveBeenCalledWith(sectionState.data, sectionState.errorsToShow);
|
expect(comp.updateForm).toHaveBeenCalledWith(sectionState);
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -224,7 +224,7 @@ export class SubmissionSectionFormComponent extends SectionModelComponent {
|
|||||||
this.submissionObject = submissionObject;
|
this.submissionObject = submissionObject;
|
||||||
this.isSectionReadonly = isSectionReadOnly;
|
this.isSectionReadonly = isSectionReadOnly;
|
||||||
// Is the first loading so init form
|
// Is the first loading so init form
|
||||||
this.initForm(sectionData);
|
this.initForm(sectionData, this.sectionData.errorsToShow, this.sectionData.serverValidationErrors);
|
||||||
this.sectionData.data = sectionData;
|
this.sectionData.data = sectionData;
|
||||||
this.subscriptions();
|
this.subscriptions();
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
@@ -328,7 +328,7 @@ export class SubmissionSectionFormComponent extends SectionModelComponent {
|
|||||||
* @param sectionData
|
* @param sectionData
|
||||||
* the section data retrieved from the server
|
* the section data retrieved from the server
|
||||||
*/
|
*/
|
||||||
initForm(sectionData: WorkspaceitemSectionFormObject): void {
|
initForm(sectionData: WorkspaceitemSectionFormObject, errorsToShow: SubmissionSectionError[], serverValidationErrors: SubmissionSectionError[]): void {
|
||||||
try {
|
try {
|
||||||
this.formModel = this.formBuilderService.modelFromConfiguration(
|
this.formModel = this.formBuilderService.modelFromConfiguration(
|
||||||
this.submissionId,
|
this.submissionId,
|
||||||
@@ -339,7 +339,7 @@ export class SubmissionSectionFormComponent extends SectionModelComponent {
|
|||||||
this.isSectionReadonly,
|
this.isSectionReadonly,
|
||||||
);
|
);
|
||||||
const sectionMetadata = this.sectionService.computeSectionConfiguredMetadata(this.formConfig);
|
const sectionMetadata = this.sectionService.computeSectionConfiguredMetadata(this.formConfig);
|
||||||
this.sectionService.updateSectionData(this.submissionId, this.sectionData.id, sectionData, this.sectionData.errorsToShow, this.sectionData.serverValidationErrors, sectionMetadata);
|
this.sectionService.updateSectionData(this.submissionId, this.sectionData.id, sectionData, errorsToShow, serverValidationErrors, sectionMetadata);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const msg: string = this.translate.instant('error.submission.sections.init-form-error') + e.toString();
|
const msg: string = this.translate.instant('error.submission.sections.init-form-error') + e.toString();
|
||||||
const sectionError: SubmissionSectionError = {
|
const sectionError: SubmissionSectionError = {
|
||||||
@@ -356,12 +356,13 @@ export class SubmissionSectionFormComponent extends SectionModelComponent {
|
|||||||
/**
|
/**
|
||||||
* Update form model
|
* Update form model
|
||||||
*
|
*
|
||||||
* @param sectionData
|
* @param sectionState
|
||||||
* the section data retrieved from the server
|
* the section state retrieved from the server
|
||||||
* @param errors
|
|
||||||
* the section errors retrieved from the server
|
|
||||||
*/
|
*/
|
||||||
updateForm(sectionData: WorkspaceitemSectionFormObject, errors: SubmissionSectionError[]): void {
|
updateForm(sectionState: SubmissionSectionObject): void {
|
||||||
|
|
||||||
|
const sectionData = sectionState.data as WorkspaceitemSectionFormObject;
|
||||||
|
const errors = sectionState.errorsToShow;
|
||||||
|
|
||||||
if (isNotEmpty(sectionData) && !isEqual(sectionData, this.sectionData.data)) {
|
if (isNotEmpty(sectionData) && !isEqual(sectionData, this.sectionData.data)) {
|
||||||
this.sectionData.data = sectionData;
|
this.sectionData.data = sectionData;
|
||||||
@@ -369,7 +370,7 @@ export class SubmissionSectionFormComponent extends SectionModelComponent {
|
|||||||
this.isUpdating = true;
|
this.isUpdating = true;
|
||||||
this.formModel = null;
|
this.formModel = null;
|
||||||
this.cdr.detectChanges();
|
this.cdr.detectChanges();
|
||||||
this.initForm(sectionData);
|
this.initForm(sectionData, errors, sectionState.serverValidationErrors);
|
||||||
this.checksForErrors(errors);
|
this.checksForErrors(errors);
|
||||||
this.isUpdating = false;
|
this.isUpdating = false;
|
||||||
this.cdr.detectChanges();
|
this.cdr.detectChanges();
|
||||||
@@ -423,7 +424,7 @@ export class SubmissionSectionFormComponent extends SectionModelComponent {
|
|||||||
.subscribe((sectionState: SubmissionSectionObject) => {
|
.subscribe((sectionState: SubmissionSectionObject) => {
|
||||||
this.fieldsOnTheirWayToBeRemoved = new Map();
|
this.fieldsOnTheirWayToBeRemoved = new Map();
|
||||||
this.sectionMetadata = sectionState.metadata;
|
this.sectionMetadata = sectionState.metadata;
|
||||||
this.updateForm(sectionState.data as WorkspaceitemSectionFormObject, sectionState.errorsToShow);
|
this.updateForm(sectionState);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -1886,6 +1886,8 @@
|
|||||||
|
|
||||||
"footer.link.feedback": "Send Feedback",
|
"footer.link.feedback": "Send Feedback",
|
||||||
|
|
||||||
|
"footer.link.coar-notify-support": "COAR Notify",
|
||||||
|
|
||||||
"forgot-email.form.header": "Forgot Password",
|
"forgot-email.form.header": "Forgot Password",
|
||||||
|
|
||||||
"forgot-email.form.info": "Enter the email address associated with the account.",
|
"forgot-email.form.info": "Enter the email address associated with the account.",
|
||||||
@@ -2150,6 +2152,10 @@
|
|||||||
|
|
||||||
"info.feedback.page_help": "The page related to your feedback",
|
"info.feedback.page_help": "The page related to your feedback",
|
||||||
|
|
||||||
|
"info.coar-notify-support.title": "COAR Notify Support",
|
||||||
|
|
||||||
|
"info.coar-notify-support.breadcrumbs": "COAR Notify Support",
|
||||||
|
|
||||||
"item.alerts.private": "This item is non-discoverable",
|
"item.alerts.private": "This item is non-discoverable",
|
||||||
|
|
||||||
"item.alerts.withdrawn": "This item has been withdrawn",
|
"item.alerts.withdrawn": "This item has been withdrawn",
|
||||||
@@ -6173,10 +6179,6 @@
|
|||||||
"ldn-register-new-service.notification.success.title": "Success",
|
"ldn-register-new-service.notification.success.title": "Success",
|
||||||
"ldn-register-new-service.notification.success.content": "The process was successfully created",
|
"ldn-register-new-service.notification.success.content": "The process was successfully created",
|
||||||
|
|
||||||
"info.coar-notify-support.title": "Notify Support",
|
|
||||||
|
|
||||||
"info.coar-notify.breadcrumbs": "Notify Support",
|
|
||||||
|
|
||||||
"submission.sections.notify.info": "The selected service is compatible with the item according to its current status. {{ service.name }}: {{ service.description }}",
|
"submission.sections.notify.info": "The selected service is compatible with the item according to its current status. {{ service.name }}: {{ service.description }}",
|
||||||
|
|
||||||
"item.page.endorsement": "Endorsement",
|
"item.page.endorsement": "Endorsement",
|
||||||
|
@@ -73,9 +73,7 @@ const APP_CONFIG = new InjectionToken<AppConfig>('APP_CONFIG');
|
|||||||
|
|
||||||
const APP_CONFIG_STATE = makeStateKey<AppConfig>('APP_CONFIG_STATE');
|
const APP_CONFIG_STATE = makeStateKey<AppConfig>('APP_CONFIG_STATE');
|
||||||
|
|
||||||
export interface LazyDataServicesMap {
|
export type LazyDataServicesMap = Map<string, () => Promise<Type<HALDataService<any>> | { default: HALDataService<any> }>>;
|
||||||
[type: string]: () => Promise<Type<HALDataService<any>>>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const APP_DATA_SERVICES_MAP: InjectionToken<LazyDataServicesMap> = new InjectionToken<LazyDataServicesMap>('APP_DATA_SERVICES_MAP');
|
export const APP_DATA_SERVICES_MAP: InjectionToken<LazyDataServicesMap> = new InjectionToken<LazyDataServicesMap>('APP_DATA_SERVICES_MAP');
|
||||||
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { AppConfig } from './app-config.interface';
|
import { AppConfig } from './app-config.interface';
|
||||||
import { UniversalConfig } from './universal-config.interface';
|
import { SSRConfig } from './ssr-config.interface';
|
||||||
|
|
||||||
export interface BuildConfig extends AppConfig {
|
export interface BuildConfig extends AppConfig {
|
||||||
universal: UniversalConfig;
|
ssr: SSRConfig;
|
||||||
}
|
}
|
||||||
|
@@ -34,7 +34,7 @@ export class DefaultAppConfig implements AppConfig {
|
|||||||
// NOTE: will log all redux actions and transfers in console
|
// NOTE: will log all redux actions and transfers in console
|
||||||
debug = false;
|
debug = false;
|
||||||
|
|
||||||
// Angular Universal server settings
|
// Angular express server settings
|
||||||
// NOTE: these must be 'synced' with the 'dspace.ui.url' setting in your backend's local.cfg.
|
// NOTE: these must be 'synced' with the 'dspace.ui.url' setting in your backend's local.cfg.
|
||||||
ui: UIServerConfig = {
|
ui: UIServerConfig = {
|
||||||
ssl: false,
|
ssl: false,
|
||||||
@@ -468,9 +468,13 @@ export class DefaultAppConfig implements AppConfig {
|
|||||||
// Disabling the privacy policy feature will result in:
|
// Disabling the privacy policy feature will result in:
|
||||||
// - A 404 page if you manually try to navigate to the privacy policy page at info/privacy
|
// - A 404 page if you manually try to navigate to the privacy policy page at info/privacy
|
||||||
// - All mentions of the privacy policy being removed from the UI (e.g. in the footer)
|
// - All mentions of the privacy policy being removed from the UI (e.g. in the footer)
|
||||||
|
// Disabling the COAR notify support page feature will result in:
|
||||||
|
// - A 404 page if you manually try to navigate to the COAR notify support page
|
||||||
|
// - All mentions of the COAR notify support page being removed from the UI (e.g. in the footer)
|
||||||
info: InfoConfig = {
|
info: InfoConfig = {
|
||||||
enableEndUserAgreement: true,
|
enableEndUserAgreement: true,
|
||||||
enablePrivacyStatement: true,
|
enablePrivacyStatement: true,
|
||||||
|
enableCOARNotifySupport: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Whether to enable Markdown (https://commonmark.org/) and MathJax (https://www.mathjax.org/)
|
// Whether to enable Markdown (https://commonmark.org/) and MathJax (https://www.mathjax.org/)
|
||||||
|
@@ -3,4 +3,5 @@ import { Config } from './config.interface';
|
|||||||
export interface InfoConfig extends Config {
|
export interface InfoConfig extends Config {
|
||||||
enableEndUserAgreement: boolean;
|
enableEndUserAgreement: boolean;
|
||||||
enablePrivacyStatement: boolean;
|
enablePrivacyStatement: boolean;
|
||||||
|
enableCOARNotifySupport: boolean;
|
||||||
}
|
}
|
||||||
|
21
src/config/ssr-config.interface.ts
Normal file
21
src/config/ssr-config.interface.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { Config } from './config.interface';
|
||||||
|
|
||||||
|
export interface SSRConfig extends Config {
|
||||||
|
/**
|
||||||
|
* A boolean flag indicating whether the SSR configuration is enabled
|
||||||
|
* Defaults to true.
|
||||||
|
*/
|
||||||
|
enabled: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable request performance profiling data collection and printing the results in the server console.
|
||||||
|
* Defaults to false.
|
||||||
|
*/
|
||||||
|
enablePerformanceProfiler: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reduce render blocking requests by inlining critical CSS.
|
||||||
|
* Defaults to true.
|
||||||
|
*/
|
||||||
|
inlineCriticalCss: boolean;
|
||||||
|
}
|
@@ -6,5 +6,5 @@ export const StoreDevModules = [
|
|||||||
StoreDevtoolsModule.instrument({
|
StoreDevtoolsModule.instrument({
|
||||||
maxAge: 1000,
|
maxAge: 1000,
|
||||||
logOnly: false,
|
logOnly: false,
|
||||||
}),
|
connectInZone: true }),
|
||||||
];
|
];
|
||||||
|
@@ -1,7 +0,0 @@
|
|||||||
import { Config } from './config.interface';
|
|
||||||
|
|
||||||
export interface UniversalConfig extends Config {
|
|
||||||
preboot: boolean;
|
|
||||||
async: boolean;
|
|
||||||
time: boolean;
|
|
||||||
}
|
|
@@ -3,10 +3,10 @@ import { BuildConfig } from '../config/build-config.interface';
|
|||||||
export const environment: Partial<BuildConfig> = {
|
export const environment: Partial<BuildConfig> = {
|
||||||
production: true,
|
production: true,
|
||||||
|
|
||||||
// Angular Universal settings
|
// Angular SSR settings
|
||||||
universal: {
|
ssr: {
|
||||||
preboot: true,
|
enabled: true,
|
||||||
async: true,
|
enablePerformanceProfiler: false,
|
||||||
time: false,
|
inlineCriticalCss: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@@ -7,14 +7,14 @@ import { NotificationAnimationsType } from '../app/shared/notifications/models/n
|
|||||||
export const environment: BuildConfig = {
|
export const environment: BuildConfig = {
|
||||||
production: false,
|
production: false,
|
||||||
|
|
||||||
// Angular Universal settings
|
// Angular SSR settings
|
||||||
universal: {
|
ssr: {
|
||||||
preboot: true,
|
enabled: true,
|
||||||
async: true,
|
enablePerformanceProfiler: false,
|
||||||
time: false,
|
inlineCriticalCss: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Angular Universal server settings.
|
// Angular express server settings.
|
||||||
ui: {
|
ui: {
|
||||||
ssl: false,
|
ssl: false,
|
||||||
host: 'dspace.com',
|
host: 'dspace.com',
|
||||||
@@ -314,6 +314,7 @@ export const environment: BuildConfig = {
|
|||||||
info: {
|
info: {
|
||||||
enableEndUserAgreement: true,
|
enableEndUserAgreement: true,
|
||||||
enablePrivacyStatement: true,
|
enablePrivacyStatement: true,
|
||||||
|
enableCOARNotifySupport: true,
|
||||||
},
|
},
|
||||||
markdown: {
|
markdown: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
@@ -8,11 +8,11 @@ import { BuildConfig } from '../config/build-config.interface';
|
|||||||
export const environment: Partial<BuildConfig> = {
|
export const environment: Partial<BuildConfig> = {
|
||||||
production: false,
|
production: false,
|
||||||
|
|
||||||
// Angular Universal settings
|
// Angular SSR settings
|
||||||
universal: {
|
ssr: {
|
||||||
preboot: false,
|
enabled: false,
|
||||||
async: true,
|
enablePerformanceProfiler: false,
|
||||||
time: false,
|
inlineCriticalCss: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
8
src/express.tokens.ts
Normal file
8
src/express.tokens.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { InjectionToken } from '@angular/core';
|
||||||
|
import {
|
||||||
|
Request,
|
||||||
|
Response,
|
||||||
|
} from 'express';
|
||||||
|
|
||||||
|
export const REQUEST: InjectionToken<Request> = new InjectionToken<Request>('REQUEST');
|
||||||
|
export const RESPONSE: InjectionToken<Response> = new InjectionToken<Response>('RESPONSE');
|
@@ -14,6 +14,4 @@ import { serverAppConfig } from './modules/app/server-app.config';
|
|||||||
|
|
||||||
const bootstrap = () => bootstrapApplication(AppComponent, serverAppConfig);
|
const bootstrap = () => bootstrapApplication(AppComponent, serverAppConfig);
|
||||||
|
|
||||||
export { renderModule } from '@angular/platform-server';
|
|
||||||
export { ngExpressEngine } from '@nguniversal/express-engine';
|
|
||||||
export default bootstrap;
|
export default bootstrap;
|
||||||
|
@@ -20,7 +20,6 @@ import {
|
|||||||
StoreConfig,
|
StoreConfig,
|
||||||
StoreModule,
|
StoreModule,
|
||||||
} from '@ngrx/store';
|
} from '@ngrx/store';
|
||||||
import { REQUEST } from '@nguniversal/express-engine/tokens';
|
|
||||||
import {
|
import {
|
||||||
MissingTranslationHandler,
|
MissingTranslationHandler,
|
||||||
TranslateLoader,
|
TranslateLoader,
|
||||||
@@ -59,6 +58,7 @@ import { KlaroService } from '../../app/shared/cookies/klaro.service';
|
|||||||
import { MissingTranslationHelper } from '../../app/shared/translate/missing-translation.helper';
|
import { MissingTranslationHelper } from '../../app/shared/translate/missing-translation.helper';
|
||||||
import { GoogleAnalyticsService } from '../../app/statistics/google-analytics.service';
|
import { GoogleAnalyticsService } from '../../app/statistics/google-analytics.service';
|
||||||
import { SubmissionService } from '../../app/submission/submission.service';
|
import { SubmissionService } from '../../app/submission/submission.service';
|
||||||
|
import { REQUEST } from '../../express.tokens';
|
||||||
import { TranslateBrowserLoader } from '../../ngx-translate-loaders/translate-browser.loader';
|
import { TranslateBrowserLoader } from '../../ngx-translate-loaders/translate-browser.loader';
|
||||||
import { BrowserInitService } from './browser-init.service';
|
import { BrowserInitService } from './browser-init.service';
|
||||||
|
|
||||||
|
@@ -62,7 +62,8 @@ $navbar-light-toggler-icon-bg: url("data:image/svg+xml;charset=utf8,<svg+viewBox
|
|||||||
|
|
||||||
$ds-home-news-link-color: $link-color !default;
|
$ds-home-news-link-color: $link-color !default;
|
||||||
$ds-home-news-link-hover-color: darken($ds-home-news-link-color, 15%) !default;
|
$ds-home-news-link-hover-color: darken($ds-home-news-link-color, 15%) !default;
|
||||||
$ds-breadcrumb-link-color: $link-color !default;
|
$ds-breadcrumb-link-color: #003333 !default;
|
||||||
|
$ds-breadcrumb-link-active-color: #040D11 !default;
|
||||||
|
|
||||||
$ds-header-navbar-border-top-color: #fff !default;
|
$ds-header-navbar-border-top-color: #fff !default;
|
||||||
$ds-header-navbar-border-bottom-color: #ced4da !default;
|
$ds-header-navbar-border-bottom-color: #ced4da !default;
|
||||||
|
@@ -6,6 +6,7 @@ import {
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { HomeCoarComponent } from '../../../../app/home-page/home-coar/home-coar.component';
|
||||||
import { ThemedHomeNewsComponent } from '../../../../app/home-page/home-news/themed-home-news.component';
|
import { ThemedHomeNewsComponent } from '../../../../app/home-page/home-news/themed-home-news.component';
|
||||||
import { HomePageComponent as BaseComponent } from '../../../../app/home-page/home-page.component';
|
import { HomePageComponent as BaseComponent } from '../../../../app/home-page/home-page.component';
|
||||||
import { RecentItemListComponent } from '../../../../app/home-page/recent-item-list/recent-item-list.component';
|
import { RecentItemListComponent } from '../../../../app/home-page/recent-item-list/recent-item-list.component';
|
||||||
@@ -13,6 +14,7 @@ import { ThemedTopLevelCommunityListComponent } from '../../../../app/home-page/
|
|||||||
import { SuggestionsPopupComponent } from '../../../../app/notifications/suggestions-popup/suggestions-popup.component';
|
import { SuggestionsPopupComponent } from '../../../../app/notifications/suggestions-popup/suggestions-popup.component';
|
||||||
import { ThemedConfigurationSearchPageComponent } from '../../../../app/search-page/themed-configuration-search-page.component';
|
import { ThemedConfigurationSearchPageComponent } from '../../../../app/search-page/themed-configuration-search-page.component';
|
||||||
import { ThemedSearchFormComponent } from '../../../../app/shared/search-form/themed-search-form.component';
|
import { ThemedSearchFormComponent } from '../../../../app/shared/search-form/themed-search-form.component';
|
||||||
|
import { PageWithSidebarComponent } from '../../../../app/shared/sidebar/page-with-sidebar.component';
|
||||||
import { ViewTrackerComponent } from '../../../../app/statistics/angulartics/dspace/view-tracker.component';
|
import { ViewTrackerComponent } from '../../../../app/statistics/angulartics/dspace/view-tracker.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -22,7 +24,7 @@ import { ViewTrackerComponent } from '../../../../app/statistics/angulartics/dsp
|
|||||||
// templateUrl: './home-page.component.html'
|
// templateUrl: './home-page.component.html'
|
||||||
templateUrl: '../../../../app/home-page/home-page.component.html',
|
templateUrl: '../../../../app/home-page/home-page.component.html',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [ThemedHomeNewsComponent, NgIf, ViewTrackerComponent, ThemedSearchFormComponent, ThemedTopLevelCommunityListComponent, RecentItemListComponent, AsyncPipe, TranslateModule, NgClass, ThemedConfigurationSearchPageComponent, SuggestionsPopupComponent],
|
imports: [ThemedHomeNewsComponent, NgIf, ViewTrackerComponent, ThemedSearchFormComponent, ThemedTopLevelCommunityListComponent, RecentItemListComponent, AsyncPipe, TranslateModule, NgClass, ThemedConfigurationSearchPageComponent, SuggestionsPopupComponent, PageWithSidebarComponent, HomeCoarComponent],
|
||||||
})
|
})
|
||||||
export class HomePageComponent extends BaseComponent {
|
export class HomePageComponent extends BaseComponent {
|
||||||
|
|
||||||
|
@@ -23,10 +23,10 @@
|
|||||||
<p>The test user accounts below have their password set to the name of this
|
<p>The test user accounts below have their password set to the name of this
|
||||||
software in lowercase.</p>
|
software in lowercase.</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Demo Site Administrator = dspacedemo+admin@gmail.com</li>
|
<li>Demo Site Administrator = dspacedemo+admin@gmail.com</li>
|
||||||
<li>Demo Community Administrator = dspacedemo+commadmin@gmail.com</li>
|
<li>Demo Community Administrator = dspacedemo+commadmin@gmail.com</li>
|
||||||
<li>Demo Collection Administrator = dspacedemo+colladmin@gmail.com</li>
|
<li>Demo Collection Administrator = dspacedemo+colladmin@gmail.com</li>
|
||||||
<li>Demo Submitter = dspacedemo+submit@gmail.com</li>
|
<li>Demo Submitter = dspacedemo+submit@gmail.com</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -35,5 +35,5 @@
|
|||||||
<source type="image/jpg" srcset="assets/dspace/images/banner.jpg 2000w, assets/dspace/images/banner-half.jpg 1200w, assets/dspace/images/banner-tall.jpg 768w">
|
<source type="image/jpg" srcset="assets/dspace/images/banner.jpg 2000w, assets/dspace/images/banner-half.jpg 1200w, assets/dspace/images/banner-tall.jpg 768w">
|
||||||
<img alt="" [src]="'assets/dspace/images/banner.jpg'"/><!-- without the []="''" Firefox downloads both the fallback and the resolved image -->
|
<img alt="" [src]="'assets/dspace/images/banner.jpg'"/><!-- without the []="''" Firefox downloads both the fallback and the resolved image -->
|
||||||
</picture>
|
</picture>
|
||||||
<small class="credits">Photo by <a href="https://www.pexels.com/@inspiredimages">@inspiredimages</a></small>
|
<small class="credits">Photo by <a href="https://www.pexels.com/@inspiredimages">@inspiredimages</a></small>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -85,3 +85,6 @@ $navbar-dark-color: #fff;
|
|||||||
|
|
||||||
$ds-home-news-link-color: #92c642;
|
$ds-home-news-link-color: #92c642;
|
||||||
$ds-header-navbar-border-bottom-color: #92c642;
|
$ds-header-navbar-border-bottom-color: #92c642;
|
||||||
|
|
||||||
|
$ds-breadcrumb-link-color: #154E66 !default;
|
||||||
|
$ds-breadcrumb-link-active-color: #040D11 !default;
|
||||||
|
Reference in New Issue
Block a user