diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7758020724..539fd740ee 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,11 +29,11 @@ jobs: steps: # https://github.com/actions/checkout - name: Checkout codebase - uses: actions/checkout@v1 + uses: actions/checkout@v2 # https://github.com/actions/setup-node - name: Install Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 + uses: actions/setup-node@v2 with: node-version: ${{ matrix.node-version }} @@ -82,7 +82,7 @@ jobs: # Upload coverage reports to Codecov (for Node v12 only) # https://github.com/codecov/codecov-action - name: Upload coverage to Codecov.io - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v2 if: matrix.node-version == '12.x' # Using docker-compose start backend using CI configuration diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000000..00ec2fa8f7 --- /dev/null +++ b/.github/workflows/docker.yml @@ -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 }} diff --git a/.vscode/settings.json b/.vscode/settings.json index e8522b85d7..f60eb01f00 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,5 +3,6 @@ "i18n-ally.localesPaths": [ "src/assets/i18n", "src/app/core/locale" - ] + ], + "typescript.tsdk": "node_modules\\typescript\\lib" } \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index db9983cace..2d98971112 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ # 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 ADD . /app/ EXPOSE 4000 diff --git a/LICENSE b/LICENSE index f55d21fe42..b381f6d968 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -DSpace source code BSD License: +BSD 3-Clause License 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 documentation and/or other materials provided with the distribution. -- Neither the name DuraSpace nor the name of the DSpace Foundation -nor the names of its contributors may be used to endorse or promote -products derived from this software without specific prior written -permission. +- Neither the name of the copyright holder nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. 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 A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, @@ -29,11 +28,4 @@ OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH -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. +DAMAGE. \ No newline at end of file diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000000..44bbf95d2a --- /dev/null +++ b/NOTICE @@ -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. \ No newline at end of file diff --git a/README.md b/README.md index 69b6132478..91ad0f9fab 100644 --- a/README.md +++ b/README.md @@ -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. -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. @@ -449,4 +451,8 @@ DSpace uses GitHub to track issues: 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. diff --git a/angular.json b/angular.json index 81ef9d49b4..c6607fc80a 100644 --- a/angular.json +++ b/angular.json @@ -177,16 +177,13 @@ } }, "outputPath": "dist/server", - "main": "src/main.server.ts", + "main": "server.ts", "tsConfig": "tsconfig.server.json" }, "configurations": { "production": { "sourceMap": false, - "optimization": { - "scripts": false, - "styles": true - } + "optimization": true } } }, diff --git a/cypress.json b/cypress.json index cded267c48..e06de8e4c5 100644 --- a/cypress.json +++ b/cypress.json @@ -5,5 +5,6 @@ "screenshotsFolder": "cypress/screenshots", "pluginsFile": "cypress/plugins/index.ts", "fixturesFolder": "cypress/fixtures", - "baseUrl": "http://localhost:4000" + "baseUrl": "http://localhost:4000", + "retries": 2 } \ No newline at end of file diff --git a/docker/README.md b/docker/README.md index 747db22143..b0943562af 100644 --- a/docker/README.md +++ b/docker/README.md @@ -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. *** +## '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-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. diff --git a/docker/docker-compose-ci.yml b/docker/docker-compose-ci.yml index d2d02f0a55..18fa152c9d 100644 --- a/docker/docker-compose-ci.yml +++ b/docker/docker-compose-ci.yml @@ -64,7 +64,7 @@ services: dspacesolr: container_name: dspacesolr # 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 depends_on: - dspace diff --git a/docker/docker-compose-rest.yml b/docker/docker-compose-rest.yml index 370ccbbdf1..3534682afc 100644 --- a/docker/docker-compose-rest.yml +++ b/docker/docker-compose-rest.yml @@ -62,7 +62,7 @@ services: dspacesolr: container_name: dspacesolr # 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 depends_on: - dspace diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 7c5c326959..e518dc99d2 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -20,7 +20,7 @@ services: DSPACE_NAMESPACE: / DSPACE_PORT: '4000' DSPACE_SSL: "false" - image: dspace/dspace-angular:latest + image: dspace/dspace-angular:dspace-7_x build: context: .. dockerfile: Dockerfile diff --git a/package.json b/package.json index f5b0201455..28ffd3d970 100644 --- a/package.json +++ b/package.json @@ -26,16 +26,14 @@ "build": "ng build", "build:stats": "ng build --stats-json", "build:prod": "yarn run build:ssr", - "build:ssr": "yarn run build:client-and-server-bundles && yarn run compile:server", - "build:client-and-server-bundles": "ng build --prod && ng run dspace-angular:server:production --bundleDependencies true", + "build:ssr": "ng build --configuration production && ng run dspace-angular:server:production", "test:watch": "npm-run-all --parallel config:test:watch test", "test": "ng test --sourceMap=true --watch=true", "test:headless": "ng test --watch=false --sourceMap=true --browsers=ChromeHeadless --code-coverage", "lint": "ng lint", "lint-fix": "ng lint --fix=true", "e2e": "ng e2e", - "compile:server": "webpack --config webpack.server.config.js --progress --color", - "serve:ssr": "node dist/server", + "serve:ssr": "node dist/server/main", "clean:coverage": "rimraf coverage", "clean:dist": "rimraf dist", "clean:doc": "rimraf doc", @@ -65,26 +63,26 @@ "webdriver-manager": "^12.1.8" }, "dependencies": { - "@angular/animations": "~10.2.3", - "@angular/cdk": "^10.2.6", - "@angular/common": "~10.2.3", - "@angular/compiler": "~10.2.3", - "@angular/core": "~10.2.3", - "@angular/forms": "~10.2.3", - "@angular/localize": "10.2.3", - "@angular/platform-browser": "~10.2.3", - "@angular/platform-browser-dynamic": "~10.2.3", - "@angular/platform-server": "~10.2.3", - "@angular/router": "~10.2.3", + "@angular/animations": "~11.2.14", + "@angular/cdk": "^11.2.13", + "@angular/common": "~11.2.14", + "@angular/compiler": "~11.2.14", + "@angular/core": "~11.2.14", + "@angular/forms": "~11.2.14", + "@angular/localize": "11.2.14", + "@angular/platform-browser": "~11.2.14", + "@angular/platform-browser-dynamic": "~11.2.14", + "@angular/platform-server": "~11.2.14", + "@angular/router": "~11.2.14", "@angularclass/bootloader": "1.0.1", "@kolkov/ngx-gallery": "^1.2.3", - "@ng-bootstrap/ng-bootstrap": "7.0.0", - "@ng-dynamic-forms/core": "^12.0.0", - "@ng-dynamic-forms/ui-ng-bootstrap": "^12.0.0", - "@ngrx/effects": "^10.0.1", - "@ngrx/router-store": "^10.0.1", - "@ngrx/store": "^10.0.1", - "@nguniversal/express-engine": "10.1.0", + "@ng-bootstrap/ng-bootstrap": "9.1.3", + "@ng-dynamic-forms/core": "^13.0.0", + "@ng-dynamic-forms/ui-ng-bootstrap": "^13.0.0", + "@ngrx/effects": "^11.1.1", + "@ngrx/router-store": "^11.1.1", + "@ngrx/store": "^11.1.1", + "@nguniversal/express-engine": "11.2.1", "@ngx-translate/core": "^13.0.0", "@nicky-lenaers/ngx-scroll-to": "^9.0.0", "angular-idle-preload": "3.0.0", @@ -94,9 +92,9 @@ "caniuse-lite": "^1.0.30001165", "cerialize": "0.1.18", "cli-progress": "^3.8.0", + "compression": "^1.7.4", "cookie-parser": "1.4.5", "core-js": "^3.7.0", - "debug-loader": "^0.0.1", "deepmerge": "^4.2.2", "express": "^4.17.1", "express-rate-limit": "^5.1.3", @@ -105,78 +103,78 @@ "filesize": "^6.1.0", "font-awesome": "4.7.0", "https": "1.0.0", + "http-proxy-middleware": "^1.0.5", "js-cookie": "2.2.1", "json5": "^2.1.3", "jsonschema": "1.4.0", "jwt-decode": "^3.1.2", "klaro": "^0.7.10", - "mirador": "^3.0.0", + "lodash": "^4.17.21", + "mirador": "^3.3.0", "mirador-dl-plugin": "^0.13.0", - "mirador-share-plugin": "^0.10.0", + "mirador-share-plugin": "^0.11.0", "moment": "^2.29.1", "morgan": "^1.10.0", - "ng-mocks": "10.5.4", + "ng-mocks": "11.11.2", "ng2-file-upload": "1.4.0", - "ng2-nouislider": "^1.8.2", + "ng2-nouislider": "^1.8.3", "ngx-infinite-scroll": "^10.0.1", "ngx-moment": "^5.0.0", "ngx-pagination": "5.0.0", - "ngx-sortablejs": "^10.0.0", + "ngx-sortablejs": "^11.1.0", "nouislider": "^14.6.3", "pem": "1.14.4", "postcss-cli": "^8.3.0", - "react": "^16.14.0", - "react-dom": "^16.14.0", "reflect-metadata": "^0.1.13", "rxjs": "^6.6.3", - "rxjs-spy": "^7.5.3", - "sass-resources-loader": "^2.1.1", "sortablejs": "1.13.0", "tslib": "^2.0.0", + "url-parse": "^1.5.3", + "uuid": "^8.3.2", "webfontloader": "1.6.28", "zone.js": "^0.10.3" }, "devDependencies": { "@angular-builders/custom-webpack": "10.0.1", - "@angular-devkit/build-angular": "~0.1002.0", - "@angular/cli": "~10.2.0", - "@angular/compiler-cli": "~10.2.3", - "@angular/language-service": "~10.2.3", + "@angular-devkit/build-angular": "~0.1102.15", + "@angular/cli": "~11.2.15", + "@angular/compiler-cli": "~11.2.14", + "@angular/language-service": "~11.2.14", "@cypress/schematic": "^1.5.0", "@fortawesome/fontawesome-free": "^5.5.0", - "@ngrx/store-devtools": "^10.0.1", - "@ngtools/webpack": "10.2.0", - "@nguniversal/builders": "~10.1.0", + "@ngrx/store-devtools": "^11.1.1", + "@ngtools/webpack": "10.2.3", + "@nguniversal/builders": "~11.2.1", "@types/deep-freeze": "0.1.2", "@types/express": "^4.17.9", "@types/file-saver": "^2.0.1", - "@types/jasmine": "^3.6.2", + "@types/jasmine": "~3.6.0", "@types/jasminewd2": "~2.0.8", "@types/js-cookie": "2.2.6", "@types/lodash": "^4.14.165", "@types/node": "^14.14.9", "axe-core": "^4.3.3", - "codelyzer": "^6.0.1", + "codelyzer": "^6.0.0", "compression-webpack-plugin": "^3.0.1", "copy-webpack-plugin": "^6.4.1", "css-loader": "3.4.0", "cssnano": "^4.1.10", "cypress": "8.6.0", "cypress-axe": "^0.13.0", + "debug-loader": "^0.0.1", "deep-freeze": "0.0.1", "dotenv": "^8.2.0", "fork-ts-checker-webpack-plugin": "^6.0.3", "html-loader": "^1.3.2", "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-spec-reporter": "^6.0.0", + "jasmine-spec-reporter": "~5.0.0", "karma": "^5.2.3", - "karma-chrome-launcher": "^3.1.0", + "karma-chrome-launcher": "~3.1.0", "karma-coverage-istanbul-reporter": "~3.0.2", - "karma-jasmine": "^4.0.1", - "karma-jasmine-html-reporter": "^1.5.4", + "karma-jasmine": "~4.0.0", + "karma-jasmine-html-reporter": "^1.5.0", "karma-mocha-reporter": "2.2.5", "nodemon": "^2.0.2", "npm-run-all": "^4.1.5", @@ -189,17 +187,21 @@ "protractor": "^7.0.0", "protractor-istanbul-plugin": "2.0.0", "raw-loader": "0.5.1", + "react": "^16.14.0", + "react-dom": "^16.14.0", "rimraf": "^3.0.2", + "rxjs-spy": "^7.5.3", + "sass-resources-loader": "^2.1.1", "script-ext-html-webpack-plugin": "2.1.5", "string-replace-loader": "^2.3.0", "terser-webpack-plugin": "^2.3.1", "ts-loader": "^5.2.0", - "ts-node": "^8.8.1", + "ts-node": "^8.10.2", "tslint": "^6.1.3", "typescript": "~4.0.5", "webpack": "^4.44.2", "webpack-bundle-analyzer": "^4.4.0", "webpack-cli": "^4.2.0", - "webpack-node-externals": "1.7.2" + "webpack-dev-server": "^4.5.0" } } diff --git a/server.ts b/server.ts index e9fb1a7fd0..c00bdb5ef5 100644 --- a/server.ts +++ b/server.ts @@ -30,6 +30,7 @@ import { join } from 'path'; import { enableProdMode } from '@angular/core'; import { existsSync } from 'fs'; +import { ngExpressEngine } from '@nguniversal/express-engine'; import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens'; import { environment } from './src/environments/environment'; 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 { UIServerConfig } from './src/config/ui-server-config.interface'; +import { ServerAppModule } from './src/main.server'; + /* * 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'; -// * 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'); // The Express app is exported so that it can be used by serverless Functions. @@ -59,7 +59,6 @@ export function app() { */ const server = express(); - /* * If production mode is enabled in the environment file: * - Enable Angular's production mode @@ -227,47 +226,59 @@ function run() { }); } -/* - * If SSL is enabled - * - Read credentials from configuration files - * - Call script to start an HTTPS server with these credentials - * When SSL is disabled - * - Start an HTTP server on the configured port and host - */ -if (environment.ui.ssl) { - let serviceKey; - try { - serviceKey = fs.readFileSync('./config/ssl/key.pem'); - } catch (e) { - console.warn('Service key not found at ./config/ssl/key.pem'); - } +function start() { + /* + * If SSL is enabled + * - Read credentials from configuration files + * - Call script to start an HTTPS server with these credentials + * When SSL is disabled + * - Start an HTTP server on the configured port and host + */ + if (environment.ui.ssl) { + let serviceKey; + try { + serviceKey = fs.readFileSync('./config/ssl/key.pem'); + } catch (e) { + console.warn('Service key not found at ./config/ssl/key.pem'); + } - let certificate; - try { - certificate = fs.readFileSync('./config/ssl/cert.pem'); - } catch (e) { - console.warn('Certificate not found at ./config/ssl/key.pem'); - } + let certificate; + try { + certificate = fs.readFileSync('./config/ssl/cert.pem'); + } catch (e) { + console.warn('Certificate not found at ./config/ssl/key.pem'); + } - if (serviceKey && certificate) { - createHttpsServer({ - serviceKey: serviceKey, - certificate: certificate - }); + if (serviceKey && certificate) { + createHttpsServer({ + serviceKey: serviceKey, + certificate: certificate + }); + } else { + console.warn('Disabling certificate validation and proceeding with a self-signed certificate. If this is a production server, it is recommended that you configure a valid certificate instead.'); + + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; // lgtm[js/disabling-certificate-validation] + + pem.createCertificate({ + days: 1, + selfSigned: true + }, (error, keys) => { + createHttpsServer(keys); + }); + } } else { - console.warn('Disabling certificate validation and proceeding with a self-signed certificate. If this is a production server, it is recommended that you configure a valid certificate instead.'); - - process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; // lgtm[js/disabling-certificate-validation] - - pem.createCertificate({ - days: 1, - selfSigned: true - }, (error, keys) => { - createHttpsServer(keys); - }); + run(); } -} else { - 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'; diff --git a/src/app/access-control/group-registry/group-form/group-form.component.spec.ts b/src/app/access-control/group-registry/group-form/group-form.component.spec.ts index 5f0f570044..2307f3c6fa 100644 --- a/src/app/access-control/group-registry/group-form/group-form.component.spec.ts +++ b/src/app/access-control/group-registry/group-form/group-form.component.spec.ts @@ -2,7 +2,7 @@ import { CommonModule } from '@angular/common'; import { HttpClient } from '@angular/common/http'; import { NO_ERRORS_SCHEMA } from '@angular/core'; 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 { ActivatedRoute, Router } from '@angular/router'; 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 { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; import { Operation } from 'fast-json-patch'; +import { ValidateGroupExists } from './validators/group-exists.validator'; describe('GroupFormComponent', () => { let component: GroupFormComponent; @@ -117,7 +118,69 @@ describe('GroupFormComponent', () => { 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(); router = new RouterMock(); 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>> { + 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(); + }); + })); + }); + }); + }); diff --git a/src/app/access-control/group-registry/group-form/group-form.component.ts b/src/app/access-control/group-registry/group-form/group-form.component.ts index b2b9fab58d..826b7dbe69 100644 --- a/src/app/access-control/group-registry/group-form/group-form.component.ts +++ b/src/app/access-control/group-registry/group-form/group-form.component.ts @@ -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 { ActivatedRoute, Router } from '@angular/router'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; @@ -16,7 +16,7 @@ import { of as observableOf, Subscription, } 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 { getCommunityEditRolesRoute } from '../../../community-page/community-page-routing-paths'; 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 { NoContent } from '../../../core/shared/NoContent.model'; import { Operation } from 'fast-json-patch'; +import { ValidateGroupExists } from './validators/group-exists.validator'; @Component({ selector: 'ds-group-form', @@ -126,6 +127,12 @@ export class GroupFormComponent implements OnInit, OnDestroy { */ public AlertTypeEnum = AlertType; + /** + * Subscription to email field value change + */ + groupNameValueChangeSubscribe: Subscription; + + constructor(public groupDataService: GroupDataService, private ePersonDataService: EPersonDataService, private dSpaceObjectDataService: DSpaceObjectDataService, @@ -136,7 +143,8 @@ export class GroupFormComponent implements OnInit, OnDestroy { protected router: Router, private authorizationService: AuthorizationDataService, private modalService: NgbModal, - public requestService: RequestService) { + public requestService: RequestService, + protected changeDetectorRef: ChangeDetectorRef) { } ngOnInit() { @@ -192,6 +200,14 @@ export class GroupFormComponent implements OnInit, OnDestroy { this.groupDescription, ]; 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( observableCombineLatest( this.groupDataService.getActiveGroup(), @@ -201,6 +217,10 @@ export class GroupFormComponent implements OnInit, OnDestroy { ).subscribe(([activeGroup, canEdit, linkedObject]) => { if (activeGroup != null) { + + // Disable group name exists validator + this.formGroup.controls.groupName.clearAsyncValidators(); + this.groupBeingEdited = activeGroup; if (linkedObject?.name) { @@ -436,6 +456,11 @@ export class GroupFormComponent implements OnInit, OnDestroy { ngOnDestroy(): void { this.groupDataService.cancelEditGroup(); this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe()); + + if ( hasValue(this.groupNameValueChangeSubscribe) ) { + this.groupNameValueChangeSubscribe.unsubscribe(); + } + } /** diff --git a/src/app/access-control/group-registry/group-form/validators/group-exists.validator.ts b/src/app/access-control/group-registry/group-form/validators/group-exists.validator.ts new file mode 100644 index 0000000000..88f22413e9 --- /dev/null +++ b/src/app/access-control/group-registry/group-form/validators/group-exists.validator.ts @@ -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 + */ + static createValidator(groupDataService: GroupDataService) { + return (control: AbstractControl): Promise | Observable => { + 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; + }), + ); + }; + } +} diff --git a/src/app/admin/admin-search-page/admin-search.module.ts b/src/app/admin/admin-search-page/admin-search.module.ts index 0b3b7df9bb..ecf8d8a2d2 100644 --- a/src/app/admin/admin-search-page/admin-search.module.ts +++ b/src/app/admin/admin-search-page/admin-search.module.ts @@ -36,7 +36,7 @@ const ENTRY_COMPONENTS = [ export class AdminSearchModule { /** * 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() { return { diff --git a/src/app/admin/admin-workflow-page/admin-workflow.module.ts b/src/app/admin/admin-workflow-page/admin-workflow.module.ts index 4715ae16f4..02e5e01023 100644 --- a/src/app/admin/admin-workflow-page/admin-workflow.module.ts +++ b/src/app/admin/admin-workflow-page/admin-workflow.module.ts @@ -28,7 +28,7 @@ const ENTRY_COMPONENTS = [ export class AdminWorkflowModuleModule { /** * 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() { return { diff --git a/src/app/admin/admin.module.ts b/src/app/admin/admin.module.ts index 25cdd67dcf..6c756dd851 100644 --- a/src/app/admin/admin.module.ts +++ b/src/app/admin/admin.module.ts @@ -34,7 +34,7 @@ const ENTRY_COMPONENTS = [ export class AdminModule { /** * 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() { return { diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 157ada622d..04d2c55bdd 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -202,8 +202,8 @@ import { GroupAdministratorGuard } from './core/data/feature-authorization/featu { path: '**', pathMatch: 'full', component: ThemedPageNotFoundComponent }, ]} ],{ - onSameUrlNavigation: 'reload', - }) + onSameUrlNavigation: 'reload', +}) ], exports: [RouterModule], }) diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index 3f2dc45ce7..937b71eb5a 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -171,7 +171,8 @@ describe('App component', () => { TestBed.configureTestingModule(getDefaultTestBedConf()); TestBed.overrideProvider(ThemeService, {useValue: getMockThemeService('custom')}); 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]); fixture = TestBed.createComponent(AppComponent); comp = fixture.componentInstance; diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 6f06a84144..03075f4f18 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -31,12 +31,12 @@ import { AuthService } from './core/auth/auth.service'; import { CSSVariableService } from './shared/sass-helper/sass-helper.service'; import { MenuService } from './shared/menu/menu.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 { environment } from '../environments/environment'; import { models } from './core/core.module'; 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 { GoogleAnalyticsService } from './statistics/google-analytics.service'; import { DOCUMENT, isPlatformBrowser } from '@angular/common'; @@ -115,11 +115,11 @@ export class AppComponent implements OnInit, AfterViewInit { this.isThemeCSSLoading$.next(true); } if (hasValue(themeName)) { - this.setThemeCss(themeName); + this.loadGlobalThemeConfig(themeName); } else if (hasValue(DEFAULT_THEME_CONFIG)) { - this.setThemeCss(DEFAULT_THEME_CONFIG.name); + this.loadGlobalThemeConfig(DEFAULT_THEME_CONFIG.name); } 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 * @@ -241,9 +246,13 @@ export class AppComponent implements OnInit, AfterViewInit { */ private setThemeCss(themeName: string): void { 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 // 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'); link.setAttribute('rel', 'stylesheet'); link.setAttribute('type', 'text/css'); @@ -265,6 +274,78 @@ export class AppComponent implements OnInit, AfterViewInit { 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() { const isIdle$ = this.authService.isUserIdle(); const isAuthenticated$ = this.authService.isAuthenticated(); diff --git a/src/app/breadcrumbs/breadcrumbs.component.html b/src/app/breadcrumbs/breadcrumbs.component.html index beb5039178..51524fde48 100644 --- a/src/app/breadcrumbs/breadcrumbs.component.html +++ b/src/app/breadcrumbs/breadcrumbs.component.html @@ -10,11 +10,11 @@ - + - + diff --git a/src/app/breadcrumbs/breadcrumbs.component.scss b/src/app/breadcrumbs/breadcrumbs.component.scss index 6967b53de1..412dca87db 100644 --- a/src/app/breadcrumbs/breadcrumbs.component.scss +++ b/src/app/breadcrumbs/breadcrumbs.component.scss @@ -10,6 +10,19 @@ 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 { color: var(--ds-breadcrumb-link-color) !important; } @@ -18,5 +31,6 @@ li.breadcrumb-item.active { } .breadcrumb-item+ .breadcrumb-item::before { + display: block; content: quote("•") !important; } diff --git a/src/app/breadcrumbs/breadcrumbs.component.spec.ts b/src/app/breadcrumbs/breadcrumbs.component.spec.ts index dd08a74f23..69387e7534 100644 --- a/src/app/breadcrumbs/breadcrumbs.component.spec.ts +++ b/src/app/breadcrumbs/breadcrumbs.component.spec.ts @@ -8,7 +8,7 @@ import { By } from '@angular/platform-browser'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { TranslateLoaderMock } from '../shared/testing/translate-loader.mock'; 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'; describe('BreadcrumbsComponent', () => { @@ -72,7 +72,7 @@ describe('BreadcrumbsComponent', () => { expect(breadcrumbs.length).toBe(3); expectBreadcrumb(breadcrumbs[0], 'home.breadcrumbs', '/'); expectBreadcrumb(breadcrumbs[1], 'bc 1', '/example.com'); - expectBreadcrumb(breadcrumbs[2], 'bc 2', null); + expectBreadcrumb(breadcrumbs[2].query(By.css('.text-truncate')), 'bc 2', null); }); }); diff --git a/src/app/breadcrumbs/breadcrumbs.component.ts b/src/app/breadcrumbs/breadcrumbs.component.ts index 488fd37a2c..248fb446ed 100644 --- a/src/app/breadcrumbs/breadcrumbs.component.ts +++ b/src/app/breadcrumbs/breadcrumbs.component.ts @@ -1,7 +1,7 @@ import { Component } from '@angular/core'; import { Breadcrumb } from './breadcrumb/breadcrumb.model'; import { BreadcrumbsService } from './breadcrumbs.service'; -import { Observable } from 'rxjs/internal/Observable'; +import { Observable } from 'rxjs'; /** * Component representing the breadcrumbs of a page diff --git a/src/app/browse-by/browse-by.module.ts b/src/app/browse-by/browse-by.module.ts index 2d3618aae6..cd44e52b81 100644 --- a/src/app/browse-by/browse-by.module.ts +++ b/src/app/browse-by/browse-by.module.ts @@ -31,7 +31,7 @@ const ENTRY_COMPONENTS = [ export class BrowseByModule { /** * 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() { return { diff --git a/src/app/collection-page/collection-item-mapper/collection-item-mapper.component.html b/src/app/collection-page/collection-item-mapper/collection-item-mapper.component.html index e10b9da247..042beddc84 100644 --- a/src/app/collection-page/collection-item-mapper/collection-item-mapper.component.html +++ b/src/app/collection-page/collection-item-mapper/collection-item-mapper.component.html @@ -5,9 +5,10 @@

{{'collection.edit.item-mapper.description' | translate}}

- - - + +
diff --git a/src/app/collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts b/src/app/collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts index 5ae1445cef..0dfd013449 100644 --- a/src/app/collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts +++ b/src/app/collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts @@ -27,7 +27,7 @@ import { ItemSelectComponent } from '../../shared/object-select/item-select/item import { ObjectSelectService } from '../../shared/object-select/object-select.service'; import { ObjectSelectServiceStub } from '../../shared/testing/object-select-service.stub'; 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 { ErrorComponent } from '../../shared/error/error.component'; import { LoadingComponent } from '../../shared/loading/loading.component'; diff --git a/src/app/collection-page/edit-collection-page/collection-source/collection-source-controls/collection-source-controls.component.ts b/src/app/collection-page/edit-collection-page/collection-source/collection-source-controls/collection-source-controls.component.ts index abc5fe3083..7113c25e9f 100644 --- a/src/app/collection-page/edit-collection-page/collection-source/collection-source-controls/collection-source-controls.component.ts +++ b/src/app/collection-page/edit-collection-page/collection-source/collection-source-controls/collection-source-controls.component.ts @@ -11,18 +11,16 @@ import { import { filter, map, switchMap, tap } from 'rxjs/operators'; import { hasValue, hasValueOperator } from '../../../../shared/empty.util'; 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 { NotificationsService } from '../../../../shared/notifications/notifications.service'; import { Collection } from '../../../../core/shared/collection.model'; import { CollectionDataService } from '../../../../core/data/collection-data.service'; -import { Observable } from 'rxjs/internal/Observable'; import { Process } from '../../../../process-page/processes/process.model'; import { TranslateService } from '@ngx-translate/core'; import { HttpClient } from '@angular/common/http'; import { BitstreamDataService } from '../../../../core/data/bitstream-data.service'; 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 diff --git a/src/app/community-list-page/community-list-datasource.ts b/src/app/community-list-page/community-list-datasource.ts index 80ee51cd34..02774b794c 100644 --- a/src/app/community-list-page/community-list-datasource.ts +++ b/src/app/community-list-page/community-list-datasource.ts @@ -1,9 +1,8 @@ -import { Subscription } from 'rxjs/internal/Subscription'; import { FindListOptions } from '../core/data/request.models'; import { hasValue } from '../shared/empty.util'; import { CommunityListService, FlatNode } from './community-list-service'; import { CollectionViewer, DataSource } from '@angular/cdk/collections'; -import { BehaviorSubject, Observable, } from 'rxjs'; +import { BehaviorSubject, Observable, Subscription } from 'rxjs'; import { finalize } from 'rxjs/operators'; /** diff --git a/src/app/core/cache/builders/link.service.ts b/src/app/core/cache/builders/link.service.ts index 66f91dbbd6..af616332c0 100644 --- a/src/app/core/cache/builders/link.service.ts +++ b/src/app/core/cache/builders/link.service.ts @@ -10,8 +10,7 @@ import { LinkDefinition } from './build-decorators'; import { RemoteData } from '../../data/remote-data'; -import { Observable } from 'rxjs/internal/Observable'; -import { EMPTY } from 'rxjs'; +import { EMPTY, Observable } from 'rxjs'; import { ResourceType } from '../../shared/resource-type'; /** diff --git a/src/app/core/data/eperson-registration.service.spec.ts b/src/app/core/data/eperson-registration.service.spec.ts index 768d83c024..2407249615 100644 --- a/src/app/core/data/eperson-registration.service.spec.ts +++ b/src/app/core/data/eperson-registration.service.spec.ts @@ -7,7 +7,7 @@ import { PostRequest } from './request.models'; import { Registration } from '../shared/registration.model'; import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; 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'; describe('EpersonRegistrationService', () => { diff --git a/src/app/core/data/href-only-data.service.ts b/src/app/core/data/href-only-data.service.ts index c1298c054c..b1bc14ec6f 100644 --- a/src/app/core/data/href-only-data.service.ts +++ b/src/app/core/data/href-only-data.service.ts @@ -14,7 +14,7 @@ import { FindListOptions } from './request.models'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { dataService } from '../cache/builders/build-decorators'; import { RemoteData } from './remote-data'; -import { Observable } from 'rxjs/internal/Observable'; +import { Observable } from 'rxjs'; import { PaginatedList } from './paginated-list.model'; import { ITEM_TYPE } from '../shared/item-relationships/item-type.resource-type'; import { LICENSE } from '../shared/license.resource-type'; diff --git a/src/app/core/pagination/pagination.service.ts b/src/app/core/pagination/pagination.service.ts index dae6991834..ef996a26c1 100644 --- a/src/app/core/pagination/pagination.service.ts +++ b/src/app/core/pagination/pagination.service.ts @@ -10,7 +10,6 @@ import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util'; import { difference } from '../../shared/object.util'; import { isNumeric } from 'rxjs/internal-compatibility'; - @Injectable({ providedIn: 'root', }) diff --git a/src/app/core/shared/external-source.model.ts b/src/app/core/shared/external-source.model.ts index 9f20a732f4..0fe41c1156 100644 --- a/src/app/core/shared/external-source.model.ts +++ b/src/app/core/shared/external-source.model.ts @@ -7,7 +7,7 @@ import { HALLink } from './hal-link.model'; import { ResourceType } from './resource-type'; import { RemoteData } from '../data/remote-data'; 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 { ItemType } from './item-relationships/item-type.model'; diff --git a/src/app/core/shared/file.service.ts b/src/app/core/shared/file.service.ts index 98c468e9a3..f673629c8d 100644 --- a/src/app/core/shared/file.service.ts +++ b/src/app/core/shared/file.service.ts @@ -5,7 +5,7 @@ import { map, take } from 'rxjs/operators'; import { NativeWindowRef, NativeWindowService } from '../services/window.service'; import { URLCombiner } from '../url-combiner/url-combiner'; 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. diff --git a/src/app/core/shared/metadata.models.ts b/src/app/core/shared/metadata.models.ts index c29ac3bd2b..463f61c077 100644 --- a/src/app/core/shared/metadata.models.ts +++ b/src/app/core/shared/metadata.models.ts @@ -1,4 +1,4 @@ -import * as uuidv4 from 'uuid/v4'; +import { v4 as uuidv4 } from 'uuid'; import { autoserialize, Serialize, Deserialize } from 'cerialize'; import { hasValue } from '../../shared/empty.util'; /* tslint:disable:max-classes-per-file */ diff --git a/src/app/core/shared/metadata.utils.spec.ts b/src/app/core/shared/metadata.utils.spec.ts index bc91d0585e..812e65bcba 100644 --- a/src/app/core/shared/metadata.utils.spec.ts +++ b/src/app/core/shared/metadata.utils.spec.ts @@ -1,5 +1,5 @@ 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 { Metadata } from './metadata.utils'; diff --git a/src/app/core/shared/search/search-configuration.service.ts b/src/app/core/shared/search/search-configuration.service.ts index 74af230810..8c37fbc8f5 100644 --- a/src/app/core/shared/search/search-configuration.service.ts +++ b/src/app/core/shared/search/search-configuration.service.ts @@ -26,7 +26,7 @@ import { hasNoValue, hasValue, isNotEmpty, isNotEmptyOperator } from '../../../s import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { SearchConfig } from './search-filters/search-config.model'; import { SearchService } from './search.service'; -import { of } from 'rxjs/internal/observable/of'; +import { of } from 'rxjs'; import { PaginationService } from '../../pagination/pagination.service'; /** diff --git a/src/app/core/shared/uuid.service.ts b/src/app/core/shared/uuid.service.ts index 6c02facbac..3b9baf8e8e 100644 --- a/src/app/core/shared/uuid.service.ts +++ b/src/app/core/shared/uuid.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import * as uuidv4 from 'uuid/v4'; +import { v4 as uuidv4 } from 'uuid'; @Injectable() export class UUIDService { diff --git a/src/app/core/tasks/claimed-task-data.service.spec.ts b/src/app/core/tasks/claimed-task-data.service.spec.ts index ab9727592e..cdba0e4553 100644 --- a/src/app/core/tasks/claimed-task-data.service.spec.ts +++ b/src/app/core/tasks/claimed-task-data.service.spec.ts @@ -8,7 +8,7 @@ import { RemoteDataBuildService } from '../cache/builders/remote-data-build.serv import { NotificationsService } from '../../shared/notifications/notifications.service'; import { CoreState } from '../core.reducers'; 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 { RequestParam } from '../cache/models/request-param.model'; import { getTestScheduler } from 'jasmine-marbles'; diff --git a/src/app/core/tasks/pool-task-data.service.spec.ts b/src/app/core/tasks/pool-task-data.service.spec.ts index 7279c96e5c..7fe0ec67bf 100644 --- a/src/app/core/tasks/pool-task-data.service.spec.ts +++ b/src/app/core/tasks/pool-task-data.service.spec.ts @@ -10,7 +10,7 @@ import { CoreState } from '../core.reducers'; import { PoolTaskDataService } from './pool-task-data.service'; import { getTestScheduler } from 'jasmine-marbles'; 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 { RequestParam } from '../cache/models/request-param.model'; import { HttpOptions } from '../dspace-rest/dspace-rest.service'; diff --git a/src/app/core/tasks/tasks.service.spec.ts b/src/app/core/tasks/tasks.service.spec.ts index f0c86d2abf..fd9aa038b9 100644 --- a/src/app/core/tasks/tasks.service.spec.ts +++ b/src/app/core/tasks/tasks.service.spec.ts @@ -17,7 +17,7 @@ import { HttpClient, HttpHeaders } from '@angular/common/http'; import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service'; import { ChangeAnalyzer } from '../data/change-analyzer'; 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 { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock'; import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; diff --git a/src/app/core/xsrf/xsrf.interceptor.ts b/src/app/core/xsrf/xsrf.interceptor.ts index a10959c1c8..d527924a28 100644 --- a/src/app/core/xsrf/xsrf.interceptor.ts +++ b/src/app/core/xsrf/xsrf.interceptor.ts @@ -8,11 +8,10 @@ import { HttpResponse, HttpXsrfTokenExtractor } from '@angular/common/http'; -import { Observable } from 'rxjs/internal/Observable'; +import { Observable, throwError } from 'rxjs'; import { tap, catchError } from 'rxjs/operators'; import { RESTURLCombiner } from '../url-combiner/rest-url-combiner'; 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) export const XSRF_REQUEST_HEADER = 'X-XSRF-TOKEN'; diff --git a/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal-issue/journal-issue-search-result-list-element.component.html b/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal-issue/journal-issue-search-result-list-element.component.html index fa4c06d36a..6d88c9761b 100644 --- a/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal-issue/journal-issue-search-result-list-element.component.html +++ b/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal-issue/journal-issue-search-result-list-element.component.html @@ -1,10 +1,10 @@ diff --git a/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal-volume/journal-volume-search-result-list-element.component.html b/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal-volume/journal-volume-search-result-list-element.component.html index 71aeb79c35..b7cb645e31 100644 --- a/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal-volume/journal-volume-search-result-list-element.component.html +++ b/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal-volume/journal-volume-search-result-list-element.component.html @@ -1,10 +1,10 @@ diff --git a/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal/journal-search-result-list-element.component.html b/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal/journal-search-result-list-element.component.html index 05db5b8702..988fb2d4b5 100644 --- a/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal/journal-search-result-list-element.component.html +++ b/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal/journal-search-result-list-element.component.html @@ -1,10 +1,10 @@ diff --git a/src/app/entity-groups/journal-entities/journal-entities.module.ts b/src/app/entity-groups/journal-entities/journal-entities.module.ts index e23a729d6a..dc88490eac 100644 --- a/src/app/entity-groups/journal-entities/journal-entities.module.ts +++ b/src/app/entity-groups/journal-entities/journal-entities.module.ts @@ -54,7 +54,7 @@ const ENTRY_COMPONENTS = [ export class JournalEntitiesModule { /** * 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() { return { diff --git a/src/app/entity-groups/research-entities/item-list-elements/search-result-list-elements/project/project-search-result-list-element.component.html b/src/app/entity-groups/research-entities/item-list-elements/search-result-list-elements/project/project-search-result-list-element.component.html index ae1d8c7510..d711ad7c18 100644 --- a/src/app/entity-groups/research-entities/item-list-elements/search-result-list-elements/project/project-search-result-list-element.component.html +++ b/src/app/entity-groups/research-entities/item-list-elements/search-result-list-elements/project/project-search-result-list-element.component.html @@ -1,10 +1,10 @@ diff --git a/src/app/entity-groups/research-entities/research-entities.module.ts b/src/app/entity-groups/research-entities/research-entities.module.ts index 2e91a6f1e7..c9d6c2d5b6 100644 --- a/src/app/entity-groups/research-entities/research-entities.module.ts +++ b/src/app/entity-groups/research-entities/research-entities.module.ts @@ -74,7 +74,7 @@ const COMPONENTS = [ export class ResearchEntitiesModule { /** * 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() { return { diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.html b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.html index 62014f06bd..ccb37eb6ac 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.html +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.html @@ -1,18 +1,24 @@
-
- {{ bitstreamName }} +
+ + {{ bitstreamName }} +
+ {{ bitstream?.firstMetadataValue('dc.description') }} +
- {{ (format$ | async)?.shortDescription }} + + {{ (format$ | async)?.shortDescription }} +
diff --git a/src/app/item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.html b/src/app/item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.html index 589981d3c9..c779c4aafa 100644 --- a/src/app/item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.html +++ b/src/app/item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.html @@ -5,9 +5,10 @@

{{'item.edit.item-mapper.description' | translate}}

- - - + +
diff --git a/src/app/item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts b/src/app/item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts index c15c84a647..b5473fa02d 100644 --- a/src/app/item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts +++ b/src/app/item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts @@ -7,7 +7,7 @@ import { ActivatedRoute, Router } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; 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 { CollectionDataService } from '../../../core/data/collection-data.service'; import { ItemDataService } from '../../../core/data/item-data.service'; diff --git a/src/app/item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.html b/src/app/item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.html index fe46906f47..f5543af971 100644 --- a/src/app/item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.html +++ b/src/app/item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.html @@ -26,7 +26,7 @@
- {{metadata?.value}} + {{metadata?.value}}