Merge remote-tracking branch 'origin/main' into CST-4506_item_embargo

This commit is contained in:
Giuseppe Digilio
2021-12-22 15:28:58 +01:00
153 changed files with 16482 additions and 14487 deletions

View File

@@ -29,11 +29,11 @@ jobs:
steps: steps:
# https://github.com/actions/checkout # https://github.com/actions/checkout
- name: Checkout codebase - name: Checkout codebase
uses: actions/checkout@v1 uses: actions/checkout@v2
# https://github.com/actions/setup-node # https://github.com/actions/setup-node
- name: Install Node.js ${{ matrix.node-version }} - name: Install Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1 uses: actions/setup-node@v2
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
@@ -82,7 +82,7 @@ jobs:
# Upload coverage reports to Codecov (for Node v12 only) # Upload coverage reports to Codecov (for Node v12 only)
# https://github.com/codecov/codecov-action # https://github.com/codecov/codecov-action
- name: Upload coverage to Codecov.io - name: Upload coverage to Codecov.io
uses: codecov/codecov-action@v1 uses: codecov/codecov-action@v2
if: matrix.node-version == '12.x' if: matrix.node-version == '12.x'
# Using docker-compose start backend using CI configuration # Using docker-compose start backend using CI configuration

78
.github/workflows/docker.yml vendored Normal file
View File

@@ -0,0 +1,78 @@
# DSpace Docker image build for hub.docker.com
name: Docker images
# Run this Build for all pushes to 'main' or maintenance branches, or tagged releases.
# Also run for PRs to ensure PR doesn't break Docker build process
on:
push:
branches:
- main
- 'dspace-**'
tags:
- 'dspace-**'
pull_request:
jobs:
docker:
# Ensure this job never runs on forked repos. It's only executed for 'dspace/dspace-angular'
if: github.repository == 'dspace/dspace-angular'
runs-on: ubuntu-latest
env:
# Define tags to use for Docker images based on Git tags/branches (for docker/metadata-action)
# For a new commit on default branch (main), use the literal tag 'dspace-7_x' on Docker image.
# For a new commit on other branches, use the branch name as the tag for Docker image.
# For a new tag, copy that tag name as the tag for Docker image.
IMAGE_TAGS: |
type=raw,value=dspace-7_x,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }}
type=ref,event=branch,enable=${{ !endsWith(github.ref, github.event.repository.default_branch) }}
type=ref,event=tag
# Define default tag "flavor" for docker/metadata-action per
# https://github.com/docker/metadata-action#flavor-input
# We turn off 'latest' tag by default.
TAGS_FLAVOR: |
latest=false
steps:
# https://github.com/actions/checkout
- name: Checkout codebase
uses: actions/checkout@v2
# https://github.com/docker/setup-buildx-action
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v1
# https://github.com/docker/login-action
- name: Login to DockerHub
# Only login if not a PR, as PRs only trigger a Docker build and not a push
if: github.event_name != 'pull_request'
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
###############################################
# Build/Push the 'dspace/dspace-angular' image
###############################################
# https://github.com/docker/metadata-action
# Get Metadata for docker_build step below
- name: Sync metadata (tags, labels) from GitHub to Docker for 'dspace-angular' image
id: meta_build
uses: docker/metadata-action@v3
with:
images: dspace/dspace-angular
tags: ${{ env.IMAGE_TAGS }}
flavor: ${{ env.TAGS_FLAVOR }}
# https://github.com/docker/build-push-action
- name: Build and push 'dspace-angular' image
id: docker_build
uses: docker/build-push-action@v2
with:
context: .
file: ./Dockerfile
# For pull requests, we run the Docker build (to ensure no PR changes break the build),
# but we ONLY do an image push to DockerHub if it's NOT a PR
push: ${{ github.event_name != 'pull_request' }}
# Use tags / labels provided by 'docker/metadata-action' above
tags: ${{ steps.meta_build.outputs.tags }}
labels: ${{ steps.meta_build.outputs.labels }}

View File

@@ -3,5 +3,6 @@
"i18n-ally.localesPaths": [ "i18n-ally.localesPaths": [
"src/assets/i18n", "src/assets/i18n",
"src/app/core/locale" "src/app/core/locale"
] ],
"typescript.tsdk": "node_modules\\typescript\\lib"
} }

View File

@@ -1,7 +1,7 @@
# This image will be published as dspace/dspace-angular # This image will be published as dspace/dspace-angular
# See https://dspace-labs.github.io/DSpace-Docker-Images/ for usage details # See https://github.com/DSpace/dspace-angular/tree/main/docker for usage details
FROM node:12-alpine FROM node:14-alpine
WORKDIR /app WORKDIR /app
ADD . /app/ ADD . /app/
EXPOSE 4000 EXPOSE 4000

18
LICENSE
View File

@@ -1,4 +1,4 @@
DSpace source code BSD License: BSD 3-Clause License
Copyright (c) 2002-2021, LYRASIS. All rights reserved. Copyright (c) 2002-2021, LYRASIS. All rights reserved.
@@ -13,13 +13,12 @@ notice, this list of conditions and the following disclaimer.
notice, this list of conditions and the following disclaimer in the notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution. documentation and/or other materials provided with the distribution.
- Neither the name DuraSpace nor the name of the DSpace Foundation - Neither the name of the copyright holder nor the names of its
nor the names of its contributors may be used to endorse or promote contributors may be used to endorse or promote products derived from
products derived from this software without specific prior written this software without specific prior written permission.
permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
@@ -30,10 +29,3 @@ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
DAMAGE. DAMAGE.
DSpace uses third-party libraries which may be distributed under
different licenses to the above. Information about these licenses
is detailed in the LICENSES_THIRD_PARTY file at the root of the source
tree. You must agree to the terms of these licenses, in addition to
the above DSpace source code license, in order to use this software.

28
NOTICE Normal file
View File

@@ -0,0 +1,28 @@
Licenses of Third-Party Libraries
=================================
DSpace uses third-party libraries which may be distributed under
different licenses than specified in our LICENSE file. Information
about these licenses is detailed in the LICENSES_THIRD_PARTY file at
the root of the source tree. You must agree to the terms of these
licenses, in addition to the DSpace source code license, in order to
use this software.
Licensing Notices
=================
[July 2019] DuraSpace joined with LYRASIS (another 501(c)3 organization) in July 2019.
LYRASIS holds the copyrights of DuraSpace.
[July 2009] Fedora Commons joined with the DSpace Foundation and began operating under
the new name DuraSpace in July 2009. DuraSpace holds the copyrights of
the DSpace Foundation, Inc.
[July 2007] The DSpace Foundation, Inc. is a 501(c)3 corporation established in July 2007
with a mission to promote and advance the dspace platform enabling management,
access and preservation of digital works. The Foundation was able to transfer
the legal copyright from Hewlett-Packard Company (HP) and Massachusetts
Institute of Technology (MIT) to the DSpace Foundation in October 2007. Many
of the files in the source code may contain a copyright statement stating HP
and MIT possess the copyright, in these instances please note that the copy
right has transferred to the DSpace foundation, and subsequently to DuraSpace.

View File

@@ -230,7 +230,9 @@ E2E tests (aka integration tests) use [Cypress.io](https://www.cypress.io/). Con
The test files can be found in the `./cypress/integration/` folder. The test files can be found in the `./cypress/integration/` folder.
Before you can run e2e tests, you MUST have a running backend (i.e. REST API). By default, the e2e tests look for this at http://localhost:8080/server/ or whatever `rest` backend is defined in your `environment.prod.ts` or `environment.common.ts`. You may override this using env variables, see [Configuring](#configuring). Before you can run e2e tests, two things are required:
1. You MUST have a running backend (i.e. REST API). By default, the e2e tests look for this at http://localhost:8080/server/ or whatever `rest` backend is defined in your `environment.prod.ts` or `environment.common.ts`. You may override this using env variables, see [Configuring](#configuring).
2. Your backend MUST include our Entities Test Data set. Some tests run against a (currently hardcoded) Community/Collection/Item UUID. These UUIDs are all valid for our Entities Test Data set. The Entities Test Data set may be installed easily via Docker, see https://github.com/DSpace/DSpace/tree/main/dspace/src/main/docker-compose#ingest-option-2-ingest-entities-test-data
Run `ng e2e` to kick off the tests. This will start Cypress and allow you to select the browser you wish to use, as well as whether you wish to run all tests or an individual test file. Once you click run on test(s), this opens the [Cypress Test Runner](https://docs.cypress.io/guides/core-concepts/test-runner) to run your test(s) and show you the results. Run `ng e2e` to kick off the tests. This will start Cypress and allow you to select the browser you wish to use, as well as whether you wish to run all tests or an individual test file. Once you click run on test(s), this opens the [Cypress Test Runner](https://docs.cypress.io/guides/core-concepts/test-runner) to run your test(s) and show you the results.
@@ -449,4 +451,8 @@ DSpace uses GitHub to track issues:
License License
------- -------
This project's source code is made available under the DSpace BSD License: http://www.dspace.org/license DSpace source code is freely available under a standard [BSD 3-Clause license](https://opensource.org/licenses/BSD-3-Clause).
The full license is available in the [LICENSE](LICENSE) file or online at http://www.dspace.org/license/
DSpace uses third-party libraries which may be distributed under different licenses. Those licenses are listed
in the [LICENSES_THIRD_PARTY](LICENSES_THIRD_PARTY) file.

View File

@@ -177,16 +177,13 @@
} }
}, },
"outputPath": "dist/server", "outputPath": "dist/server",
"main": "src/main.server.ts", "main": "server.ts",
"tsConfig": "tsconfig.server.json" "tsConfig": "tsconfig.server.json"
}, },
"configurations": { "configurations": {
"production": { "production": {
"sourceMap": false, "sourceMap": false,
"optimization": { "optimization": true
"scripts": false,
"styles": true
}
} }
} }
}, },

View File

@@ -5,5 +5,6 @@
"screenshotsFolder": "cypress/screenshots", "screenshotsFolder": "cypress/screenshots",
"pluginsFile": "cypress/plugins/index.ts", "pluginsFile": "cypress/plugins/index.ts",
"fixturesFolder": "cypress/fixtures", "fixturesFolder": "cypress/fixtures",
"baseUrl": "http://localhost:4000" "baseUrl": "http://localhost:4000",
"retries": 2
} }

View File

@@ -4,6 +4,20 @@
:warning: **NOT PRODUCTION READY** The below Docker Compose resources are not guaranteed "production ready" at this time. They have been built for development/testing only. Therefore, DSpace Docker images may not be fully secured or up-to-date. While you are welcome to base your own images on these DSpace images/resources, these should not be used "as is" in any production scenario. :warning: **NOT PRODUCTION READY** The below Docker Compose resources are not guaranteed "production ready" at this time. They have been built for development/testing only. Therefore, DSpace Docker images may not be fully secured or up-to-date. While you are welcome to base your own images on these DSpace images/resources, these should not be used "as is" in any production scenario.
*** ***
## 'Dockerfile' in root directory
This Dockerfile is used to build a *development* DSpace 7 Angular UI image, published as 'dspace/dspace-angular'
```
docker build -t dspace/dspace-angular:dspace-7_x .
```
This image is built *automatically* after each commit is made to the `main` branch.
Admins to our DockerHub repo can manually publish with the following command.
```
docker push dspace/dspace-angular:dspace-7_x
```
## docker directory ## docker directory
- docker-compose.yml - docker-compose.yml
- Starts DSpace Angular with Docker Compose from the current branch. This file assumes that a DSpace 7 REST instance will also be started in Docker. - Starts DSpace Angular with Docker Compose from the current branch. This file assumes that a DSpace 7 REST instance will also be started in Docker.

View File

@@ -64,7 +64,7 @@ services:
dspacesolr: dspacesolr:
container_name: dspacesolr container_name: dspacesolr
# Uses official Solr image at https://hub.docker.com/_/solr/ # Uses official Solr image at https://hub.docker.com/_/solr/
image: solr:8.8 image: solr:8.11-slim
# Needs main 'dspace' container to start first to guarantee access to solr_configs # Needs main 'dspace' container to start first to guarantee access to solr_configs
depends_on: depends_on:
- dspace - dspace

View File

@@ -62,7 +62,7 @@ services:
dspacesolr: dspacesolr:
container_name: dspacesolr container_name: dspacesolr
# Uses official Solr image at https://hub.docker.com/_/solr/ # Uses official Solr image at https://hub.docker.com/_/solr/
image: solr:8.8 image: solr:8.11-slim
# Needs main 'dspace' container to start first to guarantee access to solr_configs # Needs main 'dspace' container to start first to guarantee access to solr_configs
depends_on: depends_on:
- dspace - dspace

View File

@@ -20,7 +20,7 @@ services:
DSPACE_NAMESPACE: / DSPACE_NAMESPACE: /
DSPACE_PORT: '4000' DSPACE_PORT: '4000'
DSPACE_SSL: "false" DSPACE_SSL: "false"
image: dspace/dspace-angular:latest image: dspace/dspace-angular:dspace-7_x
build: build:
context: .. context: ..
dockerfile: Dockerfile dockerfile: Dockerfile

View File

@@ -26,16 +26,14 @@
"build": "ng build", "build": "ng build",
"build:stats": "ng build --stats-json", "build:stats": "ng build --stats-json",
"build:prod": "yarn run build:ssr", "build:prod": "yarn run build:ssr",
"build:ssr": "yarn run build:client-and-server-bundles && yarn run compile:server", "build:ssr": "ng build --configuration production && ng run dspace-angular:server:production",
"build:client-and-server-bundles": "ng build --prod && ng run dspace-angular:server:production --bundleDependencies true",
"test:watch": "npm-run-all --parallel config:test:watch test", "test:watch": "npm-run-all --parallel config:test:watch test",
"test": "ng test --sourceMap=true --watch=true", "test": "ng test --sourceMap=true --watch=true",
"test:headless": "ng test --watch=false --sourceMap=true --browsers=ChromeHeadless --code-coverage", "test:headless": "ng test --watch=false --sourceMap=true --browsers=ChromeHeadless --code-coverage",
"lint": "ng lint", "lint": "ng lint",
"lint-fix": "ng lint --fix=true", "lint-fix": "ng lint --fix=true",
"e2e": "ng e2e", "e2e": "ng e2e",
"compile:server": "webpack --config webpack.server.config.js --progress --color", "serve:ssr": "node dist/server/main",
"serve:ssr": "node dist/server",
"clean:coverage": "rimraf coverage", "clean:coverage": "rimraf coverage",
"clean:dist": "rimraf dist", "clean:dist": "rimraf dist",
"clean:doc": "rimraf doc", "clean:doc": "rimraf doc",
@@ -65,26 +63,26 @@
"webdriver-manager": "^12.1.8" "webdriver-manager": "^12.1.8"
}, },
"dependencies": { "dependencies": {
"@angular/animations": "~10.2.3", "@angular/animations": "~11.2.14",
"@angular/cdk": "^10.2.6", "@angular/cdk": "^11.2.13",
"@angular/common": "~10.2.3", "@angular/common": "~11.2.14",
"@angular/compiler": "~10.2.3", "@angular/compiler": "~11.2.14",
"@angular/core": "~10.2.3", "@angular/core": "~11.2.14",
"@angular/forms": "~10.2.3", "@angular/forms": "~11.2.14",
"@angular/localize": "10.2.3", "@angular/localize": "11.2.14",
"@angular/platform-browser": "~10.2.3", "@angular/platform-browser": "~11.2.14",
"@angular/platform-browser-dynamic": "~10.2.3", "@angular/platform-browser-dynamic": "~11.2.14",
"@angular/platform-server": "~10.2.3", "@angular/platform-server": "~11.2.14",
"@angular/router": "~10.2.3", "@angular/router": "~11.2.14",
"@angularclass/bootloader": "1.0.1", "@angularclass/bootloader": "1.0.1",
"@kolkov/ngx-gallery": "^1.2.3", "@kolkov/ngx-gallery": "^1.2.3",
"@ng-bootstrap/ng-bootstrap": "7.0.0", "@ng-bootstrap/ng-bootstrap": "9.1.3",
"@ng-dynamic-forms/core": "^12.0.0", "@ng-dynamic-forms/core": "^13.0.0",
"@ng-dynamic-forms/ui-ng-bootstrap": "^12.0.0", "@ng-dynamic-forms/ui-ng-bootstrap": "^13.0.0",
"@ngrx/effects": "^10.0.1", "@ngrx/effects": "^11.1.1",
"@ngrx/router-store": "^10.0.1", "@ngrx/router-store": "^11.1.1",
"@ngrx/store": "^10.0.1", "@ngrx/store": "^11.1.1",
"@nguniversal/express-engine": "10.1.0", "@nguniversal/express-engine": "11.2.1",
"@ngx-translate/core": "^13.0.0", "@ngx-translate/core": "^13.0.0",
"@nicky-lenaers/ngx-scroll-to": "^9.0.0", "@nicky-lenaers/ngx-scroll-to": "^9.0.0",
"angular-idle-preload": "3.0.0", "angular-idle-preload": "3.0.0",
@@ -94,9 +92,9 @@
"caniuse-lite": "^1.0.30001165", "caniuse-lite": "^1.0.30001165",
"cerialize": "0.1.18", "cerialize": "0.1.18",
"cli-progress": "^3.8.0", "cli-progress": "^3.8.0",
"compression": "^1.7.4",
"cookie-parser": "1.4.5", "cookie-parser": "1.4.5",
"core-js": "^3.7.0", "core-js": "^3.7.0",
"debug-loader": "^0.0.1",
"deepmerge": "^4.2.2", "deepmerge": "^4.2.2",
"express": "^4.17.1", "express": "^4.17.1",
"express-rate-limit": "^5.1.3", "express-rate-limit": "^5.1.3",
@@ -105,78 +103,78 @@
"filesize": "^6.1.0", "filesize": "^6.1.0",
"font-awesome": "4.7.0", "font-awesome": "4.7.0",
"https": "1.0.0", "https": "1.0.0",
"http-proxy-middleware": "^1.0.5",
"js-cookie": "2.2.1", "js-cookie": "2.2.1",
"json5": "^2.1.3", "json5": "^2.1.3",
"jsonschema": "1.4.0", "jsonschema": "1.4.0",
"jwt-decode": "^3.1.2", "jwt-decode": "^3.1.2",
"klaro": "^0.7.10", "klaro": "^0.7.10",
"mirador": "^3.0.0", "lodash": "^4.17.21",
"mirador": "^3.3.0",
"mirador-dl-plugin": "^0.13.0", "mirador-dl-plugin": "^0.13.0",
"mirador-share-plugin": "^0.10.0", "mirador-share-plugin": "^0.11.0",
"moment": "^2.29.1", "moment": "^2.29.1",
"morgan": "^1.10.0", "morgan": "^1.10.0",
"ng-mocks": "10.5.4", "ng-mocks": "11.11.2",
"ng2-file-upload": "1.4.0", "ng2-file-upload": "1.4.0",
"ng2-nouislider": "^1.8.2", "ng2-nouislider": "^1.8.3",
"ngx-infinite-scroll": "^10.0.1", "ngx-infinite-scroll": "^10.0.1",
"ngx-moment": "^5.0.0", "ngx-moment": "^5.0.0",
"ngx-pagination": "5.0.0", "ngx-pagination": "5.0.0",
"ngx-sortablejs": "^10.0.0", "ngx-sortablejs": "^11.1.0",
"nouislider": "^14.6.3", "nouislider": "^14.6.3",
"pem": "1.14.4", "pem": "1.14.4",
"postcss-cli": "^8.3.0", "postcss-cli": "^8.3.0",
"react": "^16.14.0",
"react-dom": "^16.14.0",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rxjs": "^6.6.3", "rxjs": "^6.6.3",
"rxjs-spy": "^7.5.3",
"sass-resources-loader": "^2.1.1",
"sortablejs": "1.13.0", "sortablejs": "1.13.0",
"tslib": "^2.0.0", "tslib": "^2.0.0",
"url-parse": "^1.5.3",
"uuid": "^8.3.2",
"webfontloader": "1.6.28", "webfontloader": "1.6.28",
"zone.js": "^0.10.3" "zone.js": "^0.10.3"
}, },
"devDependencies": { "devDependencies": {
"@angular-builders/custom-webpack": "10.0.1", "@angular-builders/custom-webpack": "10.0.1",
"@angular-devkit/build-angular": "~0.1002.0", "@angular-devkit/build-angular": "~0.1102.15",
"@angular/cli": "~10.2.0", "@angular/cli": "~11.2.15",
"@angular/compiler-cli": "~10.2.3", "@angular/compiler-cli": "~11.2.14",
"@angular/language-service": "~10.2.3", "@angular/language-service": "~11.2.14",
"@cypress/schematic": "^1.5.0", "@cypress/schematic": "^1.5.0",
"@fortawesome/fontawesome-free": "^5.5.0", "@fortawesome/fontawesome-free": "^5.5.0",
"@ngrx/store-devtools": "^10.0.1", "@ngrx/store-devtools": "^11.1.1",
"@ngtools/webpack": "10.2.0", "@ngtools/webpack": "10.2.3",
"@nguniversal/builders": "~10.1.0", "@nguniversal/builders": "~11.2.1",
"@types/deep-freeze": "0.1.2", "@types/deep-freeze": "0.1.2",
"@types/express": "^4.17.9", "@types/express": "^4.17.9",
"@types/file-saver": "^2.0.1", "@types/file-saver": "^2.0.1",
"@types/jasmine": "^3.6.2", "@types/jasmine": "~3.6.0",
"@types/jasminewd2": "~2.0.8", "@types/jasminewd2": "~2.0.8",
"@types/js-cookie": "2.2.6", "@types/js-cookie": "2.2.6",
"@types/lodash": "^4.14.165", "@types/lodash": "^4.14.165",
"@types/node": "^14.14.9", "@types/node": "^14.14.9",
"axe-core": "^4.3.3", "axe-core": "^4.3.3",
"codelyzer": "^6.0.1", "codelyzer": "^6.0.0",
"compression-webpack-plugin": "^3.0.1", "compression-webpack-plugin": "^3.0.1",
"copy-webpack-plugin": "^6.4.1", "copy-webpack-plugin": "^6.4.1",
"css-loader": "3.4.0", "css-loader": "3.4.0",
"cssnano": "^4.1.10", "cssnano": "^4.1.10",
"cypress": "8.6.0", "cypress": "8.6.0",
"cypress-axe": "^0.13.0", "cypress-axe": "^0.13.0",
"debug-loader": "^0.0.1",
"deep-freeze": "0.0.1", "deep-freeze": "0.0.1",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"fork-ts-checker-webpack-plugin": "^6.0.3", "fork-ts-checker-webpack-plugin": "^6.0.3",
"html-loader": "^1.3.2", "html-loader": "^1.3.2",
"html-webpack-plugin": "^4.5.0", "html-webpack-plugin": "^4.5.0",
"http-proxy-middleware": "^1.0.5", "jasmine-core": "~3.6.0",
"jasmine-core": "^3.6.0",
"jasmine-marbles": "0.6.0", "jasmine-marbles": "0.6.0",
"jasmine-spec-reporter": "^6.0.0", "jasmine-spec-reporter": "~5.0.0",
"karma": "^5.2.3", "karma": "^5.2.3",
"karma-chrome-launcher": "^3.1.0", "karma-chrome-launcher": "~3.1.0",
"karma-coverage-istanbul-reporter": "~3.0.2", "karma-coverage-istanbul-reporter": "~3.0.2",
"karma-jasmine": "^4.0.1", "karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "^1.5.4", "karma-jasmine-html-reporter": "^1.5.0",
"karma-mocha-reporter": "2.2.5", "karma-mocha-reporter": "2.2.5",
"nodemon": "^2.0.2", "nodemon": "^2.0.2",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
@@ -189,17 +187,21 @@
"protractor": "^7.0.0", "protractor": "^7.0.0",
"protractor-istanbul-plugin": "2.0.0", "protractor-istanbul-plugin": "2.0.0",
"raw-loader": "0.5.1", "raw-loader": "0.5.1",
"react": "^16.14.0",
"react-dom": "^16.14.0",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"rxjs-spy": "^7.5.3",
"sass-resources-loader": "^2.1.1",
"script-ext-html-webpack-plugin": "2.1.5", "script-ext-html-webpack-plugin": "2.1.5",
"string-replace-loader": "^2.3.0", "string-replace-loader": "^2.3.0",
"terser-webpack-plugin": "^2.3.1", "terser-webpack-plugin": "^2.3.1",
"ts-loader": "^5.2.0", "ts-loader": "^5.2.0",
"ts-node": "^8.8.1", "ts-node": "^8.10.2",
"tslint": "^6.1.3", "tslint": "^6.1.3",
"typescript": "~4.0.5", "typescript": "~4.0.5",
"webpack": "^4.44.2", "webpack": "^4.44.2",
"webpack-bundle-analyzer": "^4.4.0", "webpack-bundle-analyzer": "^4.4.0",
"webpack-cli": "^4.2.0", "webpack-cli": "^4.2.0",
"webpack-node-externals": "1.7.2" "webpack-dev-server": "^4.5.0"
} }
} }

View File

@@ -30,6 +30,7 @@ import { join } from 'path';
import { enableProdMode } from '@angular/core'; import { enableProdMode } from '@angular/core';
import { existsSync } from 'fs'; import { existsSync } from 'fs';
import { ngExpressEngine } from '@nguniversal/express-engine';
import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens'; 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';
@@ -37,6 +38,8 @@ import { hasValue, hasNoValue } from './src/app/shared/empty.util';
import { APP_BASE_HREF } from '@angular/common'; import { APP_BASE_HREF } from '@angular/common';
import { UIServerConfig } from './src/config/ui-server-config.interface'; import { UIServerConfig } from './src/config/ui-server-config.interface';
import { ServerAppModule } from './src/main.server';
/* /*
* Set path for the browser application's dist folder * Set path for the browser application's dist folder
*/ */
@@ -46,9 +49,6 @@ const IIIF_VIEWER = join(process.cwd(), 'dist/iiif');
const indexHtml = existsSync(join(DIST_FOLDER, 'index.html')) ? 'index.html' : 'index'; const indexHtml = existsSync(join(DIST_FOLDER, 'index.html')) ? 'index.html' : 'index';
// * NOTE :: leave this as require() since this file is built Dynamically from webpack
const { ServerAppModule, ngExpressEngine } = require('./dist/server/main');
const cookieParser = require('cookie-parser'); const cookieParser = require('cookie-parser');
// The Express app is exported so that it can be used by serverless Functions. // The Express app is exported so that it can be used by serverless Functions.
@@ -59,7 +59,6 @@ export function app() {
*/ */
const server = express(); const server = express();
/* /*
* If production mode is enabled in the environment file: * If production mode is enabled in the environment file:
* - Enable Angular's production mode * - Enable Angular's production mode
@@ -227,14 +226,15 @@ function run() {
}); });
} }
/* function start() {
/*
* If SSL is enabled * If SSL is enabled
* - Read credentials from configuration files * - Read credentials from configuration files
* - Call script to start an HTTPS server with these credentials * - Call script to start an HTTPS server with these credentials
* When SSL is disabled * When SSL is disabled
* - Start an HTTP server on the configured port and host * - Start an HTTP server on the configured port and host
*/ */
if (environment.ui.ssl) { if (environment.ui.ssl) {
let serviceKey; let serviceKey;
try { try {
serviceKey = fs.readFileSync('./config/ssl/key.pem'); serviceKey = fs.readFileSync('./config/ssl/key.pem');
@@ -266,8 +266,19 @@ if (environment.ui.ssl) {
createHttpsServer(keys); createHttpsServer(keys);
}); });
} }
} else { } else {
run(); run();
}
}
// Webpack will replace 'require' with '__webpack_require__'
// '__non_webpack_require__' is a proxy to Node 'require'
// The below code is to ensure that the server is run only when not requiring the bundle.
declare const __non_webpack_require__: NodeRequire;
const mainModule = __non_webpack_require__.main;
const moduleFilename = (mainModule && mainModule.filename) || '';
if (moduleFilename === __filename || moduleFilename.includes('iisnode')) {
start();
} }
export * from './src/main.server'; export * from './src/main.server';

View File

@@ -2,7 +2,7 @@ import { CommonModule } from '@angular/common';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { NO_ERRORS_SCHEMA } from '@angular/core'; import { NO_ERRORS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule, FormArray, FormControl, FormGroup,Validators, NG_VALIDATORS, NG_ASYNC_VALIDATORS } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser'; import { BrowserModule } from '@angular/platform-browser';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
@@ -34,6 +34,7 @@ import { TranslateLoaderMock } from '../../../shared/testing/translate-loader.mo
import { RouterMock } from '../../../shared/mocks/router.mock'; import { RouterMock } from '../../../shared/mocks/router.mock';
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
import { Operation } from 'fast-json-patch'; import { Operation } from 'fast-json-patch';
import { ValidateGroupExists } from './validators/group-exists.validator';
describe('GroupFormComponent', () => { describe('GroupFormComponent', () => {
let component: GroupFormComponent; let component: GroupFormComponent;
@@ -117,7 +118,69 @@ describe('GroupFormComponent', () => {
return null; return null;
} }
}; };
builderService = getMockFormBuilderService(); builderService = Object.assign(getMockFormBuilderService(),{
createFormGroup(formModel, options = null) {
const controls = {};
formModel.forEach( model => {
model.parent = parent;
const controlModel = model;
const controlState = { value: controlModel.value, disabled: controlModel.disabled };
const controlOptions = this.createAbstractControlOptions(controlModel.validators, controlModel.asyncValidators, controlModel.updateOn);
controls[model.id] = new FormControl(controlState, controlOptions);
});
return new FormGroup(controls, options);
},
createAbstractControlOptions(validatorsConfig = null, asyncValidatorsConfig = null, updateOn = null) {
return {
validators: validatorsConfig !== null ? this.getValidators(validatorsConfig) : null,
};
},
getValidators(validatorsConfig) {
return this.getValidatorFns(validatorsConfig);
},
getValidatorFns(validatorsConfig, validatorsToken = this._NG_VALIDATORS) {
let validatorFns = [];
if (this.isObject(validatorsConfig)) {
validatorFns = Object.keys(validatorsConfig).map(validatorConfigKey => {
const validatorConfigValue = validatorsConfig[validatorConfigKey];
if (this.isValidatorDescriptor(validatorConfigValue)) {
const descriptor = validatorConfigValue;
return this.getValidatorFn(descriptor.name, descriptor.args, validatorsToken);
}
return this.getValidatorFn(validatorConfigKey, validatorConfigValue, validatorsToken);
});
}
return validatorFns;
},
getValidatorFn(validatorName, validatorArgs = null, validatorsToken = this._NG_VALIDATORS) {
let validatorFn;
if (Validators.hasOwnProperty(validatorName)) { // Built-in Angular Validators
validatorFn = Validators[validatorName];
} else { // Custom Validators
if (this._DYNAMIC_VALIDATORS && this._DYNAMIC_VALIDATORS.has(validatorName)) {
validatorFn = this._DYNAMIC_VALIDATORS.get(validatorName);
} else if (validatorsToken) {
validatorFn = validatorsToken.find(validator => validator.name === validatorName);
}
}
if (validatorFn === undefined) { // throw when no validator could be resolved
throw new Error(`validator '${validatorName}' is not provided via NG_VALIDATORS, NG_ASYNC_VALIDATORS or DYNAMIC_FORM_VALIDATORS`);
}
if (validatorArgs !== null) {
return validatorFn(validatorArgs);
}
return validatorFn;
},
isValidatorDescriptor(value) {
if (this.isObject(value)) {
return value.hasOwnProperty('name') && value.hasOwnProperty('args');
}
return false;
},
isObject(value) {
return typeof value === 'object' && value !== null;
}
});
translateService = getMockTranslateService(); translateService = getMockTranslateService();
router = new RouterMock(); router = new RouterMock();
notificationService = new NotificationsServiceStub(); notificationService = new NotificationsServiceStub();
@@ -217,4 +280,72 @@ describe('GroupFormComponent', () => {
}); });
}); });
describe('check form validation', () => {
let groupCommunity;
beforeEach(() => {
groupName = 'testName';
groupCommunity = 'testgroupCommunity';
groupDescription = 'testgroupDescription';
expected = Object.assign(new Group(), {
name: groupName,
metadata: {
'dc.description': [
{
value: groupDescription
}
],
},
});
spyOn(component.submitForm, 'emit');
fixture.detectChanges();
component.initialisePage();
fixture.detectChanges();
});
describe('groupName, groupCommunity and groupDescription should be required', () => {
it('form should be invalid because the groupName is required', waitForAsync(() => {
fixture.whenStable().then(() => {
expect(component.formGroup.controls.groupName.valid).toBeFalse();
expect(component.formGroup.controls.groupName.errors.required).toBeTrue();
});
}));
});
describe('after inserting information groupName,groupCommunity and groupDescription not required', () => {
beforeEach(() => {
component.formGroup.controls.groupName.setValue('test');
fixture.detectChanges();
});
it('groupName should be valid because the groupName is set', waitForAsync(() => {
fixture.whenStable().then(() => {
expect(component.formGroup.controls.groupName.valid).toBeTrue();
expect(component.formGroup.controls.groupName.errors).toBeNull();
});
}));
});
describe('after already utilized groupName', () => {
beforeEach(() => {
const groupsDataServiceStubWithGroup = Object.assign(groupsDataServiceStub,{
searchGroups(query: string): Observable<RemoteData<PaginatedList<Group>>> {
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [expected]));
}
});
component.formGroup.controls.groupName.setValue('testName');
component.formGroup.controls.groupName.setAsyncValidators(ValidateGroupExists.createValidator(groupsDataServiceStubWithGroup));
fixture.detectChanges();
});
it('groupName should not be valid because groupName is already taken', waitForAsync(() => {
fixture.whenStable().then(() => {
expect(component.formGroup.controls.groupName.valid).toBeFalse();
expect(component.formGroup.controls.groupName.errors.groupExists).toBeTruthy();
});
}));
});
});
}); });

View File

@@ -1,4 +1,4 @@
import { Component, EventEmitter, HostListener, OnDestroy, OnInit, Output } from '@angular/core'; import { Component, EventEmitter, HostListener, OnDestroy, OnInit, Output, ChangeDetectorRef } from '@angular/core';
import { FormGroup } from '@angular/forms'; import { FormGroup } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
@@ -16,7 +16,7 @@ import {
of as observableOf, of as observableOf,
Subscription, Subscription,
} from 'rxjs'; } from 'rxjs';
import { catchError, map, switchMap, take, filter } from 'rxjs/operators'; import { catchError, map, switchMap, take, filter, debounceTime } from 'rxjs/operators';
import { getCollectionEditRolesRoute } from '../../../collection-page/collection-page-routing-paths'; import { getCollectionEditRolesRoute } from '../../../collection-page/collection-page-routing-paths';
import { getCommunityEditRolesRoute } from '../../../community-page/community-page-routing-paths'; import { getCommunityEditRolesRoute } from '../../../community-page/community-page-routing-paths';
import { DSpaceObjectDataService } from '../../../core/data/dspace-object-data.service'; import { DSpaceObjectDataService } from '../../../core/data/dspace-object-data.service';
@@ -45,6 +45,7 @@ import { NotificationsService } from '../../../shared/notifications/notification
import { followLink } from '../../../shared/utils/follow-link-config.model'; import { followLink } from '../../../shared/utils/follow-link-config.model';
import { NoContent } from '../../../core/shared/NoContent.model'; import { NoContent } from '../../../core/shared/NoContent.model';
import { Operation } from 'fast-json-patch'; import { Operation } from 'fast-json-patch';
import { ValidateGroupExists } from './validators/group-exists.validator';
@Component({ @Component({
selector: 'ds-group-form', selector: 'ds-group-form',
@@ -126,6 +127,12 @@ export class GroupFormComponent implements OnInit, OnDestroy {
*/ */
public AlertTypeEnum = AlertType; public AlertTypeEnum = AlertType;
/**
* Subscription to email field value change
*/
groupNameValueChangeSubscribe: Subscription;
constructor(public groupDataService: GroupDataService, constructor(public groupDataService: GroupDataService,
private ePersonDataService: EPersonDataService, private ePersonDataService: EPersonDataService,
private dSpaceObjectDataService: DSpaceObjectDataService, private dSpaceObjectDataService: DSpaceObjectDataService,
@@ -136,7 +143,8 @@ export class GroupFormComponent implements OnInit, OnDestroy {
protected router: Router, protected router: Router,
private authorizationService: AuthorizationDataService, private authorizationService: AuthorizationDataService,
private modalService: NgbModal, private modalService: NgbModal,
public requestService: RequestService) { public requestService: RequestService,
protected changeDetectorRef: ChangeDetectorRef) {
} }
ngOnInit() { ngOnInit() {
@@ -192,6 +200,14 @@ export class GroupFormComponent implements OnInit, OnDestroy {
this.groupDescription, this.groupDescription,
]; ];
this.formGroup = this.formBuilderService.createFormGroup(this.formModel); this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
if (!!this.formGroup.controls.groupName) {
this.formGroup.controls.groupName.setAsyncValidators(ValidateGroupExists.createValidator(this.groupDataService));
this.groupNameValueChangeSubscribe = this.groupName.valueChanges.pipe(debounceTime(300)).subscribe(() => {
this.changeDetectorRef.detectChanges();
});
}
this.subs.push( this.subs.push(
observableCombineLatest( observableCombineLatest(
this.groupDataService.getActiveGroup(), this.groupDataService.getActiveGroup(),
@@ -201,6 +217,10 @@ export class GroupFormComponent implements OnInit, OnDestroy {
).subscribe(([activeGroup, canEdit, linkedObject]) => { ).subscribe(([activeGroup, canEdit, linkedObject]) => {
if (activeGroup != null) { if (activeGroup != null) {
// Disable group name exists validator
this.formGroup.controls.groupName.clearAsyncValidators();
this.groupBeingEdited = activeGroup; this.groupBeingEdited = activeGroup;
if (linkedObject?.name) { if (linkedObject?.name) {
@@ -436,6 +456,11 @@ export class GroupFormComponent implements OnInit, OnDestroy {
ngOnDestroy(): void { ngOnDestroy(): void {
this.groupDataService.cancelEditGroup(); this.groupDataService.cancelEditGroup();
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe()); this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
if ( hasValue(this.groupNameValueChangeSubscribe) ) {
this.groupNameValueChangeSubscribe.unsubscribe();
}
} }
/** /**

View File

@@ -0,0 +1,33 @@
import { AbstractControl, ValidationErrors } from '@angular/forms';
import { Observable } from 'rxjs';
import { map} from 'rxjs/operators';
import { GroupDataService } from '../../../../core/eperson/group-data.service';
import { getFirstSucceededRemoteListPayload } from '../../../../core/shared/operators';
import { Group } from '../../../../core/eperson/models/group.model';
export class ValidateGroupExists {
/**
* This method will create the validator with the groupDataService requested from component
* @param groupDataService the service with DI in the component that this validator is being utilized.
* @return Observable<ValidationErrors | null>
*/
static createValidator(groupDataService: GroupDataService) {
return (control: AbstractControl): Promise<ValidationErrors | null> | Observable<ValidationErrors | null> => {
return groupDataService.searchGroups(control.value, {
currentPage: 1,
elementsPerPage: 100
})
.pipe(
getFirstSucceededRemoteListPayload(),
map( (groups: Group[]) => {
return groups.filter(group => group.name === control.value);
}),
map( (groups: Group[]) => {
return groups.length > 0 ? { groupExists: true } : null;
}),
);
};
}
}

View File

@@ -36,7 +36,7 @@ const ENTRY_COMPONENTS = [
export class AdminSearchModule { export class AdminSearchModule {
/** /**
* NOTE: this method allows to resolve issue with components that using a custom decorator * NOTE: this method allows to resolve issue with components that using a custom decorator
* which are not loaded during CSR otherwise * which are not loaded during SSR otherwise
*/ */
static withEntryComponents() { static withEntryComponents() {
return { return {

View File

@@ -28,7 +28,7 @@ const ENTRY_COMPONENTS = [
export class AdminWorkflowModuleModule { export class AdminWorkflowModuleModule {
/** /**
* NOTE: this method allows to resolve issue with components that using a custom decorator * NOTE: this method allows to resolve issue with components that using a custom decorator
* which are not loaded during CSR otherwise * which are not loaded during SSR otherwise
*/ */
static withEntryComponents() { static withEntryComponents() {
return { return {

View File

@@ -34,7 +34,7 @@ const ENTRY_COMPONENTS = [
export class AdminModule { export class AdminModule {
/** /**
* NOTE: this method allows to resolve issue with components that using a custom decorator * NOTE: this method allows to resolve issue with components that using a custom decorator
* which are not loaded during CSR otherwise * which are not loaded during SSR otherwise
*/ */
static withEntryComponents() { static withEntryComponents() {
return { return {

View File

@@ -203,7 +203,7 @@ import { GroupAdministratorGuard } from './core/data/feature-authorization/featu
]} ]}
],{ ],{
onSameUrlNavigation: 'reload', onSameUrlNavigation: 'reload',
}) })
], ],
exports: [RouterModule], exports: [RouterModule],
}) })

View File

@@ -171,7 +171,8 @@ describe('App component', () => {
TestBed.configureTestingModule(getDefaultTestBedConf()); TestBed.configureTestingModule(getDefaultTestBedConf());
TestBed.overrideProvider(ThemeService, {useValue: getMockThemeService('custom')}); TestBed.overrideProvider(ThemeService, {useValue: getMockThemeService('custom')});
document = TestBed.inject(DOCUMENT); document = TestBed.inject(DOCUMENT);
headSpy = jasmine.createSpyObj('head', ['appendChild']); headSpy = jasmine.createSpyObj('head', ['appendChild', 'getElementsByClassName']);
headSpy.getElementsByClassName.and.returnValue([]);
spyOn(document, 'getElementsByTagName').and.returnValue([headSpy]); spyOn(document, 'getElementsByTagName').and.returnValue([headSpy]);
fixture = TestBed.createComponent(AppComponent); fixture = TestBed.createComponent(AppComponent);
comp = fixture.componentInstance; comp = fixture.componentInstance;

View File

@@ -31,12 +31,12 @@ import { AuthService } from './core/auth/auth.service';
import { CSSVariableService } from './shared/sass-helper/sass-helper.service'; import { CSSVariableService } from './shared/sass-helper/sass-helper.service';
import { MenuService } from './shared/menu/menu.service'; import { MenuService } from './shared/menu/menu.service';
import { HostWindowService } from './shared/host-window.service'; import { HostWindowService } from './shared/host-window.service';
import { ThemeConfig } from '../config/theme.model'; import { HeadTagConfig, ThemeConfig } from '../config/theme.model';
import { Angulartics2DSpace } from './statistics/angulartics/dspace-provider'; import { Angulartics2DSpace } from './statistics/angulartics/dspace-provider';
import { environment } from '../environments/environment'; import { environment } from '../environments/environment';
import { models } from './core/core.module'; import { models } from './core/core.module';
import { LocaleService } from './core/locale/locale.service'; import { LocaleService } from './core/locale/locale.service';
import { hasValue, isNotEmpty } from './shared/empty.util'; import { hasNoValue, hasValue, isNotEmpty } from './shared/empty.util';
import { KlaroService } from './shared/cookies/klaro.service'; import { KlaroService } from './shared/cookies/klaro.service';
import { GoogleAnalyticsService } from './statistics/google-analytics.service'; import { GoogleAnalyticsService } from './statistics/google-analytics.service';
import { DOCUMENT, isPlatformBrowser } from '@angular/common'; import { DOCUMENT, isPlatformBrowser } from '@angular/common';
@@ -115,11 +115,11 @@ export class AppComponent implements OnInit, AfterViewInit {
this.isThemeCSSLoading$.next(true); this.isThemeCSSLoading$.next(true);
} }
if (hasValue(themeName)) { if (hasValue(themeName)) {
this.setThemeCss(themeName); this.loadGlobalThemeConfig(themeName);
} else if (hasValue(DEFAULT_THEME_CONFIG)) { } else if (hasValue(DEFAULT_THEME_CONFIG)) {
this.setThemeCss(DEFAULT_THEME_CONFIG.name); this.loadGlobalThemeConfig(DEFAULT_THEME_CONFIG.name);
} else { } else {
this.setThemeCss(BASE_THEME_NAME); this.loadGlobalThemeConfig(BASE_THEME_NAME);
} }
}); });
@@ -233,6 +233,11 @@ export class AppComponent implements OnInit, AfterViewInit {
} }
} }
private loadGlobalThemeConfig(themeName: string): void {
this.setThemeCss(themeName);
this.setHeadTags(themeName);
}
/** /**
* Update the theme css file in <head> * Update the theme css file in <head>
* *
@@ -241,9 +246,13 @@ export class AppComponent implements OnInit, AfterViewInit {
*/ */
private setThemeCss(themeName: string): void { private setThemeCss(themeName: string): void {
const head = this.document.getElementsByTagName('head')[0]; const head = this.document.getElementsByTagName('head')[0];
if (hasNoValue(head)) {
return;
}
// Array.from to ensure we end up with an array, not an HTMLCollection, which would be // Array.from to ensure we end up with an array, not an HTMLCollection, which would be
// automatically updated if we add nodes later // automatically updated if we add nodes later
const currentThemeLinks = Array.from(this.document.getElementsByClassName('theme-css')); const currentThemeLinks = Array.from(head.getElementsByClassName('theme-css'));
const link = this.document.createElement('link'); const link = this.document.createElement('link');
link.setAttribute('rel', 'stylesheet'); link.setAttribute('rel', 'stylesheet');
link.setAttribute('type', 'text/css'); link.setAttribute('type', 'text/css');
@@ -265,6 +274,78 @@ export class AppComponent implements OnInit, AfterViewInit {
head.appendChild(link); head.appendChild(link);
} }
private setHeadTags(themeName: string): void {
const head = this.document.getElementsByTagName('head')[0];
if (hasNoValue(head)) {
return;
}
// clear head tags
const currentHeadTags = Array.from(head.getElementsByClassName('theme-head-tag'));
if (hasValue(currentHeadTags)) {
currentHeadTags.forEach((currentHeadTag: any) => currentHeadTag.remove());
}
// create new head tags (not yet added to DOM)
const headTagFragment = this.document.createDocumentFragment();
this.createHeadTags(themeName)
.forEach(newHeadTag => headTagFragment.appendChild(newHeadTag));
// add new head tags to DOM
head.appendChild(headTagFragment);
}
private createHeadTags(themeName: string): HTMLElement[] {
const themeConfig = this.themeService.getThemeConfigFor(themeName);
const headTagConfigs = themeConfig?.headTags;
if (hasNoValue(headTagConfigs)) {
const parentThemeName = themeConfig?.extends;
if (hasValue(parentThemeName)) {
// inherit the head tags of the parent theme
return this.createHeadTags(parentThemeName);
}
const defaultThemeName = DEFAULT_THEME_CONFIG.name;
if (
hasNoValue(defaultThemeName) ||
themeName === defaultThemeName ||
themeName === BASE_THEME_NAME
) {
// last resort, use fallback favicon.ico
return [
this.createHeadTag({
'tagName': 'link',
'attributes': {
'rel': 'icon',
'href': 'assets/images/favicon.ico',
'sizes': 'any',
}
})
];
}
// inherit the head tags of the default theme
return this.createHeadTags(DEFAULT_THEME_CONFIG.name);
}
return headTagConfigs.map(this.createHeadTag.bind(this));
}
private createHeadTag(headTagConfig: HeadTagConfig): HTMLElement {
const tag = this.document.createElement(headTagConfig.tagName);
if (hasValue(headTagConfig.attributes)) {
Object.entries(headTagConfig.attributes)
.forEach(([key, value]) => tag.setAttribute(key, value));
}
// 'class' attribute should always be 'theme-head-tag' for removal
tag.setAttribute('class', 'theme-head-tag');
return tag;
}
private trackIdleModal() { private trackIdleModal() {
const isIdle$ = this.authService.isUserIdle(); const isIdle$ = this.authService.isUserIdle();
const isAuthenticated$ = this.authService.isAuthenticated(); const isAuthenticated$ = this.authService.isAuthenticated();

View File

@@ -10,11 +10,11 @@
</nav> </nav>
<ng-template #breadcrumb let-text="text" let-url="url"> <ng-template #breadcrumb let-text="text" let-url="url">
<li class="breadcrumb-item"><a [routerLink]="url">{{text | translate}}</a></li> <li class="breadcrumb-item"><div class="breadcrumb-item-limiter"><a [routerLink]="url" class="text-truncate">{{text | translate}}</a></div></li>
</ng-template> </ng-template>
<ng-template #activeBreadcrumb let-text="text"> <ng-template #activeBreadcrumb let-text="text">
<li class="breadcrumb-item active" aria-current="page">{{text | translate}}</li> <li class="breadcrumb-item active" aria-current="page"><div class="breadcrumb-item-limiter"><div class="text-truncate">{{text | translate}}</div></div></li>
</ng-template> </ng-template>
</ng-container> </ng-container>

View File

@@ -10,6 +10,19 @@
background-color: var(--ds-breadcrumb-bg); background-color: var(--ds-breadcrumb-bg);
} }
li.breadcrumb-item {
display: flex;
}
.breadcrumb-item-limiter {
display: inline-block;
max-width: var(--ds-breadcrumb-max-length);
> * {
max-width: 100%;
display: block;
}
}
li.breadcrumb-item > a { li.breadcrumb-item > a {
color: var(--ds-breadcrumb-link-color) !important; color: var(--ds-breadcrumb-link-color) !important;
} }
@@ -18,5 +31,6 @@ li.breadcrumb-item.active {
} }
.breadcrumb-item+ .breadcrumb-item::before { .breadcrumb-item+ .breadcrumb-item::before {
display: block;
content: quote("") !important; content: quote("") !important;
} }

View File

@@ -8,7 +8,7 @@ import { By } from '@angular/platform-browser';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { TranslateLoaderMock } from '../shared/testing/translate-loader.mock'; import { TranslateLoaderMock } from '../shared/testing/translate-loader.mock';
import { RouterTestingModule } from '@angular/router/testing'; import { RouterTestingModule } from '@angular/router/testing';
import { of as observableOf } from 'rxjs/internal/observable/of'; import { of as observableOf } from 'rxjs';
import { DebugElement } from '@angular/core'; import { DebugElement } from '@angular/core';
describe('BreadcrumbsComponent', () => { describe('BreadcrumbsComponent', () => {
@@ -72,7 +72,7 @@ describe('BreadcrumbsComponent', () => {
expect(breadcrumbs.length).toBe(3); expect(breadcrumbs.length).toBe(3);
expectBreadcrumb(breadcrumbs[0], 'home.breadcrumbs', '/'); expectBreadcrumb(breadcrumbs[0], 'home.breadcrumbs', '/');
expectBreadcrumb(breadcrumbs[1], 'bc 1', '/example.com'); expectBreadcrumb(breadcrumbs[1], 'bc 1', '/example.com');
expectBreadcrumb(breadcrumbs[2], 'bc 2', null); expectBreadcrumb(breadcrumbs[2].query(By.css('.text-truncate')), 'bc 2', null);
}); });
}); });

View File

@@ -1,7 +1,7 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { Breadcrumb } from './breadcrumb/breadcrumb.model'; import { Breadcrumb } from './breadcrumb/breadcrumb.model';
import { BreadcrumbsService } from './breadcrumbs.service'; import { BreadcrumbsService } from './breadcrumbs.service';
import { Observable } from 'rxjs/internal/Observable'; import { Observable } from 'rxjs';
/** /**
* Component representing the breadcrumbs of a page * Component representing the breadcrumbs of a page

View File

@@ -31,7 +31,7 @@ const ENTRY_COMPONENTS = [
export class BrowseByModule { export class BrowseByModule {
/** /**
* NOTE: this method allows to resolve issue with components that using a custom decorator * NOTE: this method allows to resolve issue with components that using a custom decorator
* which are not loaded during CSR otherwise * which are not loaded during SSR otherwise
*/ */
static withEntryComponents() { static withEntryComponents() {
return { return {

View File

@@ -5,9 +5,10 @@
<p [innerHTML]="'collection.edit.item-mapper.collection' | translate:{ name: (collectionName$ |async) }" id="collection-name"></p> <p [innerHTML]="'collection.edit.item-mapper.collection' | translate:{ name: (collectionName$ |async) }" id="collection-name"></p>
<p>{{'collection.edit.item-mapper.description' | translate}}</p> <p>{{'collection.edit.item-mapper.description' | translate}}</p>
<ngb-tabset (tabChange)="tabChange($event)" [destroyOnHide]="true" #tabs="ngbTabset"> <ul ngbNav (navChange)="tabChange($event)" [destroyOnHide]="true" #tabs="ngbNav" class="nav-tabs">
<ngb-tab title="{{'collection.edit.item-mapper.tabs.browse' | translate}}" id="browseTab"> <li [ngbNavItem]="'browseTab'">
<ng-template ngbTabContent> <a ngbNavLink>{{'collection.edit.item-mapper.tabs.browse' | translate}}</a>
<ng-template ngbNavContent>
<div class="mt-2"> <div class="mt-2">
<ds-item-select class="mt-2" <ds-item-select class="mt-2"
[key]="'browse'" [key]="'browse'"
@@ -21,9 +22,10 @@
(cancel)="onCancel()"></ds-item-select> (cancel)="onCancel()"></ds-item-select>
</div> </div>
</ng-template> </ng-template>
</ngb-tab> </li>
<ngb-tab title="{{'collection.edit.item-mapper.tabs.map' | translate}}" id="mapTab"> <li [ngbNavItem]="'mapTab'">
<ng-template ngbTabContent> <a ngbNavLink>{{'collection.edit.item-mapper.tabs.map' | translate}}</a>
<ng-template ngbNavContent>
<div class="row mt-2"> <div class="row mt-2">
<div class="col-12 col-lg-6"> <div class="col-12 col-lg-6">
<ds-search-form id="search-form" <ds-search-form id="search-form"
@@ -52,8 +54,9 @@
{{'collection.edit.item-mapper.no-search' | translate}} {{'collection.edit.item-mapper.no-search' | translate}}
</div> </div>
</ng-template> </ng-template>
</ngb-tab> </li>
</ngb-tabset> </ul>
<div [ngbNavOutlet]="tabs"></div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -27,7 +27,7 @@ import { ItemSelectComponent } from '../../shared/object-select/item-select/item
import { ObjectSelectService } from '../../shared/object-select/object-select.service'; import { ObjectSelectService } from '../../shared/object-select/object-select.service';
import { ObjectSelectServiceStub } from '../../shared/testing/object-select-service.stub'; import { ObjectSelectServiceStub } from '../../shared/testing/object-select-service.stub';
import { VarDirective } from '../../shared/utils/var.directive'; import { VarDirective } from '../../shared/utils/var.directive';
import { of as observableOf } from 'rxjs/internal/observable/of'; import { of as observableOf } from 'rxjs';
import { RouteService } from '../../core/services/route.service'; import { RouteService } from '../../core/services/route.service';
import { ErrorComponent } from '../../shared/error/error.component'; import { ErrorComponent } from '../../shared/error/error.component';
import { LoadingComponent } from '../../shared/loading/loading.component'; import { LoadingComponent } from '../../shared/loading/loading.component';

View File

@@ -11,18 +11,16 @@ import {
import { filter, map, switchMap, tap } from 'rxjs/operators'; import { filter, map, switchMap, tap } from 'rxjs/operators';
import { hasValue, hasValueOperator } from '../../../../shared/empty.util'; import { hasValue, hasValueOperator } from '../../../../shared/empty.util';
import { ProcessStatus } from '../../../../process-page/processes/process-status.model'; import { ProcessStatus } from '../../../../process-page/processes/process-status.model';
import { Subscription } from 'rxjs/internal/Subscription'; import { BehaviorSubject, Observable, Subscription } from 'rxjs';
import { RequestService } from '../../../../core/data/request.service'; import { RequestService } from '../../../../core/data/request.service';
import { NotificationsService } from '../../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../../shared/notifications/notifications.service';
import { Collection } from '../../../../core/shared/collection.model'; import { Collection } from '../../../../core/shared/collection.model';
import { CollectionDataService } from '../../../../core/data/collection-data.service'; import { CollectionDataService } from '../../../../core/data/collection-data.service';
import { Observable } from 'rxjs/internal/Observable';
import { Process } from '../../../../process-page/processes/process.model'; import { Process } from '../../../../process-page/processes/process.model';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { BitstreamDataService } from '../../../../core/data/bitstream-data.service'; import { BitstreamDataService } from '../../../../core/data/bitstream-data.service';
import { ContentSourceSetSerializer } from '../../../../core/shared/content-source-set-serializer'; import { ContentSourceSetSerializer } from '../../../../core/shared/content-source-set-serializer';
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
/** /**
* Component that contains the controls to run, reset and test the harvest * Component that contains the controls to run, reset and test the harvest

View File

@@ -1,9 +1,8 @@
import { Subscription } from 'rxjs/internal/Subscription';
import { FindListOptions } from '../core/data/request.models'; import { FindListOptions } from '../core/data/request.models';
import { hasValue } from '../shared/empty.util'; import { hasValue } from '../shared/empty.util';
import { CommunityListService, FlatNode } from './community-list-service'; import { CommunityListService, FlatNode } from './community-list-service';
import { CollectionViewer, DataSource } from '@angular/cdk/collections'; import { CollectionViewer, DataSource } from '@angular/cdk/collections';
import { BehaviorSubject, Observable, } from 'rxjs'; import { BehaviorSubject, Observable, Subscription } from 'rxjs';
import { finalize } from 'rxjs/operators'; import { finalize } from 'rxjs/operators';
/** /**

View File

@@ -10,8 +10,7 @@ import {
LinkDefinition LinkDefinition
} from './build-decorators'; } from './build-decorators';
import { RemoteData } from '../../data/remote-data'; import { RemoteData } from '../../data/remote-data';
import { Observable } from 'rxjs/internal/Observable'; import { EMPTY, Observable } from 'rxjs';
import { EMPTY } from 'rxjs';
import { ResourceType } from '../../shared/resource-type'; import { ResourceType } from '../../shared/resource-type';
/** /**

View File

@@ -7,7 +7,7 @@ import { PostRequest } from './request.models';
import { Registration } from '../shared/registration.model'; import { Registration } from '../shared/registration.model';
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
import { of as observableOf } from 'rxjs/internal/observable/of'; import { of as observableOf } from 'rxjs';
import { TestScheduler } from 'rxjs/testing'; import { TestScheduler } from 'rxjs/testing';
describe('EpersonRegistrationService', () => { describe('EpersonRegistrationService', () => {

View File

@@ -14,7 +14,7 @@ import { FindListOptions } from './request.models';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { dataService } from '../cache/builders/build-decorators'; import { dataService } from '../cache/builders/build-decorators';
import { RemoteData } from './remote-data'; import { RemoteData } from './remote-data';
import { Observable } from 'rxjs/internal/Observable'; import { Observable } from 'rxjs';
import { PaginatedList } from './paginated-list.model'; import { PaginatedList } from './paginated-list.model';
import { ITEM_TYPE } from '../shared/item-relationships/item-type.resource-type'; import { ITEM_TYPE } from '../shared/item-relationships/item-type.resource-type';
import { LICENSE } from '../shared/license.resource-type'; import { LICENSE } from '../shared/license.resource-type';

View File

@@ -10,7 +10,6 @@ import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util';
import { difference } from '../../shared/object.util'; import { difference } from '../../shared/object.util';
import { isNumeric } from 'rxjs/internal-compatibility'; import { isNumeric } from 'rxjs/internal-compatibility';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })

View File

@@ -7,7 +7,7 @@ import { HALLink } from './hal-link.model';
import { ResourceType } from './resource-type'; import { ResourceType } from './resource-type';
import { RemoteData } from '../data/remote-data'; import { RemoteData } from '../data/remote-data';
import { PaginatedList } from '../data/paginated-list.model'; import { PaginatedList } from '../data/paginated-list.model';
import { Observable } from 'rxjs/internal/Observable'; import { Observable } from 'rxjs';
import { ITEM_TYPE } from './item-relationships/item-type.resource-type'; import { ITEM_TYPE } from './item-relationships/item-type.resource-type';
import { ItemType } from './item-relationships/item-type.model'; import { ItemType } from './item-relationships/item-type.model';

View File

@@ -5,7 +5,7 @@ import { map, take } from 'rxjs/operators';
import { NativeWindowRef, NativeWindowService } from '../services/window.service'; import { NativeWindowRef, NativeWindowService } from '../services/window.service';
import { URLCombiner } from '../url-combiner/url-combiner'; import { URLCombiner } from '../url-combiner/url-combiner';
import { hasValue } from '../../shared/empty.util'; import { hasValue } from '../../shared/empty.util';
import { Observable } from 'rxjs/internal/Observable'; import { Observable } from 'rxjs';
/** /**
* Provides utility methods to save files on the client-side. * Provides utility methods to save files on the client-side.

View File

@@ -1,4 +1,4 @@
import * as uuidv4 from 'uuid/v4'; import { v4 as uuidv4 } from 'uuid';
import { autoserialize, Serialize, Deserialize } from 'cerialize'; import { autoserialize, Serialize, Deserialize } from 'cerialize';
import { hasValue } from '../../shared/empty.util'; import { hasValue } from '../../shared/empty.util';
/* tslint:disable:max-classes-per-file */ /* tslint:disable:max-classes-per-file */

View File

@@ -1,5 +1,5 @@
import { isUndefined } from '../../shared/empty.util'; import { isUndefined } from '../../shared/empty.util';
import * as uuidv4 from 'uuid/v4'; import { v4 as uuidv4 } from 'uuid';
import { MetadataMap, MetadataValue, MetadataValueFilter, MetadatumViewModel } from './metadata.models'; import { MetadataMap, MetadataValue, MetadataValueFilter, MetadatumViewModel } from './metadata.models';
import { Metadata } from './metadata.utils'; import { Metadata } from './metadata.utils';

View File

@@ -26,7 +26,7 @@ import { hasNoValue, hasValue, isNotEmpty, isNotEmptyOperator } from '../../../s
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
import { SearchConfig } from './search-filters/search-config.model'; import { SearchConfig } from './search-filters/search-config.model';
import { SearchService } from './search.service'; import { SearchService } from './search.service';
import { of } from 'rxjs/internal/observable/of'; import { of } from 'rxjs';
import { PaginationService } from '../../pagination/pagination.service'; import { PaginationService } from '../../pagination/pagination.service';
/** /**

View File

@@ -1,5 +1,5 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import * as uuidv4 from 'uuid/v4'; import { v4 as uuidv4 } from 'uuid';
@Injectable() @Injectable()
export class UUIDService { export class UUIDService {

View File

@@ -8,7 +8,7 @@ import { RemoteDataBuildService } from '../cache/builders/remote-data-build.serv
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { CoreState } from '../core.reducers'; import { CoreState } from '../core.reducers';
import { ClaimedTaskDataService } from './claimed-task-data.service'; import { ClaimedTaskDataService } from './claimed-task-data.service';
import { of as observableOf } from 'rxjs/internal/observable/of'; import { of as observableOf } from 'rxjs';
import { FindListOptions } from '../data/request.models'; import { FindListOptions } from '../data/request.models';
import { RequestParam } from '../cache/models/request-param.model'; import { RequestParam } from '../cache/models/request-param.model';
import { getTestScheduler } from 'jasmine-marbles'; import { getTestScheduler } from 'jasmine-marbles';

View File

@@ -10,7 +10,7 @@ import { CoreState } from '../core.reducers';
import { PoolTaskDataService } from './pool-task-data.service'; import { PoolTaskDataService } from './pool-task-data.service';
import { getTestScheduler } from 'jasmine-marbles'; import { getTestScheduler } from 'jasmine-marbles';
import { TestScheduler } from 'rxjs/testing'; import { TestScheduler } from 'rxjs/testing';
import { of as observableOf } from 'rxjs/internal/observable/of'; import { of as observableOf } from 'rxjs';
import { FindListOptions } from '../data/request.models'; import { FindListOptions } from '../data/request.models';
import { RequestParam } from '../cache/models/request-param.model'; import { RequestParam } from '../cache/models/request-param.model';
import { HttpOptions } from '../dspace-rest/dspace-rest.service'; import { HttpOptions } from '../dspace-rest/dspace-rest.service';

View File

@@ -17,7 +17,7 @@ import { HttpClient, HttpHeaders } from '@angular/common/http';
import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service'; import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';
import { ChangeAnalyzer } from '../data/change-analyzer'; import { ChangeAnalyzer } from '../data/change-analyzer';
import { compare, Operation } from 'fast-json-patch'; import { compare, Operation } from 'fast-json-patch';
import { of as observableOf } from 'rxjs/internal/observable/of'; import { of as observableOf } from 'rxjs';
import { HttpOptions } from '../dspace-rest/dspace-rest.service'; import { HttpOptions } from '../dspace-rest/dspace-rest.service';
import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock'; import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock';
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';

View File

@@ -8,11 +8,10 @@ import {
HttpResponse, HttpResponse,
HttpXsrfTokenExtractor HttpXsrfTokenExtractor
} from '@angular/common/http'; } from '@angular/common/http';
import { Observable } from 'rxjs/internal/Observable'; import { Observable, throwError } from 'rxjs';
import { tap, catchError } from 'rxjs/operators'; import { tap, catchError } from 'rxjs/operators';
import { RESTURLCombiner } from '../url-combiner/rest-url-combiner'; import { RESTURLCombiner } from '../url-combiner/rest-url-combiner';
import { CookieService } from '../services/cookie.service'; import { CookieService } from '../services/cookie.service';
import { throwError } from 'rxjs';
// Name of XSRF header we may send in requests to backend (this is a standard name defined by Angular) // Name of XSRF header we may send in requests to backend (this is a standard name defined by Angular)
export const XSRF_REQUEST_HEADER = 'X-XSRF-TOKEN'; export const XSRF_REQUEST_HEADER = 'X-XSRF-TOKEN';

View File

@@ -1,10 +1,10 @@
<ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge> <ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge>
<ds-truncatable [id]="dso.id"> <ds-truncatable [id]="dso.id">
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" <a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer"
[routerLink]="[itemPageRoute]" class="lead item-list-title" [routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out"
[innerHTML]="dsoTitle"></a> [innerHTML]="dsoTitle"></a>
<span *ngIf="linkType == linkTypes.None" <span *ngIf="linkType == linkTypes.None"
class="lead item-list-title" class="lead item-list-title dont-break-out"
[innerHTML]="dsoTitle"></span> [innerHTML]="dsoTitle"></span>
<span class="text-muted"> <span class="text-muted">
<ds-truncatable-part [id]="dso.id" [minLines]="1"> <ds-truncatable-part [id]="dso.id" [minLines]="1">

View File

@@ -1,10 +1,10 @@
<ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge> <ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge>
<ds-truncatable [id]="dso.id"> <ds-truncatable [id]="dso.id">
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" <a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer"
[routerLink]="[itemPageRoute]" class="lead item-list-title" [routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out"
[innerHTML]="dsoTitle"></a> [innerHTML]="dsoTitle"></a>
<span *ngIf="linkType == linkTypes.None" <span *ngIf="linkType == linkTypes.None"
class="lead item-list-title" class="lead item-list-title dont-break-out"
[innerHTML]="dsoTitle"></span> [innerHTML]="dsoTitle"></span>
<span class="text-muted"> <span class="text-muted">
<ds-truncatable-part [id]="dso.id" [minLines]="1"> <ds-truncatable-part [id]="dso.id" [minLines]="1">

View File

@@ -1,10 +1,10 @@
<ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge> <ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge>
<ds-truncatable [id]="dso.id"> <ds-truncatable [id]="dso.id">
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" <a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer"
[routerLink]="[itemPageRoute]" class="lead item-list-title" [routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out"
[innerHTML]="dsoTitle"></a> [innerHTML]="dsoTitle"></a>
<span *ngIf="linkType == linkTypes.None" <span *ngIf="linkType == linkTypes.None"
class="lead item-list-title" class="lead item-list-title dont-break-out"
[innerHTML]="dsoTitle"></span> [innerHTML]="dsoTitle"></span>
<span class="text-muted"> <span class="text-muted">
<ds-truncatable-part [id]="dso.id" [minLines]="1"> <ds-truncatable-part [id]="dso.id" [minLines]="1">

View File

@@ -54,7 +54,7 @@ const ENTRY_COMPONENTS = [
export class JournalEntitiesModule { export class JournalEntitiesModule {
/** /**
* NOTE: this method allows to resolve issue with components that using a custom decorator * NOTE: this method allows to resolve issue with components that using a custom decorator
* which are not loaded during CSR otherwise * which are not loaded during SSR otherwise
*/ */
static withEntryComponents() { static withEntryComponents() {
return { return {

View File

@@ -1,10 +1,10 @@
<ds-truncatable [id]="dso.id"> <ds-truncatable [id]="dso.id">
<ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge> <ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge>
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" <a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer"
[routerLink]="[itemPageRoute]" class="lead item-list-title" [routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out"
[innerHTML]="dsoTitle"></a> [innerHTML]="dsoTitle"></a>
<span *ngIf="linkType == linkTypes.None" <span *ngIf="linkType == linkTypes.None"
class="lead item-list-title" class="lead item-list-title dont-break-out"
[innerHTML]="dsoTitle"></span> [innerHTML]="dsoTitle"></span>
<!--<span class="text-muted">--> <!--<span class="text-muted">-->
<!--<ds-truncatable-part [id]="dso.id" [minLines]="1">--> <!--<ds-truncatable-part [id]="dso.id" [minLines]="1">-->

View File

@@ -74,7 +74,7 @@ const COMPONENTS = [
export class ResearchEntitiesModule { export class ResearchEntitiesModule {
/** /**
* NOTE: this method allows to resolve issue with components that using a custom decorator * NOTE: this method allows to resolve issue with components that using a custom decorator
* which are not loaded during CSR otherwise * which are not loaded during SSR otherwise
*/ */
static withEntryComponents() { static withEntryComponents() {
return { return {

View File

@@ -1,18 +1,24 @@
<ng-template #bitstreamView> <ng-template #bitstreamView>
<div class="{{columnSizes.columns[0].buildClasses()}} row-element d-flex"> <div class="{{columnSizes.columns[0].buildClasses()}} row-element d-flex">
<ng-content select="[slot=drag-handle]"></ng-content> <ng-content select="[slot=drag-handle]"></ng-content>
<div class="float-left d-flex align-items-center"> <div class="float-left d-flex align-items-center overflow-hidden">
<span class="text-truncate">
{{ bitstreamName }} {{ bitstreamName }}
</span>
</div> </div>
</div> </div>
<div class="{{columnSizes.columns[1].buildClasses()}} row-element d-flex align-items-center"> <div class="{{columnSizes.columns[1].buildClasses()}} row-element d-flex align-items-center">
<div class="w-100"> <div class="w-100">
<span class="text-truncate">
{{ bitstream?.firstMetadataValue('dc.description') }} {{ bitstream?.firstMetadataValue('dc.description') }}
</span>
</div> </div>
</div> </div>
<div class="{{columnSizes.columns[2].buildClasses()}} row-element d-flex align-items-center"> <div class="{{columnSizes.columns[2].buildClasses()}} row-element d-flex align-items-center">
<div class="text-center w-100"> <div class="text-center w-100">
<span class="text-truncate">
{{ (format$ | async)?.shortDescription }} {{ (format$ | async)?.shortDescription }}
</span>
</div> </div>
</div> </div>
<div class="{{columnSizes.columns[3].buildClasses()}} row-element d-flex align-items-center"> <div class="{{columnSizes.columns[3].buildClasses()}} row-element d-flex align-items-center">

View File

@@ -5,9 +5,10 @@
<p [innerHTML]="'item.edit.item-mapper.item' | translate:{ name: (itemName$ | async) }" id="item-name"></p> <p [innerHTML]="'item.edit.item-mapper.item' | translate:{ name: (itemName$ | async) }" id="item-name"></p>
<p>{{'item.edit.item-mapper.description' | translate}}</p> <p>{{'item.edit.item-mapper.description' | translate}}</p>
<ngb-tabset (tabChange)="tabChange($event)" [destroyOnHide]="true" #tabs="ngbTabset"> <ul ngbNav (navChange)="tabChange($event)" [destroyOnHide]="true" #tabs="ngbNav" class="nav-tabs">
<ngb-tab title="{{'item.edit.item-mapper.tabs.browse' | translate}}" id="browseTab"> <li [ngbNavItem]="'browseTab'">
<ng-template ngbTabContent> <a ngbNavLink>{{'item.edit.item-mapper.tabs.browse' | translate}}</a>
<ng-template ngbNavContent>
<div class="mt-2"> <div class="mt-2">
<ds-collection-select class="mt-2" <ds-collection-select class="mt-2"
[key]="'browse'" [key]="'browse'"
@@ -20,9 +21,10 @@
(cancel)="onCancel()"></ds-collection-select> (cancel)="onCancel()"></ds-collection-select>
</div> </div>
</ng-template> </ng-template>
</ngb-tab> </li>
<ngb-tab title="{{'item.edit.item-mapper.tabs.map' | translate}}" id="mapTab"> <li [ngbNavItem]="'mapTab'">
<ng-template ngbTabContent> <a ngbNavLink>{{'item.edit.item-mapper.tabs.map' | translate}}</a>
<ng-template ngbNavContent>
<div class="row mt-2"> <div class="row mt-2">
<div class="col-12 col-lg-6"> <div class="col-12 col-lg-6">
<ds-search-form id="search-form" <ds-search-form id="search-form"
@@ -50,8 +52,9 @@
{{'item.edit.item-mapper.no-search' | translate}} {{'item.edit.item-mapper.no-search' | translate}}
</div> </div>
</ng-template> </ng-template>
</ngb-tab> </li>
</ngb-tabset> </ul>
<div [ngbNavOutlet]="tabs"></div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -7,7 +7,7 @@ import { ActivatedRoute, Router } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing'; import { RouterTestingModule } from '@angular/router/testing';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { of as observableOf } from 'rxjs/internal/observable/of'; import { of as observableOf } from 'rxjs';
import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model'; import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model';
import { CollectionDataService } from '../../../core/data/collection-data.service'; import { CollectionDataService } from '../../../core/data/collection-data.service';
import { ItemDataService } from '../../../core/data/item-data.service'; import { ItemDataService } from '../../../core/data/item-data.service';

View File

@@ -26,7 +26,7 @@
<td class="w-100"> <td class="w-100">
<div class="value-field"> <div class="value-field">
<div *ngIf="!(editable | async)"> <div *ngIf="!(editable | async)">
<span>{{metadata?.value}}</span> <span class="dont-break-out">{{metadata?.value}}</span>
</div> </div>
<div *ngIf="(editable | async)" class="field-container"> <div *ngIf="(editable | async)" class="field-container">
<textarea class="form-control" type="textarea" attr.aria-labelledby="fieldValue" [(ngModel)]="metadata.value" [dsDebounce] <textarea class="form-control" type="textarea" attr.aria-labelledby="fieldValue" [(ngModel)]="metadata.value" [dsDebounce]

View File

@@ -3,7 +3,7 @@ import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
import { LinkService } from '../../../../core/cache/builders/link.service'; import { LinkService } from '../../../../core/cache/builders/link.service';
import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions'; import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions';
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
import { combineLatest as observableCombineLatest, from as observableFrom, Observable } from 'rxjs'; import { combineLatest as observableCombineLatest, from as observableFrom, BehaviorSubject, Observable, Subscription } from 'rxjs';
import { import {
FieldUpdate, FieldUpdate,
FieldUpdates, FieldUpdates,
@@ -30,8 +30,6 @@ import { followLink } from '../../../../shared/utils/follow-link-config.model';
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 { Collection } from '../../../../core/shared/collection.model'; import { Collection } from '../../../../core/shared/collection.model';
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
import { Subscription } from 'rxjs/internal/Subscription';
import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model'; import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model';
import { PaginationService } from '../../../../core/pagination/pagination.service'; import { PaginationService } from '../../../../core/pagination/pagination.service';
import { RelationshipTypeService } from '../../../../core/data/relationship-type.service'; import { RelationshipTypeService } from '../../../../core/data/relationship-type.service';

View File

@@ -6,9 +6,8 @@ import {
FieldUpdates, FieldUpdates,
RelationshipIdentifiable, RelationshipIdentifiable,
} from '../../../core/data/object-updates/object-updates.reducer'; } from '../../../core/data/object-updates/object-updates.reducer';
import { Observable } from 'rxjs/internal/Observable';
import { map, startWith, switchMap, take } from 'rxjs/operators'; import { map, startWith, switchMap, take } from 'rxjs/operators';
import { combineLatest as observableCombineLatest, of as observableOf, zip as observableZip } from 'rxjs'; import { combineLatest as observableCombineLatest, of as observableOf, zip as observableZip, Observable } from 'rxjs';
import { followLink } from '../../../shared/utils/follow-link-config.model'; import { followLink } from '../../../shared/utils/follow-link-config.model';
import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component'; import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component';
import { ItemDataService } from '../../../core/data/item-data.service'; import { ItemDataService } from '../../../core/data/item-data.service';

View File

@@ -1,5 +1,5 @@
<ds-metadata-field-wrapper [label]="label | translate"> <ds-metadata-field-wrapper [label]="label | translate">
<a *ngFor="let mdValue of mdValues; let last=last;" [href]="mdValue.value"> <a class="dont-break-out" *ngFor="let mdValue of mdValues; let last=last;" [href]="mdValue.value">
{{ linktext || mdValue.value }}<span *ngIf="!last" [innerHTML]="separator"></span> {{ linktext || mdValue.value }}<span *ngIf="!last" [innerHTML]="separator"></span>
</a> </a>
</ds-metadata-field-wrapper> </ds-metadata-field-wrapper>

View File

@@ -1,5 +1,5 @@
<ds-metadata-field-wrapper [label]="label | translate"> <ds-metadata-field-wrapper [label]="label | translate">
<span *ngFor="let mdValue of mdValues; let last=last;"> <span class="dont-break-out" *ngFor="let mdValue of mdValues; let last=last;">
{{mdValue.value}}<span *ngIf="!last" [innerHTML]="separator"></span> {{mdValue.value}}<span *ngIf="!last" [innerHTML]="separator"></span>
</span> </span>
</ds-metadata-field-wrapper> </ds-metadata-field-wrapper>

View File

@@ -33,7 +33,7 @@ import { NgxGalleryModule } from '@kolkov/ngx-gallery';
import { MiradorViewerComponent } from './mirador-viewer/mirador-viewer.component'; import { MiradorViewerComponent } from './mirador-viewer/mirador-viewer.component';
import { VersionPageComponent } from './version-page/version-page/version-page.component'; import { VersionPageComponent } from './version-page/version-page/version-page.component';
import { VersionedItemComponent } from './simple/item-types/versioned-item/versioned-item.component'; import { VersionedItemComponent } from './simple/item-types/versioned-item/versioned-item.component';
import { ThemedFileSectionComponent} from './simple/field-components/file-section/themed-file-section.component'; import { ThemedFileSectionComponent } from './simple/field-components/file-section/themed-file-section.component';
const ENTRY_COMPONENTS = [ const ENTRY_COMPONENTS = [
@@ -91,7 +91,7 @@ const DECLARATIONS = [
export class ItemPageModule { export class ItemPageModule {
/** /**
* NOTE: this method allows to resolve issue with components that using a custom decorator * NOTE: this method allows to resolve issue with components that using a custom decorator
* which are not loaded during CSR otherwise * which are not loaded during SSR otherwise
*/ */
static withEntryComponents() { static withEntryComponents() {
return { return {

View File

@@ -1,5 +1,5 @@
import { NO_ERRORS_SCHEMA } from '@angular/core'; import { NO_ERRORS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { NgxGalleryOptions } from '@kolkov/ngx-gallery'; import { NgxGalleryOptions } from '@kolkov/ngx-gallery';
import { Bitstream } from '../../../core/shared/bitstream.model'; import { Bitstream } from '../../../core/shared/bitstream.model';
import { MediaViewerItem } from '../../../core/shared/media-viewer-item.model'; import { MediaViewerItem } from '../../../core/shared/media-viewer-item.model';
@@ -55,7 +55,7 @@ describe('MediaViewerImageComponent', () => {
] ]
); );
beforeEach(async(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports:[], imports:[],
declarations: [MediaViewerImageComponent], declarations: [MediaViewerImageComponent],

View File

@@ -1,5 +1,5 @@
import { NO_ERRORS_SCHEMA } from '@angular/core'; import { NO_ERRORS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { of as observableOf } from 'rxjs'; import { of as observableOf } from 'rxjs';
@@ -17,7 +17,7 @@ describe('MediaViewerVideoComponent', () => {
let component: MediaViewerVideoComponent; let component: MediaViewerVideoComponent;
let fixture: ComponentFixture<MediaViewerVideoComponent>; let fixture: ComponentFixture<MediaViewerVideoComponent>;
beforeEach(async(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ imports: [
TranslateModule.forRoot({ TranslateModule.forRoot({

View File

@@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { Bitstream } from '../../core/shared/bitstream.model'; import { Bitstream } from '../../core/shared/bitstream.model';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { createPaginatedList } from '../../shared/testing/utils.test'; import { createPaginatedList } from '../../shared/testing/utils.test';
@@ -60,7 +60,7 @@ describe('MediaViewerComponent', () => {
{ bitstream: mockBitstream, format: 'image', thumbnail: null } { bitstream: mockBitstream, format: 'image', thumbnail: null }
); );
beforeEach(async(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ imports: [
TranslateModule.forRoot({ TranslateModule.forRoot({

View File

@@ -3,9 +3,8 @@ import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
import { Item } from '../../core/shared/item.model'; import { Item } from '../../core/shared/item.model';
import { environment } from '../../../environments/environment'; import { environment } from '../../../environments/environment';
import { BitstreamDataService } from '../../core/data/bitstream-data.service'; import { BitstreamDataService } from '../../core/data/bitstream-data.service';
import { Observable } from 'rxjs/internal/Observable'; import { Observable, of } from 'rxjs';
import { map, take } from 'rxjs/operators'; import { map, take } from 'rxjs/operators';
import { of } from 'rxjs';
import { isPlatformBrowser } from '@angular/common'; import { isPlatformBrowser } from '@angular/common';
import { MiradorViewerService } from './mirador-viewer.service'; import { MiradorViewerService } from './mirador-viewer.service';
import { HostWindowService, WidthCategory } from '../../shared/host-window.service'; import { HostWindowService, WidthCategory } from '../../shared/host-window.service';

View File

@@ -1,5 +1,5 @@
import { Injectable, isDevMode } from '@angular/core'; import { Injectable, isDevMode } from '@angular/core';
import { Observable } from 'rxjs/internal/Observable'; import { Observable } from 'rxjs';
import { Item } from '../../core/shared/item.model'; import { Item } from '../../core/shared/item.model';
import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../../core/shared/operators'; import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../../core/shared/operators';
import { last, map, switchMap } from 'rxjs/operators'; import { last, map, switchMap } from 'rxjs/operators';

View File

@@ -4,7 +4,7 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { Observable } from 'rxjs/internal/Observable'; import { Observable } from 'rxjs';
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';
import { BitstreamDataService } from '../../../../core/data/bitstream-data.service'; import { BitstreamDataService } from '../../../../core/data/bitstream-data.service';

View File

@@ -1,6 +1,8 @@
<ngb-tabset *ngIf="relationTypes.length > 1" [destroyOnHide]="true" #tabs="ngbTabset" [activeId]="activeTab$ | async" (tabChange)="onTabChange($event)"> <ng-container *ngIf="relationTypes.length > 1">
<ngb-tab *ngFor="let relationType of relationTypes" title="{{'item.page.relationships.' + relationType.label | translate}}" [id]="relationType.filter"> <ul ngbNav #tabs="ngbNav" [destroyOnHide]="true" [activeId]="activeTab$ | async" (navChange)="onTabChange($event)" class="nav-tabs">
<ng-template ngbTabContent> <li *ngFor="let relationType of relationTypes" [ngbNavItem]="relationType.filter">
<a ngbNavLink>{{'item.page.relationships.' + relationType.label | translate}}</a>
<ng-template ngbNavContent>
<div class="mt-4"> <div class="mt-4">
<ds-related-entities-search [item]="item" <ds-related-entities-search [item]="item"
[relationType]="relationType.filter" [relationType]="relationType.filter"
@@ -10,8 +12,10 @@
</ds-related-entities-search> </ds-related-entities-search>
</div> </div>
</ng-template> </ng-template>
</ngb-tab> </li>
</ngb-tabset> </ul>
<div [ngbNavOutlet]="tabs"></div>
</ng-container>
<div *ngIf="relationTypes.length === 1" class="mt-4"> <div *ngIf="relationTypes.length === 1" class="mt-4">
<ds-related-entities-search *ngVar="relationTypes[0] as relationType" [item]="item" <ds-related-entities-search *ngVar="relationTypes[0] as relationType" [item]="item"
[relationType]="relationType.filter" [relationType]="relationType.filter"

View File

@@ -1,5 +1,5 @@
import { Component, DebugElement, NO_ERRORS_SCHEMA } from '@angular/core'; import { Component, DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing'; import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
@@ -75,7 +75,7 @@ describe('MyDSpaceNewExternalDropdownComponent test', () => {
}; };
describe('With only one Entity', () => { describe('With only one Entity', () => {
beforeEach(async(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ imports: [
CommonModule, CommonModule,
@@ -126,7 +126,7 @@ describe('MyDSpaceNewExternalDropdownComponent test', () => {
}); });
describe('With more than one Entity', () => { describe('With more than one Entity', () => {
beforeEach(async(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ imports: [
CommonModule, CommonModule,

View File

@@ -1,5 +1,5 @@
import { Component, DebugElement, NO_ERRORS_SCHEMA } from '@angular/core'; import { Component, DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing'; import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
@@ -79,7 +79,7 @@ describe('MyDSpaceNewSubmissionDropdownComponent test', () => {
}; };
describe('With only one Entity', () => { describe('With only one Entity', () => {
beforeEach(async(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ imports: [
CommonModule, CommonModule,
@@ -130,7 +130,7 @@ describe('MyDSpaceNewSubmissionDropdownComponent test', () => {
}); });
describe('With more than one Entity', () => { describe('With more than one Entity', () => {
beforeEach(async(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ imports: [
CommonModule, CommonModule,

View File

@@ -4,7 +4,7 @@
<ds-search-sidebar *ngIf="!(isXsOrSm$ | async)" class="col-3 sidebar-md-sticky" <ds-search-sidebar *ngIf="!(isXsOrSm$ | async)" class="col-3 sidebar-md-sticky"
id="search-sidebar" id="search-sidebar"
[configurationList]="(configurationList$ | async)" [configurationList]="(configurationList$ | async)"
[resultCount]="(resultsRD$ | async)?.payload.totalElements" [resultCount]="(resultsRD$ | async)?.payload?.totalElements"
[viewModeList]="viewModeList" [viewModeList]="viewModeList"
[searchOptions]="(searchOptions$ | async)" [searchOptions]="(searchOptions$ | async)"
[sortOptions]="(sortOptions$ | async)" [sortOptions]="(sortOptions$ | async)"
@@ -27,7 +27,7 @@
<ds-search-sidebar *ngIf="(isXsOrSm$ | async)" class="col-12" <ds-search-sidebar *ngIf="(isXsOrSm$ | async)" class="col-12"
id="search-sidebar-sm" id="search-sidebar-sm"
[configurationList]="(configurationList$ | async)" [configurationList]="(configurationList$ | async)"
[resultCount]="(resultsRD$ | async)?.payload.totalElements" [resultCount]="(resultsRD$ | async)?.payload?.totalElements"
(toggleSidebar)="closeSidebar()" (toggleSidebar)="closeSidebar()"
[ngClass]="{'active': !(isSidebarCollapsed() | async)}" [ngClass]="{'active': !(isSidebarCollapsed() | async)}"
[searchOptions]="(searchOptions$ | async)" [searchOptions]="(searchOptions$ | async)"

View File

@@ -19,7 +19,7 @@ import { PaginatedSearchOptions } from '../shared/search/paginated-search-option
import { SearchService } from '../core/shared/search/search.service'; import { SearchService } from '../core/shared/search/search.service';
import { SidebarService } from '../shared/sidebar/sidebar.service'; import { SidebarService } from '../shared/sidebar/sidebar.service';
import { hasValue } from '../shared/empty.util'; import { hasValue } from '../shared/empty.util';
import { getFirstSucceededRemoteData } from '../core/shared/operators'; import { getFirstCompletedRemoteData } from '../core/shared/operators';
import { MyDSpaceResponseParsingService } from '../core/data/mydspace-response-parsing.service'; import { MyDSpaceResponseParsingService } from '../core/data/mydspace-response-parsing.service';
import { SearchConfigurationOption } from '../shared/search/search-switch-configuration/search-configuration-option.model'; import { SearchConfigurationOption } from '../shared/search/search-switch-configuration/search-configuration-option.model';
import { RoleType } from '../core/roles/role-types'; import { RoleType } from '../core/roles/role-types';
@@ -30,7 +30,7 @@ import { MyDSpaceRequest } from '../core/data/request.models';
import { SearchResult } from '../shared/search/search-result.model'; import { SearchResult } from '../shared/search/search-result.model';
import { Context } from '../core/shared/context.model'; import { Context } from '../core/shared/context.model';
import { SortOptions } from '../core/cache/models/sort-options.model'; import { SortOptions } from '../core/cache/models/sort-options.model';
import { RouteService } from '../core/services/route.service'; import { SearchObjects } from '../shared/search/search-objects.model';
export const MYDSPACE_ROUTE = '/mydspace'; export const MYDSPACE_ROUTE = '/mydspace';
export const SEARCH_CONFIG_SERVICE: InjectionToken<SearchConfigurationService> = new InjectionToken<SearchConfigurationService>('searchConfigurationService'); export const SEARCH_CONFIG_SERVICE: InjectionToken<SearchConfigurationService> = new InjectionToken<SearchConfigurationService>('searchConfigurationService');
@@ -111,8 +111,7 @@ export class MyDSpacePageComponent implements OnInit {
constructor(private service: SearchService, constructor(private service: SearchService,
private sidebarService: SidebarService, private sidebarService: SidebarService,
private windowService: HostWindowService, private windowService: HostWindowService,
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: MyDSpaceConfigurationService, @Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: MyDSpaceConfigurationService) {
private routeService: RouteService) {
this.isXsOrSm$ = this.windowService.isXsOrSm(); this.isXsOrSm$ = this.windowService.isXsOrSm();
this.service.setServiceOptions(MyDSpaceResponseParsingService, MyDSpaceRequest); this.service.setServiceOptions(MyDSpaceResponseParsingService, MyDSpaceRequest);
} }
@@ -134,8 +133,8 @@ export class MyDSpacePageComponent implements OnInit {
this.searchOptions$ = this.searchConfigService.paginatedSearchOptions; this.searchOptions$ = this.searchConfigService.paginatedSearchOptions;
this.sub = this.searchOptions$.pipe( this.sub = this.searchOptions$.pipe(
tap(() => this.resultsRD$.next(null)), tap(() => this.resultsRD$.next(null)),
switchMap((options: PaginatedSearchOptions) => this.service.search(options).pipe(getFirstSucceededRemoteData()))) switchMap((options: PaginatedSearchOptions) => this.service.search(options).pipe(getFirstCompletedRemoteData())))
.subscribe((results) => { .subscribe((results: RemoteData<SearchObjects<DSpaceObject>>) => {
this.resultsRD$.next(results); this.resultsRD$.next(results);
}); });

View File

@@ -10,5 +10,5 @@
</ds-viewable-collection> </ds-viewable-collection>
</div> </div>
<ds-loading *ngIf="isLoading()" message="{{'loading.mydspace-results' | translate}}"></ds-loading> <ds-loading *ngIf="isLoading()" message="{{'loading.mydspace-results' | translate}}"></ds-loading>
<ds-error *ngIf="searchResults?.hasFailed && (!searchResults?.errorMessage || searchResults?.statusCode != 400)" message="{{'error.search-results' | translate}}"></ds-error> <ds-error *ngIf="showError()" message="{{errorMessageLabel() | translate}}"></ds-error>
<h3 *ngIf="searchResults?.payload?.page.length == 0" class="text-center text-muted" ><span>{{'mydspace.results.no-results' | translate}}</span></h3> <h3 *ngIf="searchResults?.payload?.page.length == 0" class="text-center text-muted" ><span>{{'mydspace.results.no-results' | translate}}</span></h3>

View File

@@ -40,9 +40,19 @@ describe('MyDSpaceResultsComponent', () => {
expect(fixture.debugElement.query(By.css('a'))).toBeNull(); expect(fixture.debugElement.query(By.css('a'))).toBeNull();
}); });
it('should display error message if error is != 400', () => { it('should display error message if error is 500', () => {
(comp as any).searchResults = { hasFailed: true, error: { statusCode: 500 } }; (comp as any).searchResults = { hasFailed: true, statusCode: 500 };
fixture.detectChanges(); fixture.detectChanges();
expect(comp.showError()).toBeTrue();
expect(comp.errorMessageLabel()).toBe('error.search-results');
expect(fixture.debugElement.query(By.css('ds-error'))).not.toBeNull();
});
it('should display error message if error is 422', () => {
(comp as any).searchResults = { hasFailed: true, statusCode: 422 };
fixture.detectChanges();
expect(comp.showError()).toBeTrue();
expect(comp.errorMessageLabel()).toBe('error.invalid-search-query');
expect(fixture.debugElement.query(By.css('ds-error'))).not.toBeNull(); expect(fixture.debugElement.query(By.css('ds-error'))).not.toBeNull();
}); });

View File

@@ -58,4 +58,12 @@ export class MyDSpaceResultsComponent {
isLoading() { isLoading() {
return !this.searchResults || isEmpty(this.searchResults) || this.searchResults.isLoading; return !this.searchResults || isEmpty(this.searchResults) || this.searchResults.isLoading;
} }
showError(): boolean {
return this.searchResults?.hasFailed && (!this.searchResults?.errorMessage || this.searchResults?.statusCode !== 400);
}
errorMessageLabel(): string {
return (this.searchResults?.statusCode === 422) ? 'error.invalid-search-query' : 'error.search-results';
}
} }

View File

@@ -50,7 +50,7 @@ const ENTRY_COMPONENTS = [
export class MyDspaceSearchModule { export class MyDspaceSearchModule {
/** /**
* NOTE: this method allows to resolve issue with components that using a custom decorator * NOTE: this method allows to resolve issue with components that using a custom decorator
* which are not loaded during CSR otherwise * which are not loaded during SSR otherwise
*/ */
static withEntryComponents() { static withEntryComponents() {
return { return {

View File

@@ -58,7 +58,7 @@ const ENTRY_COMPONENTS = [
export class NavbarModule { export class NavbarModule {
/** /**
* NOTE: this method allows to resolve issue with components that using a custom decorator * NOTE: this method allows to resolve issue with components that using a custom decorator
* which are not loaded during CSR otherwise * which are not loaded during SSR otherwise
*/ */
static withEntryComponents() { static withEntryComponents() {
return { return {

View File

@@ -2,7 +2,7 @@
<div class="d-flex"> <div class="d-flex">
<h2 class="flex-grow-1">{{'process.detail.title' | translate:{id: process?.processId, name: process?.scriptName} }}</h2> <h2 class="flex-grow-1">{{'process.detail.title' | translate:{id: process?.processId, name: process?.scriptName} }}</h2>
<div> <div>
<a class="btn btn-light" [routerLink]="'/processes/new'" [queryParams]="{id: process?.processId}">{{'process.detail.create' | translate}}</a> <button class="btn btn-lg btn-success " routerLink="/processes/new" [queryParams]="{id: process?.processId}"><i class="fas fa-plus pr-2"></i>{{'process.detail.create' | translate}}</button>
</div> </div>
</div> </div>
<ds-process-detail-field id="process-name" [title]="'process.detail.script'"> <ds-process-detail-field id="process-name" [title]="'process.detail.script'">
@@ -23,11 +23,11 @@
</div> </div>
<ds-process-detail-field *ngIf="process && process.startTime" id="process-start-time" [title]="'process.detail.start-time' | translate"> <ds-process-detail-field *ngIf="process && process.startTime" id="process-start-time" [title]="'process.detail.start-time' | translate">
<div>{{ process.startTime }}</div> <div>{{ process.startTime | date:dateFormat:'UTC' }}</div>
</ds-process-detail-field> </ds-process-detail-field>
<ds-process-detail-field *ngIf="process && process.endTime" id="process-end-time" [title]="'process.detail.end-time' | translate"> <ds-process-detail-field *ngIf="process && process.endTime" id="process-end-time" [title]="'process.detail.end-time' | translate">
<div>{{ process.endTime }}</div> <div>{{ process.endTime | date:dateFormat:'UTC' }}</div>
</ds-process-detail-field> </ds-process-detail-field>
<ds-process-detail-field *ngIf="process && process.processStatus" id="process-status" [title]="'process.detail.status' | translate"> <ds-process-detail-field *ngIf="process && process.processStatus" id="process-status" [title]="'process.detail.status' | translate">
@@ -35,7 +35,7 @@
</ds-process-detail-field> </ds-process-detail-field>
<ds-process-detail-field *ngIf="isProcessFinished(process)" id="process-output" [title]="'process.detail.output'"> <ds-process-detail-field *ngIf="isProcessFinished(process)" id="process-output" [title]="'process.detail.output'">
<button *ngIf="!showOutputLogs && process?._links?.output?.href != undefined" id="showOutputButton" class="btn btn-light" (click)="showProcessOutputLogs()"> <button *ngIf="!showOutputLogs && process?._links?.output?.href != undefined" id="showOutputButton" class="btn btn-primary" (click)="showProcessOutputLogs()">
{{ 'process.detail.logs.button' | translate }} {{ 'process.detail.logs.button' | translate }}
</button> </button>
<ds-loading *ngIf="retrievingOutputLogs$ | async" class="ds-loading" message="{{ 'process.detail.logs.loading' | translate }}"></ds-loading> <ds-loading *ngIf="retrievingOutputLogs$ | async" class="ds-loading" message="{{ 'process.detail.logs.loading' | translate }}"></ds-loading>
@@ -47,7 +47,7 @@
</p> </p>
</ds-process-detail-field> </ds-process-detail-field>
<div> <div style="text-align: right;">
<a class="btn btn-light mt-3" [routerLink]="'/processes'">{{'process.detail.back' | translate}}</a> <a class="btn btn-outline-secondary mt-3" [routerLink]="'/processes'">{{'process.detail.back' | translate}}</a>
</div> </div>
</div> </div>

View File

@@ -66,6 +66,11 @@ export class ProcessDetailComponent implements OnInit {
*/ */
retrievingOutputLogs$: BehaviorSubject<boolean>; retrievingOutputLogs$: BehaviorSubject<boolean>;
/**
* Date format to use for start and end time of processes
*/
dateFormat = 'yyyy-MM-dd HH:mm:ss ZZZZ';
constructor(protected route: ActivatedRoute, constructor(protected route: ActivatedRoute,
protected router: Router, protected router: Router,
protected processService: ProcessDataService, protected processService: ProcessDataService,

View File

@@ -26,8 +26,8 @@
<td><a [routerLink]="['/processes/', process.processId]">{{process.processId}}</a></td> <td><a [routerLink]="['/processes/', process.processId]">{{process.processId}}</a></td>
<td><a [routerLink]="['/processes/', process.processId]">{{process.scriptName}}</a></td> <td><a [routerLink]="['/processes/', process.processId]">{{process.scriptName}}</a></td>
<td *ngVar="(getEpersonName(process.userId) | async) as ePersonName">{{ePersonName}}</td> <td *ngVar="(getEpersonName(process.userId) | async) as ePersonName">{{ePersonName}}</td>
<td>{{process.startTime | date:dateFormat}}</td> <td>{{process.startTime | date:dateFormat:'UTC'}}</td>
<td>{{process.endTime | date:dateFormat}}</td> <td>{{process.endTime | date:dateFormat:'UTC'}}</td>
<td>{{process.processStatus}}</td> <td>{{process.processStatus}}</td>
</tr> </tr>
</tbody> </tbody>

View File

@@ -12,12 +12,9 @@ import { By } from '@angular/platform-browser';
import { ProcessStatus } from '../processes/process-status.model'; import { ProcessStatus } from '../processes/process-status.model';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { createPaginatedList } from '../../shared/testing/utils.test'; import { createPaginatedList } from '../../shared/testing/utils.test';
import { of as observableOf } from 'rxjs';
import { PaginationService } from '../../core/pagination/pagination.service'; import { PaginationService } from '../../core/pagination/pagination.service';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { FindListOptions } from '../../core/data/request.models';
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub'; import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
import { DatePipe } from '@angular/common';
describe('ProcessOverviewComponent', () => { describe('ProcessOverviewComponent', () => {
let component: ProcessOverviewComponent; let component: ProcessOverviewComponent;
@@ -30,27 +27,29 @@ describe('ProcessOverviewComponent', () => {
let processes: Process[]; let processes: Process[];
let ePerson: EPerson; let ePerson: EPerson;
const pipe = new DatePipe('en-US');
function init() { function init() {
processes = [ processes = [
Object.assign(new Process(), { Object.assign(new Process(), {
processId: 1, processId: 1,
scriptName: 'script-name', scriptName: 'script-name',
startTime: '2020-03-19', startTime: '2020-03-19 00:30:00',
endTime: '2020-03-19', endTime: '2020-03-19 23:30:00',
processStatus: ProcessStatus.COMPLETED processStatus: ProcessStatus.COMPLETED
}), }),
Object.assign(new Process(), { Object.assign(new Process(), {
processId: 2, processId: 2,
scriptName: 'script-name', scriptName: 'script-name',
startTime: '2020-03-20', startTime: '2020-03-20 00:30:00',
endTime: '2020-03-20', endTime: '2020-03-20 23:30:00',
processStatus: ProcessStatus.FAILED processStatus: ProcessStatus.FAILED
}), }),
Object.assign(new Process(), { Object.assign(new Process(), {
processId: 3, processId: 3,
scriptName: 'another-script-name', scriptName: 'another-script-name',
startTime: '2020-03-21', startTime: '2020-03-21 00:30:00',
endTime: '2020-03-21', endTime: '2020-03-21 23:30:00',
processStatus: ProcessStatus.RUNNING processStatus: ProcessStatus.RUNNING
}) })
]; ];
@@ -135,14 +134,14 @@ describe('ProcessOverviewComponent', () => {
it('should display the start time in the fourth column', () => { it('should display the start time in the fourth column', () => {
rowElements.forEach((rowElement, index) => { rowElements.forEach((rowElement, index) => {
const el = rowElement.query(By.css('td:nth-child(4)')).nativeElement; const el = rowElement.query(By.css('td:nth-child(4)')).nativeElement;
expect(el.textContent).toContain(processes[index].startTime); expect(el.textContent).toContain(pipe.transform(processes[index].startTime, component.dateFormat, 'UTC'));
}); });
}); });
it('should display the end time in the fifth column', () => { it('should display the end time in the fifth column', () => {
rowElements.forEach((rowElement, index) => { rowElements.forEach((rowElement, index) => {
const el = rowElement.query(By.css('td:nth-child(5)')).nativeElement; const el = rowElement.query(By.css('td:nth-child(5)')).nativeElement;
expect(el.textContent).toContain(processes[index].endTime); expect(el.textContent).toContain(pipe.transform(processes[index].endTime, component.dateFormat, 'UTC'));
}); });
}); });

View File

@@ -23,7 +23,7 @@ export class ProcessPageResolver implements Resolve<RemoteData<Process>> {
* or an error if something went wrong * or an error if something went wrong
*/ */
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Process>> { resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Process>> {
return this.processService.findById(route.params.id, true, false, followLink('script')).pipe( return this.processService.findById(route.params.id, false, true, followLink('script')).pipe(
getFirstCompletedRemoteData(), getFirstCompletedRemoteData(),
); );
} }

View File

@@ -2,7 +2,7 @@ import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { map, switchMap } from 'rxjs/operators'; import { map, switchMap } from 'rxjs/operators';
import { ItemRequest } from '../../core/shared/item-request.model'; import { ItemRequest } from '../../core/shared/item-request.model';
import { Observable } from 'rxjs/internal/Observable'; import { Observable } from 'rxjs';
import { import {
getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload, getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload,
redirectOn4xx redirectOn4xx

View File

@@ -2,7 +2,7 @@ import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { map, switchMap } from 'rxjs/operators'; import { map, switchMap } from 'rxjs/operators';
import { ItemRequest } from '../../core/shared/item-request.model'; import { ItemRequest } from '../../core/shared/item-request.model';
import { Observable } from 'rxjs/internal/Observable'; import { Observable } from 'rxjs';
import { import {
getFirstCompletedRemoteData, getFirstCompletedRemoteData,
getFirstSucceededRemoteDataPayload, getFirstSucceededRemoteDataPayload,

View File

@@ -2,7 +2,7 @@ import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { map, switchMap } from 'rxjs/operators'; import { map, switchMap } from 'rxjs/operators';
import { ItemRequest } from '../../core/shared/item-request.model'; import { ItemRequest } from '../../core/shared/item-request.model';
import { Observable } from 'rxjs/internal/Observable'; import { Observable } from 'rxjs';
import { import {
getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload, getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload,
redirectOn4xx redirectOn4xx

View File

@@ -1,7 +1,7 @@
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router'; import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
import { RemoteData } from '../core/data/remote-data'; import { RemoteData } from '../core/data/remote-data';
import { ItemRequest } from '../core/shared/item-request.model'; import { ItemRequest } from '../core/shared/item-request.model';
import { Observable } from 'rxjs/internal/Observable'; import { Observable } from 'rxjs';
import { ItemRequestDataService } from '../core/data/item-request-data.service'; import { ItemRequestDataService } from '../core/data/item-request-data.service';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { getFirstCompletedRemoteData } from '../core/shared/operators'; import { getFirstCompletedRemoteData } from '../core/shared/operators';

View File

@@ -8,7 +8,7 @@ import { pushInOut } from '../shared/animations/push';
import { HostWindowService } from '../shared/host-window.service'; import { HostWindowService } from '../shared/host-window.service';
import { SidebarService } from '../shared/sidebar/sidebar.service'; import { SidebarService } from '../shared/sidebar/sidebar.service';
import { hasValue, isEmpty } from '../shared/empty.util'; import { hasValue, isEmpty } from '../shared/empty.util';
import { getFirstSucceededRemoteData } from '../core/shared/operators'; import { getFirstCompletedRemoteData } from '../core/shared/operators';
import { RouteService } from '../core/services/route.service'; import { RouteService } from '../core/services/route.service';
import { SEARCH_CONFIG_SERVICE } from '../my-dspace-page/my-dspace-page.component'; import { SEARCH_CONFIG_SERVICE } from '../my-dspace-page/my-dspace-page.component';
import { PaginatedSearchOptions } from '../shared/search/paginated-search-options.model'; import { PaginatedSearchOptions } from '../shared/search/paginated-search-options.model';
@@ -126,8 +126,8 @@ export class SearchComponent implements OnInit {
this.searchOptions$ = this.getSearchOptions(); this.searchOptions$ = this.getSearchOptions();
this.sub = this.searchOptions$.pipe( this.sub = this.searchOptions$.pipe(
switchMap((options) => this.service.search( switchMap((options) => this.service.search(
options, undefined, true, true, followLink<Item>('thumbnail', { isOptional: true }) options, undefined, false, true, followLink<Item>('thumbnail', { isOptional: true })
).pipe(getFirstSucceededRemoteData(), startWith(undefined)) ).pipe(getFirstCompletedRemoteData(), startWith(undefined))
) )
).subscribe((results) => { ).subscribe((results) => {
this.resultsRD$.next(results); this.resultsRD$.next(results);

View File

@@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { AuthService } from '../../core/auth/auth.service'; import { AuthService } from '../../core/auth/auth.service';
import { FileService } from '../../core/shared/file.service'; import { FileService } from '../../core/shared/file.service';
import { of as observableOf } from 'rxjs'; import { of as observableOf } from 'rxjs';
@@ -77,7 +77,7 @@ describe('BitstreamDownloadPageComponent', () => {
} }
describe('init', () => { describe('init', () => {
beforeEach(async(() => { beforeEach(waitForAsync(() => {
init(); init();
initTestbed(); initTestbed();
})); }));
@@ -93,7 +93,7 @@ describe('BitstreamDownloadPageComponent', () => {
describe('bitstream retrieval', () => { describe('bitstream retrieval', () => {
describe('when the user is authorized and not logged in', () => { describe('when the user is authorized and not logged in', () => {
beforeEach(async(() => { beforeEach(waitForAsync(() => {
init(); init();
(authService.isAuthenticated as jasmine.Spy).and.returnValue(observableOf(false)); (authService.isAuthenticated as jasmine.Spy).and.returnValue(observableOf(false));
@@ -109,7 +109,7 @@ describe('BitstreamDownloadPageComponent', () => {
}); });
}); });
describe('when the user is authorized and logged in', () => { describe('when the user is authorized and logged in', () => {
beforeEach(async(() => { beforeEach(waitForAsync(() => {
init(); init();
initTestbed(); initTestbed();
})); }));
@@ -123,7 +123,7 @@ describe('BitstreamDownloadPageComponent', () => {
}); });
}); });
describe('when the user is not authorized and logged in', () => { describe('when the user is not authorized and logged in', () => {
beforeEach(async(() => { beforeEach(waitForAsync(() => {
init(); init();
(authorizationService.isAuthorized as jasmine.Spy).and.returnValue(observableOf(false)); (authorizationService.isAuthorized as jasmine.Spy).and.returnValue(observableOf(false));
initTestbed(); initTestbed();
@@ -138,7 +138,7 @@ describe('BitstreamDownloadPageComponent', () => {
}); });
}); });
describe('when the user is not authorized and not logged in', () => { describe('when the user is not authorized and not logged in', () => {
beforeEach(async(() => { beforeEach(waitForAsync(() => {
init(); init();
(authService.isAuthenticated as jasmine.Spy).and.returnValue(observableOf(false)); (authService.isAuthenticated as jasmine.Spy).and.returnValue(observableOf(false));
(authorizationService.isAuthorized as jasmine.Spy).and.returnValue(observableOf(false)); (authorizationService.isAuthorized as jasmine.Spy).and.returnValue(observableOf(false));

View File

@@ -7,10 +7,9 @@ import { Bitstream } from '../../core/shared/bitstream.model';
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 { AuthService } from '../../core/auth/auth.service'; import { AuthService } from '../../core/auth/auth.service';
import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs'; import { combineLatest as observableCombineLatest, Observable, of as observableOf, Subscription } from 'rxjs';
import { getBitstreamDownloadRoute, getForbiddenRoute } from '../../app-routing-paths'; import { getBitstreamDownloadRoute, getForbiddenRoute } from '../../app-routing-paths';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { Subscription } from 'rxjs/internal/Subscription';
import { EPerson } from '../../core/eperson/models/eperson.model'; import { EPerson } from '../../core/eperson/models/eperson.model';
import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
import { ItemRequestDataService } from '../../core/data/item-request-data.service'; import { ItemRequestDataService } from '../../core/data/item-request-data.service';

View File

@@ -25,7 +25,7 @@
</div> </div>
<ul class="list-unstyled"> <ul class="list-unstyled">
<li *ngFor="let object of objects?.payload?.page" class="mt-4 mb-4"> <li *ngFor="let object of objects?.payload?.page" class="mt-4 mb-4">
<ds-listable-object-component-loader [object]="object"></ds-listable-object-component-loader> <ds-listable-object-component-loader [object]="object" [viewMode]="viewMode"></ds-listable-object-component-loader>
</li> </li>
</ul> </ul>
<div> <div>

View File

@@ -2,7 +2,7 @@ import { BrowseByComponent } from './browse-by.component';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { NO_ERRORS_SCHEMA } from '@angular/core'; import { Component, NO_ERRORS_SCHEMA } from '@angular/core';
import { of as observableOf } from 'rxjs'; import { of as observableOf } from 'rxjs';
import { SharedModule } from '../shared.module'; import { SharedModule } from '../shared.module';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
@@ -18,17 +18,31 @@ import { PaginationComponentOptions } from '../pagination/pagination-component-o
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils';
import { storeModuleConfig } from '../../app.reducer'; import { storeModuleConfig } from '../../app.reducer';
import { FindListOptions } from '../../core/data/request.models';
import { PaginationService } from '../../core/pagination/pagination.service'; import { PaginationService } from '../../core/pagination/pagination.service';
import { PaginationServiceStub } from '../testing/pagination-service.stub'; import { PaginationServiceStub } from '../testing/pagination-service.stub';
import { ListableObjectComponentLoaderComponent } from '../object-collection/shared/listable-object/listable-object-component-loader.component';
import { ViewMode } from '../../core/shared/view-mode.model';
import { BrowseEntryListElementComponent } from '../object-list/browse-entry-list-element/browse-entry-list-element.component';
import {
DEFAULT_CONTEXT,
listableObjectComponent,
} from '../object-collection/shared/listable-object/listable-object.decorator';
import { BrowseEntry } from '../../core/shared/browse-entry.model';
import { ITEM } from '../../core/shared/item.resource-type';
import { ThemeService } from '../theme-support/theme.service'; import { ThemeService } from '../theme-support/theme.service';
import SpyObj = jasmine.SpyObj;
@listableObjectComponent(BrowseEntry, ViewMode.ListElement, DEFAULT_CONTEXT, 'custom')
@Component({
selector: 'ds-browse-entry-list-element',
template: ''
})
class MockThemedBrowseEntryListElementComponent {}
describe('BrowseByComponent', () => { describe('BrowseByComponent', () => {
let comp: BrowseByComponent; let comp: BrowseByComponent;
let fixture: ComponentFixture<BrowseByComponent>; let fixture: ComponentFixture<BrowseByComponent>;
let themeService: ThemeService;
const mockItems = [ const mockItems = [
Object.assign(new Item(), { Object.assign(new Item(), {
id: 'fakeId-1', id: 'fakeId-1',
@@ -59,9 +73,12 @@ describe('BrowseByComponent', () => {
}); });
const paginationService = new PaginationServiceStub(paginationConfig); const paginationService = new PaginationServiceStub(paginationConfig);
let themeService: SpyObj<ThemeService>;
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
themeService = jasmine.createSpyObj('themeService', { themeService = jasmine.createSpyObj('themeService', {
getThemeName: 'dspace', getThemeName: 'dspace',
getThemeName$: observableOf('dspace'),
}); });
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ imports: [
@@ -82,6 +99,7 @@ describe('BrowseByComponent', () => {
declarations: [], declarations: [],
providers: [ providers: [
{provide: PaginationService, useValue: paginationService}, {provide: PaginationService, useValue: paginationService},
{provide: MockThemedBrowseEntryListElementComponent},
{ provide: ThemeService, useValue: themeService }, { provide: ThemeService, useValue: themeService },
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
@@ -170,4 +188,67 @@ describe('BrowseByComponent', () => {
}); });
}); });
describe('when enableArrows is true and browseEntries are provided', () => {
let browseEntries;
beforeEach(() => {
browseEntries = [
Object.assign(new BrowseEntry(), {
type: ITEM,
authority: 'authority key 1',
value: 'browse entry 1',
language: null,
count: 1,
}),
Object.assign(new BrowseEntry(), {
type: ITEM,
authority: null,
value: 'browse entry 2',
language: null,
count: 4,
}),
];
comp.enableArrows = true;
comp.objects$ = createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), browseEntries));
comp.paginationConfig = paginationConfig;
comp.sortConfig = Object.assign(new SortOptions('dc.title', SortDirection.ASC));
// NOTE: do NOT trigger change detection until the theme is set, such that the theme can be picked up as well
});
describe('when theme is base', () => {
beforeEach(() => {
themeService.getThemeName.and.returnValue('base');
themeService.getThemeName$.and.returnValue(observableOf('base'));
fixture.detectChanges();
});
it('should use the base component to render browse entries', () => {
const componentLoaders = fixture.debugElement.queryAll(By.directive(ListableObjectComponentLoaderComponent));
expect(componentLoaders.length).toEqual(browseEntries.length);
componentLoaders.forEach((componentLoader) => {
const browseEntry = componentLoader.query(By.css('ds-browse-entry-list-element'));
expect(browseEntry.componentInstance).toBeInstanceOf(BrowseEntryListElementComponent);
});
});
});
describe('when theme is custom', () => {
beforeEach(() => {
themeService.getThemeName.and.returnValue('custom');
themeService.getThemeName$.and.returnValue(observableOf('custom'));
fixture.detectChanges();
});
it('should use the themed component to render browse entries', () => {
const componentLoaders = fixture.debugElement.queryAll(By.directive(ListableObjectComponentLoaderComponent));
expect(componentLoaders.length).toEqual(browseEntries.length);
componentLoaders.forEach((componentLoader) => {
const browseEntry = componentLoader.query(By.css('ds-browse-entry-list-element'));
expect(browseEntry.componentInstance).toBeInstanceOf(MockThemedBrowseEntryListElementComponent);
});
});
});
});
}); });

View File

@@ -8,6 +8,7 @@ import { Observable } from 'rxjs';
import { ListableObject } from '../object-collection/shared/listable-object.model'; import { ListableObject } from '../object-collection/shared/listable-object.model';
import { getStartsWithComponent, StartsWithType } from '../starts-with/starts-with-decorator'; import { getStartsWithComponent, StartsWithType } from '../starts-with/starts-with-decorator';
import { PaginationService } from '../../core/pagination/pagination.service'; import { PaginationService } from '../../core/pagination/pagination.service';
import { ViewMode } from '../../core/shared/view-mode.model';
@Component({ @Component({
selector: 'ds-browse-by', selector: 'ds-browse-by',
@@ -22,6 +23,12 @@ import { PaginationService } from '../../core/pagination/pagination.service';
* Component to display a browse-by page for any ListableObject * Component to display a browse-by page for any ListableObject
*/ */
export class BrowseByComponent implements OnInit { export class BrowseByComponent implements OnInit {
/**
* ViewMode that should be passed to {@link ListableObjectComponentLoaderComponent}.
*/
viewMode: ViewMode = ViewMode.ListElement;
/** /**
* The i18n message to display as title * The i18n message to display as title
*/ */

View File

@@ -8,7 +8,7 @@ import { ChipsItem } from './models/chips-item.model';
import { UploaderService } from '../uploader/uploader.service'; import { UploaderService } from '../uploader/uploader.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { Options } from 'sortablejs'; import { Options } from 'sortablejs';
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; import { BehaviorSubject } from 'rxjs';
@Component({ @Component({
selector: 'ds-chips', selector: 'ds-chips',

View File

@@ -11,8 +11,8 @@
aria-labelledby="dropdownMenuButton" aria-labelledby="dropdownMenuButton"
(scroll)="onScroll($event)" (scroll)="onScroll($event)"
infiniteScroll infiniteScroll
[infiniteScrollDistance]="5" [infiniteScrollDistance]="1.5"
[infiniteScrollThrottle]="300" [infiniteScrollThrottle]="0"
[infiniteScrollUpDistance]="1.5" [infiniteScrollUpDistance]="1.5"
[fromRoot]="true" [fromRoot]="true"
[scrollWindow]="false" [scrollWindow]="false"
@@ -21,7 +21,7 @@
<button class="dropdown-item disabled" *ngIf="searchListCollection?.length == 0 && !(isLoading | async)"> <button class="dropdown-item disabled" *ngIf="searchListCollection?.length == 0 && !(isLoading | async)">
{{'submission.sections.general.no-collection' | translate}} {{'submission.sections.general.no-collection' | translate}}
</button> </button>
<ng-container *ngIf="searchListCollection?.length > 0 && !(isLoading | async)"> <ng-container *ngIf="searchListCollection?.length > 0">
<button *ngFor="let listItem of searchListCollection" <button *ngFor="let listItem of searchListCollection"
class="dropdown-item collection-item" class="dropdown-item collection-item"
title="{{ listItem.collection.name }}" title="{{ listItem.collection.name }}"

View File

@@ -223,8 +223,9 @@ export class CollectionDropdownComponent implements OnInit, OnDestroy {
switchMap((collectionsRD: RemoteData<PaginatedList<Collection>>) => { switchMap((collectionsRD: RemoteData<PaginatedList<Collection>>) => {
this.searchComplete.emit(); this.searchComplete.emit();
if (collectionsRD.hasSucceeded && collectionsRD.payload.totalElements > 0) { if (collectionsRD.hasSucceeded && collectionsRD.payload.totalElements > 0) {
if ( (this.searchListCollection.length + findOptions.elementsPerPage) >= collectionsRD.payload.totalElements ) { if (this.searchListCollection.length >= collectionsRD.payload.totalElements) {
this.hasNextPage = false; this.hasNextPage = false;
}
this.emitSelectionEvents(collectionsRD); this.emitSelectionEvents(collectionsRD);
return observableFrom(collectionsRD.payload.page).pipe( return observableFrom(collectionsRD.payload.page).pipe(
mergeMap((collection: Collection) => collection.parentCommunity.pipe( mergeMap((collection: Collection) => collection.parentCommunity.pipe(
@@ -236,7 +237,6 @@ export class CollectionDropdownComponent implements OnInit, OnDestroy {
))), ))),
reduce((acc: any, value: any) => [...acc, value], []), reduce((acc: any, value: any) => [...acc, value], []),
); );
}
} else { } else {
this.hasNextPage = false; this.hasNextPage = false;
return observableOf([]); return observableOf([]);

View File

@@ -1,7 +1,7 @@
import { Component, Input, OnInit } from '@angular/core'; import { Component, Input, OnInit } from '@angular/core';
import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { DSpaceObject } from '../../../core/shared/dspace-object.model';
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
import { Observable } from 'rxjs/internal/Observable'; import { Observable } from 'rxjs';
import { FeatureID } from '../../../core/data/feature-authorization/feature-id'; import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
@Component({ @Component({

View File

@@ -1,11 +1,10 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
import { Observable } from 'rxjs/internal/Observable'; import { Observable, of } from 'rxjs';
import { FeatureID } from '../../../core/data/feature-authorization/feature-id'; import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
import { VersionHistoryDataService } from '../../../core/data/version-history-data.service'; import { VersionHistoryDataService } from '../../../core/data/version-history-data.service';
import { Item } from '../../../core/shared/item.model'; import { Item } from '../../../core/shared/item.model';
import { map, startWith, switchMap } from 'rxjs/operators'; import { map, startWith, switchMap } from 'rxjs/operators';
import { of } from 'rxjs';
@Component({ @Component({
selector: 'ds-dso-page-version-button', selector: 'ds-dso-page-version-button',

View File

@@ -3,7 +3,7 @@
class="form-control" class="form-control"
(click)="$event.stopPropagation();" (click)="$event.stopPropagation();"
placeholder="{{'dso-selector.placeholder' | translate: { type: typesString } }}" placeholder="{{'dso-selector.placeholder' | translate: { type: typesString } }}"
[formControl]="input" dsAutoFocus (keyup.enter)="selectSingleResult()"> [formControl]="input" ngbAutofocus (keyup.enter)="selectSingleResult()">
</div> </div>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<div class="scrollable-menu list-group"> <div class="scrollable-menu list-group">

View File

@@ -1,9 +1,8 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { Observable } from 'rxjs/internal/Observable'; import { Observable, of as observableOf } from 'rxjs';
import { map, switchMap } from 'rxjs/operators'; import { map, switchMap } from 'rxjs/operators';
import { of as observableOf } from 'rxjs';
import { METADATA_EXPORT_SCRIPT_NAME, ScriptDataService } from '../../../../core/data/processes/script-data.service'; import { METADATA_EXPORT_SCRIPT_NAME, ScriptDataService } from '../../../../core/data/processes/script-data.service';
import { Collection } from '../../../../core/shared/collection.model'; import { Collection } from '../../../../core/shared/collection.model';
import { Community } from '../../../../core/shared/community.model'; import { Community } from '../../../../core/shared/community.model';

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