mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
Merge remote-tracking branch 'origin/main' into CST-4506_item_embargo
This commit is contained in:
6
.github/workflows/build.yml
vendored
6
.github/workflows/build.yml
vendored
@@ -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
|
||||
|
78
.github/workflows/docker.yml
vendored
Normal file
78
.github/workflows/docker.yml
vendored
Normal 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 }}
|
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -3,5 +3,6 @@
|
||||
"i18n-ally.localesPaths": [
|
||||
"src/assets/i18n",
|
||||
"src/app/core/locale"
|
||||
]
|
||||
],
|
||||
"typescript.tsdk": "node_modules\\typescript\\lib"
|
||||
}
|
@@ -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
|
||||
|
20
LICENSE
20
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.
|
28
NOTICE
Normal file
28
NOTICE
Normal 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.
|
10
README.md
10
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.
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@@ -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
|
||||
}
|
@@ -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.
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
100
package.json
100
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"
|
||||
}
|
||||
}
|
||||
|
93
server.ts
93
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';
|
||||
|
@@ -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<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();
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -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;
|
||||
}),
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
@@ -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 {
|
||||
|
@@ -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 {
|
||||
|
@@ -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 {
|
||||
|
@@ -202,8 +202,8 @@ import { GroupAdministratorGuard } from './core/data/feature-authorization/featu
|
||||
{ path: '**', pathMatch: 'full', component: ThemedPageNotFoundComponent },
|
||||
]}
|
||||
],{
|
||||
onSameUrlNavigation: 'reload',
|
||||
})
|
||||
onSameUrlNavigation: 'reload',
|
||||
})
|
||||
],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
|
@@ -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;
|
||||
|
@@ -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 <head>
|
||||
*
|
||||
@@ -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();
|
||||
|
@@ -10,11 +10,11 @@
|
||||
</nav>
|
||||
|
||||
<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 #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-container>
|
||||
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -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
|
||||
|
@@ -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 {
|
||||
|
@@ -5,9 +5,10 @@
|
||||
<p [innerHTML]="'collection.edit.item-mapper.collection' | translate:{ name: (collectionName$ |async) }" id="collection-name"></p>
|
||||
<p>{{'collection.edit.item-mapper.description' | translate}}</p>
|
||||
|
||||
<ngb-tabset (tabChange)="tabChange($event)" [destroyOnHide]="true" #tabs="ngbTabset">
|
||||
<ngb-tab title="{{'collection.edit.item-mapper.tabs.browse' | translate}}" id="browseTab">
|
||||
<ng-template ngbTabContent>
|
||||
<ul ngbNav (navChange)="tabChange($event)" [destroyOnHide]="true" #tabs="ngbNav" class="nav-tabs">
|
||||
<li [ngbNavItem]="'browseTab'">
|
||||
<a ngbNavLink>{{'collection.edit.item-mapper.tabs.browse' | translate}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<div class="mt-2">
|
||||
<ds-item-select class="mt-2"
|
||||
[key]="'browse'"
|
||||
@@ -21,9 +22,10 @@
|
||||
(cancel)="onCancel()"></ds-item-select>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ngb-tab>
|
||||
<ngb-tab title="{{'collection.edit.item-mapper.tabs.map' | translate}}" id="mapTab">
|
||||
<ng-template ngbTabContent>
|
||||
</li>
|
||||
<li [ngbNavItem]="'mapTab'">
|
||||
<a ngbNavLink>{{'collection.edit.item-mapper.tabs.map' | translate}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<div class="row mt-2">
|
||||
<div class="col-12 col-lg-6">
|
||||
<ds-search-form id="search-form"
|
||||
@@ -52,8 +54,9 @@
|
||||
{{'collection.edit.item-mapper.no-search' | translate}}
|
||||
</div>
|
||||
</ng-template>
|
||||
</ngb-tab>
|
||||
</ngb-tabset>
|
||||
</li>
|
||||
</ul>
|
||||
<div [ngbNavOutlet]="tabs"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -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';
|
||||
|
@@ -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
|
||||
|
@@ -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';
|
||||
|
||||
/**
|
||||
|
3
src/app/core/cache/builders/link.service.ts
vendored
3
src/app/core/cache/builders/link.service.ts
vendored
@@ -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';
|
||||
|
||||
/**
|
||||
|
@@ -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', () => {
|
||||
|
@@ -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';
|
||||
|
@@ -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',
|
||||
})
|
||||
|
@@ -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';
|
||||
|
||||
|
@@ -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.
|
||||
|
@@ -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 */
|
||||
|
@@ -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';
|
||||
|
||||
|
@@ -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';
|
||||
|
||||
/**
|
||||
|
@@ -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 {
|
||||
|
@@ -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';
|
||||
|
@@ -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';
|
||||
|
@@ -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';
|
||||
|
@@ -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';
|
||||
|
@@ -1,10 +1,10 @@
|
||||
<ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge>
|
||||
<ds-truncatable [id]="dso.id">
|
||||
<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>
|
||||
<span *ngIf="linkType == linkTypes.None"
|
||||
class="lead item-list-title"
|
||||
class="lead item-list-title dont-break-out"
|
||||
[innerHTML]="dsoTitle"></span>
|
||||
<span class="text-muted">
|
||||
<ds-truncatable-part [id]="dso.id" [minLines]="1">
|
||||
|
@@ -1,10 +1,10 @@
|
||||
<ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge>
|
||||
<ds-truncatable [id]="dso.id">
|
||||
<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>
|
||||
<span *ngIf="linkType == linkTypes.None"
|
||||
class="lead item-list-title"
|
||||
class="lead item-list-title dont-break-out"
|
||||
[innerHTML]="dsoTitle"></span>
|
||||
<span class="text-muted">
|
||||
<ds-truncatable-part [id]="dso.id" [minLines]="1">
|
||||
|
@@ -1,10 +1,10 @@
|
||||
<ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge>
|
||||
<ds-truncatable [id]="dso.id">
|
||||
<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>
|
||||
<span *ngIf="linkType == linkTypes.None"
|
||||
class="lead item-list-title"
|
||||
class="lead item-list-title dont-break-out"
|
||||
[innerHTML]="dsoTitle"></span>
|
||||
<span class="text-muted">
|
||||
<ds-truncatable-part [id]="dso.id" [minLines]="1">
|
||||
|
@@ -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 {
|
||||
|
@@ -1,10 +1,10 @@
|
||||
<ds-truncatable [id]="dso.id">
|
||||
<ds-type-badge *ngIf="showLabel" [object]="dso"></ds-type-badge>
|
||||
<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>
|
||||
<span *ngIf="linkType == linkTypes.None"
|
||||
class="lead item-list-title"
|
||||
class="lead item-list-title dont-break-out"
|
||||
[innerHTML]="dsoTitle"></span>
|
||||
<!--<span class="text-muted">-->
|
||||
<!--<ds-truncatable-part [id]="dso.id" [minLines]="1">-->
|
||||
|
@@ -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 {
|
||||
|
@@ -1,18 +1,24 @@
|
||||
<ng-template #bitstreamView>
|
||||
<div class="{{columnSizes.columns[0].buildClasses()}} row-element d-flex">
|
||||
<ng-content select="[slot=drag-handle]"></ng-content>
|
||||
<div class="float-left d-flex align-items-center">
|
||||
{{ bitstreamName }}
|
||||
<div class="float-left d-flex align-items-center overflow-hidden">
|
||||
<span class="text-truncate">
|
||||
{{ bitstreamName }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="{{columnSizes.columns[1].buildClasses()}} row-element d-flex align-items-center">
|
||||
<div class="w-100">
|
||||
<span class="text-truncate">
|
||||
{{ bitstream?.firstMetadataValue('dc.description') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="{{columnSizes.columns[2].buildClasses()}} row-element d-flex align-items-center">
|
||||
<div class="text-center w-100">
|
||||
{{ (format$ | async)?.shortDescription }}
|
||||
<span class="text-truncate">
|
||||
{{ (format$ | async)?.shortDescription }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="{{columnSizes.columns[3].buildClasses()}} row-element d-flex align-items-center">
|
||||
|
@@ -5,9 +5,10 @@
|
||||
<p [innerHTML]="'item.edit.item-mapper.item' | translate:{ name: (itemName$ | async) }" id="item-name"></p>
|
||||
<p>{{'item.edit.item-mapper.description' | translate}}</p>
|
||||
|
||||
<ngb-tabset (tabChange)="tabChange($event)" [destroyOnHide]="true" #tabs="ngbTabset">
|
||||
<ngb-tab title="{{'item.edit.item-mapper.tabs.browse' | translate}}" id="browseTab">
|
||||
<ng-template ngbTabContent>
|
||||
<ul ngbNav (navChange)="tabChange($event)" [destroyOnHide]="true" #tabs="ngbNav" class="nav-tabs">
|
||||
<li [ngbNavItem]="'browseTab'">
|
||||
<a ngbNavLink>{{'item.edit.item-mapper.tabs.browse' | translate}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<div class="mt-2">
|
||||
<ds-collection-select class="mt-2"
|
||||
[key]="'browse'"
|
||||
@@ -20,9 +21,10 @@
|
||||
(cancel)="onCancel()"></ds-collection-select>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ngb-tab>
|
||||
<ngb-tab title="{{'item.edit.item-mapper.tabs.map' | translate}}" id="mapTab">
|
||||
<ng-template ngbTabContent>
|
||||
</li>
|
||||
<li [ngbNavItem]="'mapTab'">
|
||||
<a ngbNavLink>{{'item.edit.item-mapper.tabs.map' | translate}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<div class="row mt-2">
|
||||
<div class="col-12 col-lg-6">
|
||||
<ds-search-form id="search-form"
|
||||
@@ -50,8 +52,9 @@
|
||||
{{'item.edit.item-mapper.no-search' | translate}}
|
||||
</div>
|
||||
</ng-template>
|
||||
</ngb-tab>
|
||||
</ngb-tabset>
|
||||
</li>
|
||||
</ul>
|
||||
<div [ngbNavOutlet]="tabs"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -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';
|
||||
|
@@ -26,7 +26,7 @@
|
||||
<td class="w-100">
|
||||
<div class="value-field">
|
||||
<div *ngIf="!(editable | async)">
|
||||
<span>{{metadata?.value}}</span>
|
||||
<span class="dont-break-out">{{metadata?.value}}</span>
|
||||
</div>
|
||||
<div *ngIf="(editable | async)" class="field-container">
|
||||
<textarea class="form-control" type="textarea" attr.aria-labelledby="fieldValue" [(ngModel)]="metadata.value" [dsDebounce]
|
||||
|
@@ -3,7 +3,7 @@ import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { LinkService } from '../../../../core/cache/builders/link.service';
|
||||
import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions';
|
||||
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 {
|
||||
FieldUpdate,
|
||||
FieldUpdates,
|
||||
@@ -30,8 +30,6 @@ import { followLink } from '../../../../shared/utils/follow-link-config.model';
|
||||
import { PaginatedList } from '../../../../core/data/paginated-list.model';
|
||||
import { RemoteData } from '../../../../core/data/remote-data';
|
||||
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 { PaginationService } from '../../../../core/pagination/pagination.service';
|
||||
import { RelationshipTypeService } from '../../../../core/data/relationship-type.service';
|
||||
|
@@ -6,9 +6,8 @@ import {
|
||||
FieldUpdates,
|
||||
RelationshipIdentifiable,
|
||||
} from '../../../core/data/object-updates/object-updates.reducer';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
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 { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component';
|
||||
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<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>
|
||||
</a>
|
||||
</ds-metadata-field-wrapper>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<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>
|
||||
</span>
|
||||
</ds-metadata-field-wrapper>
|
||||
|
@@ -33,7 +33,7 @@ import { NgxGalleryModule } from '@kolkov/ngx-gallery';
|
||||
import { MiradorViewerComponent } from './mirador-viewer/mirador-viewer.component';
|
||||
import { VersionPageComponent } from './version-page/version-page/version-page.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 = [
|
||||
@@ -91,7 +91,7 @@ const DECLARATIONS = [
|
||||
export class ItemPageModule {
|
||||
/**
|
||||
* 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 {
|
||||
|
@@ -1,5 +1,5 @@
|
||||
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 { Bitstream } from '../../../core/shared/bitstream.model';
|
||||
import { MediaViewerItem } from '../../../core/shared/media-viewer-item.model';
|
||||
@@ -55,7 +55,7 @@ describe('MediaViewerImageComponent', () => {
|
||||
]
|
||||
);
|
||||
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports:[],
|
||||
declarations: [MediaViewerImageComponent],
|
||||
|
@@ -1,5 +1,5 @@
|
||||
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 { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
@@ -17,7 +17,7 @@ describe('MediaViewerVideoComponent', () => {
|
||||
let component: MediaViewerVideoComponent;
|
||||
let fixture: ComponentFixture<MediaViewerVideoComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
TranslateModule.forRoot({
|
||||
|
@@ -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 { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||
import { createPaginatedList } from '../../shared/testing/utils.test';
|
||||
@@ -60,7 +60,7 @@ describe('MediaViewerComponent', () => {
|
||||
{ bitstream: mockBitstream, format: 'image', thumbnail: null }
|
||||
);
|
||||
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
TranslateModule.forRoot({
|
||||
|
@@ -3,9 +3,8 @@ import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import { environment } from '../../../environments/environment';
|
||||
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 { of } from 'rxjs';
|
||||
import { isPlatformBrowser } from '@angular/common';
|
||||
import { MiradorViewerService } from './mirador-viewer.service';
|
||||
import { HostWindowService, WidthCategory } from '../../shared/host-window.service';
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Injectable, isDevMode } from '@angular/core';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../../core/shared/operators';
|
||||
import { last, map, switchMap } from 'rxjs/operators';
|
||||
|
@@ -4,7 +4,7 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { Store } from '@ngrx/store';
|
||||
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 { ObjectCacheService } from '../../../../core/cache/object-cache.service';
|
||||
import { BitstreamDataService } from '../../../../core/data/bitstream-data.service';
|
||||
|
@@ -1,17 +1,21 @@
|
||||
<ngb-tabset *ngIf="relationTypes.length > 1" [destroyOnHide]="true" #tabs="ngbTabset" [activeId]="activeTab$ | async" (tabChange)="onTabChange($event)">
|
||||
<ngb-tab *ngFor="let relationType of relationTypes" title="{{'item.page.relationships.' + relationType.label | translate}}" [id]="relationType.filter">
|
||||
<ng-template ngbTabContent>
|
||||
<div class="mt-4">
|
||||
<ds-related-entities-search [item]="item"
|
||||
[relationType]="relationType.filter"
|
||||
[configuration]="relationType.configuration"
|
||||
[searchEnabled]="searchEnabled"
|
||||
[sideBarWidth]="sideBarWidth">
|
||||
</ds-related-entities-search>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ngb-tab>
|
||||
</ngb-tabset>
|
||||
<ng-container *ngIf="relationTypes.length > 1">
|
||||
<ul ngbNav #tabs="ngbNav" [destroyOnHide]="true" [activeId]="activeTab$ | async" (navChange)="onTabChange($event)" class="nav-tabs">
|
||||
<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">
|
||||
<ds-related-entities-search [item]="item"
|
||||
[relationType]="relationType.filter"
|
||||
[configuration]="relationType.configuration"
|
||||
[searchEnabled]="searchEnabled"
|
||||
[sideBarWidth]="sideBarWidth">
|
||||
</ds-related-entities-search>
|
||||
</div>
|
||||
</ng-template>
|
||||
</li>
|
||||
</ul>
|
||||
<div [ngbNavOutlet]="tabs"></div>
|
||||
</ng-container>
|
||||
<div *ngIf="relationTypes.length === 1" class="mt-4">
|
||||
<ds-related-entities-search *ngVar="relationTypes[0] as relationType" [item]="item"
|
||||
[relationType]="relationType.filter"
|
||||
|
@@ -1,5 +1,5 @@
|
||||
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 { By } from '@angular/platform-browser';
|
||||
import { CommonModule } from '@angular/common';
|
||||
@@ -75,7 +75,7 @@ describe('MyDSpaceNewExternalDropdownComponent test', () => {
|
||||
};
|
||||
|
||||
describe('With only one Entity', () => {
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
@@ -126,7 +126,7 @@ describe('MyDSpaceNewExternalDropdownComponent test', () => {
|
||||
});
|
||||
|
||||
describe('With more than one Entity', () => {
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
|
@@ -1,5 +1,5 @@
|
||||
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 { CommonModule } from '@angular/common';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
@@ -79,7 +79,7 @@ describe('MyDSpaceNewSubmissionDropdownComponent test', () => {
|
||||
};
|
||||
|
||||
describe('With only one Entity', () => {
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
@@ -130,7 +130,7 @@ describe('MyDSpaceNewSubmissionDropdownComponent test', () => {
|
||||
});
|
||||
|
||||
describe('With more than one Entity', () => {
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
|
@@ -4,7 +4,7 @@
|
||||
<ds-search-sidebar *ngIf="!(isXsOrSm$ | async)" class="col-3 sidebar-md-sticky"
|
||||
id="search-sidebar"
|
||||
[configurationList]="(configurationList$ | async)"
|
||||
[resultCount]="(resultsRD$ | async)?.payload.totalElements"
|
||||
[resultCount]="(resultsRD$ | async)?.payload?.totalElements"
|
||||
[viewModeList]="viewModeList"
|
||||
[searchOptions]="(searchOptions$ | async)"
|
||||
[sortOptions]="(sortOptions$ | async)"
|
||||
@@ -27,7 +27,7 @@
|
||||
<ds-search-sidebar *ngIf="(isXsOrSm$ | async)" class="col-12"
|
||||
id="search-sidebar-sm"
|
||||
[configurationList]="(configurationList$ | async)"
|
||||
[resultCount]="(resultsRD$ | async)?.payload.totalElements"
|
||||
[resultCount]="(resultsRD$ | async)?.payload?.totalElements"
|
||||
(toggleSidebar)="closeSidebar()"
|
||||
[ngClass]="{'active': !(isSidebarCollapsed() | async)}"
|
||||
[searchOptions]="(searchOptions$ | async)"
|
||||
|
@@ -19,7 +19,7 @@ import { PaginatedSearchOptions } from '../shared/search/paginated-search-option
|
||||
import { SearchService } from '../core/shared/search/search.service';
|
||||
import { SidebarService } from '../shared/sidebar/sidebar.service';
|
||||
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 { SearchConfigurationOption } from '../shared/search/search-switch-configuration/search-configuration-option.model';
|
||||
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 { Context } from '../core/shared/context.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 SEARCH_CONFIG_SERVICE: InjectionToken<SearchConfigurationService> = new InjectionToken<SearchConfigurationService>('searchConfigurationService');
|
||||
@@ -111,8 +111,7 @@ export class MyDSpacePageComponent implements OnInit {
|
||||
constructor(private service: SearchService,
|
||||
private sidebarService: SidebarService,
|
||||
private windowService: HostWindowService,
|
||||
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: MyDSpaceConfigurationService,
|
||||
private routeService: RouteService) {
|
||||
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: MyDSpaceConfigurationService) {
|
||||
this.isXsOrSm$ = this.windowService.isXsOrSm();
|
||||
this.service.setServiceOptions(MyDSpaceResponseParsingService, MyDSpaceRequest);
|
||||
}
|
||||
@@ -134,8 +133,8 @@ export class MyDSpacePageComponent implements OnInit {
|
||||
this.searchOptions$ = this.searchConfigService.paginatedSearchOptions;
|
||||
this.sub = this.searchOptions$.pipe(
|
||||
tap(() => this.resultsRD$.next(null)),
|
||||
switchMap((options: PaginatedSearchOptions) => this.service.search(options).pipe(getFirstSucceededRemoteData())))
|
||||
.subscribe((results) => {
|
||||
switchMap((options: PaginatedSearchOptions) => this.service.search(options).pipe(getFirstCompletedRemoteData())))
|
||||
.subscribe((results: RemoteData<SearchObjects<DSpaceObject>>) => {
|
||||
this.resultsRD$.next(results);
|
||||
});
|
||||
|
||||
|
@@ -10,5 +10,5 @@
|
||||
</ds-viewable-collection>
|
||||
</div>
|
||||
<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>
|
||||
|
@@ -40,9 +40,19 @@ describe('MyDSpaceResultsComponent', () => {
|
||||
expect(fixture.debugElement.query(By.css('a'))).toBeNull();
|
||||
});
|
||||
|
||||
it('should display error message if error is != 400', () => {
|
||||
(comp as any).searchResults = { hasFailed: true, error: { statusCode: 500 } };
|
||||
it('should display error message if error is 500', () => {
|
||||
(comp as any).searchResults = { hasFailed: true, statusCode: 500 };
|
||||
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();
|
||||
});
|
||||
|
||||
|
@@ -58,4 +58,12 @@ export class MyDSpaceResultsComponent {
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
@@ -50,7 +50,7 @@ const ENTRY_COMPONENTS = [
|
||||
export class MyDspaceSearchModule {
|
||||
/**
|
||||
* 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 {
|
||||
|
@@ -58,7 +58,7 @@ const ENTRY_COMPONENTS = [
|
||||
export class NavbarModule {
|
||||
/**
|
||||
* 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 {
|
||||
|
@@ -2,7 +2,7 @@
|
||||
<div class="d-flex">
|
||||
<h2 class="flex-grow-1">{{'process.detail.title' | translate:{id: process?.processId, name: process?.scriptName} }}</h2>
|
||||
<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>
|
||||
<ds-process-detail-field id="process-name" [title]="'process.detail.script'">
|
||||
@@ -23,11 +23,11 @@
|
||||
</div>
|
||||
|
||||
<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 *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 *ngIf="process && process.processStatus" id="process-status" [title]="'process.detail.status' | translate">
|
||||
@@ -35,7 +35,7 @@
|
||||
</ds-process-detail-field>
|
||||
|
||||
<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 }}
|
||||
</button>
|
||||
<ds-loading *ngIf="retrievingOutputLogs$ | async" class="ds-loading" message="{{ 'process.detail.logs.loading' | translate }}"></ds-loading>
|
||||
@@ -47,7 +47,7 @@
|
||||
</p>
|
||||
</ds-process-detail-field>
|
||||
|
||||
<div>
|
||||
<a class="btn btn-light mt-3" [routerLink]="'/processes'">{{'process.detail.back' | translate}}</a>
|
||||
<div style="text-align: right;">
|
||||
<a class="btn btn-outline-secondary mt-3" [routerLink]="'/processes'">{{'process.detail.back' | translate}}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -66,6 +66,11 @@ export class ProcessDetailComponent implements OnInit {
|
||||
*/
|
||||
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,
|
||||
protected router: Router,
|
||||
protected processService: ProcessDataService,
|
||||
|
@@ -26,8 +26,8 @@
|
||||
<td><a [routerLink]="['/processes/', process.processId]">{{process.processId}}</a></td>
|
||||
<td><a [routerLink]="['/processes/', process.processId]">{{process.scriptName}}</a></td>
|
||||
<td *ngVar="(getEpersonName(process.userId) | async) as ePersonName">{{ePersonName}}</td>
|
||||
<td>{{process.startTime | date:dateFormat}}</td>
|
||||
<td>{{process.endTime | date:dateFormat}}</td>
|
||||
<td>{{process.startTime | date:dateFormat:'UTC'}}</td>
|
||||
<td>{{process.endTime | date:dateFormat:'UTC'}}</td>
|
||||
<td>{{process.processStatus}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
@@ -12,12 +12,9 @@ import { By } from '@angular/platform-browser';
|
||||
import { ProcessStatus } from '../processes/process-status.model';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||
import { createPaginatedList } from '../../shared/testing/utils.test';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
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 { DatePipe } from '@angular/common';
|
||||
|
||||
describe('ProcessOverviewComponent', () => {
|
||||
let component: ProcessOverviewComponent;
|
||||
@@ -30,27 +27,29 @@ describe('ProcessOverviewComponent', () => {
|
||||
let processes: Process[];
|
||||
let ePerson: EPerson;
|
||||
|
||||
const pipe = new DatePipe('en-US');
|
||||
|
||||
function init() {
|
||||
processes = [
|
||||
Object.assign(new Process(), {
|
||||
processId: 1,
|
||||
scriptName: 'script-name',
|
||||
startTime: '2020-03-19',
|
||||
endTime: '2020-03-19',
|
||||
startTime: '2020-03-19 00:30:00',
|
||||
endTime: '2020-03-19 23:30:00',
|
||||
processStatus: ProcessStatus.COMPLETED
|
||||
}),
|
||||
Object.assign(new Process(), {
|
||||
processId: 2,
|
||||
scriptName: 'script-name',
|
||||
startTime: '2020-03-20',
|
||||
endTime: '2020-03-20',
|
||||
startTime: '2020-03-20 00:30:00',
|
||||
endTime: '2020-03-20 23:30:00',
|
||||
processStatus: ProcessStatus.FAILED
|
||||
}),
|
||||
Object.assign(new Process(), {
|
||||
processId: 3,
|
||||
scriptName: 'another-script-name',
|
||||
startTime: '2020-03-21',
|
||||
endTime: '2020-03-21',
|
||||
startTime: '2020-03-21 00:30:00',
|
||||
endTime: '2020-03-21 23:30:00',
|
||||
processStatus: ProcessStatus.RUNNING
|
||||
})
|
||||
];
|
||||
@@ -135,14 +134,14 @@ describe('ProcessOverviewComponent', () => {
|
||||
it('should display the start time in the fourth column', () => {
|
||||
rowElements.forEach((rowElement, index) => {
|
||||
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', () => {
|
||||
rowElements.forEach((rowElement, index) => {
|
||||
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'));
|
||||
});
|
||||
});
|
||||
|
||||
|
@@ -23,7 +23,7 @@ export class ProcessPageResolver implements Resolve<RemoteData<Process>> {
|
||||
* or an error if something went wrong
|
||||
*/
|
||||
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(),
|
||||
);
|
||||
}
|
||||
|
@@ -2,7 +2,7 @@ import { Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { map, switchMap } from 'rxjs/operators';
|
||||
import { ItemRequest } from '../../core/shared/item-request.model';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { Observable } from 'rxjs';
|
||||
import {
|
||||
getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload,
|
||||
redirectOn4xx
|
||||
|
@@ -2,7 +2,7 @@ import { Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { map, switchMap } from 'rxjs/operators';
|
||||
import { ItemRequest } from '../../core/shared/item-request.model';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { Observable } from 'rxjs';
|
||||
import {
|
||||
getFirstCompletedRemoteData,
|
||||
getFirstSucceededRemoteDataPayload,
|
||||
|
@@ -2,7 +2,7 @@ import { Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { map, switchMap } from 'rxjs/operators';
|
||||
import { ItemRequest } from '../../core/shared/item-request.model';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { Observable } from 'rxjs';
|
||||
import {
|
||||
getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload,
|
||||
redirectOn4xx
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
|
||||
import { RemoteData } from '../core/data/remote-data';
|
||||
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 { Injectable } from '@angular/core';
|
||||
import { getFirstCompletedRemoteData } from '../core/shared/operators';
|
||||
|
@@ -8,7 +8,7 @@ import { pushInOut } from '../shared/animations/push';
|
||||
import { HostWindowService } from '../shared/host-window.service';
|
||||
import { SidebarService } from '../shared/sidebar/sidebar.service';
|
||||
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 { SEARCH_CONFIG_SERVICE } from '../my-dspace-page/my-dspace-page.component';
|
||||
import { PaginatedSearchOptions } from '../shared/search/paginated-search-options.model';
|
||||
@@ -126,12 +126,12 @@ export class SearchComponent implements OnInit {
|
||||
this.searchOptions$ = this.getSearchOptions();
|
||||
this.sub = this.searchOptions$.pipe(
|
||||
switchMap((options) => this.service.search(
|
||||
options, undefined, true, true, followLink<Item>('thumbnail', { isOptional: true })
|
||||
).pipe(getFirstSucceededRemoteData(), startWith(undefined))
|
||||
options, undefined, false, true, followLink<Item>('thumbnail', { isOptional: true })
|
||||
).pipe(getFirstCompletedRemoteData(), startWith(undefined))
|
||||
)
|
||||
).subscribe((results) => {
|
||||
this.resultsRD$.next(results);
|
||||
});
|
||||
this.resultsRD$.next(results);
|
||||
});
|
||||
|
||||
if (isEmpty(this.configuration$)) {
|
||||
this.configuration$ = this.searchConfigService.getCurrentConfiguration('default');
|
||||
|
@@ -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 { FileService } from '../../core/shared/file.service';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
@@ -77,7 +77,7 @@ describe('BitstreamDownloadPageComponent', () => {
|
||||
}
|
||||
|
||||
describe('init', () => {
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
init();
|
||||
initTestbed();
|
||||
}));
|
||||
@@ -93,7 +93,7 @@ describe('BitstreamDownloadPageComponent', () => {
|
||||
|
||||
describe('bitstream retrieval', () => {
|
||||
describe('when the user is authorized and not logged in', () => {
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
init();
|
||||
(authService.isAuthenticated as jasmine.Spy).and.returnValue(observableOf(false));
|
||||
|
||||
@@ -109,7 +109,7 @@ describe('BitstreamDownloadPageComponent', () => {
|
||||
});
|
||||
});
|
||||
describe('when the user is authorized and logged in', () => {
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
init();
|
||||
initTestbed();
|
||||
}));
|
||||
@@ -123,7 +123,7 @@ describe('BitstreamDownloadPageComponent', () => {
|
||||
});
|
||||
});
|
||||
describe('when the user is not authorized and logged in', () => {
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
init();
|
||||
(authorizationService.isAuthorized as jasmine.Spy).and.returnValue(observableOf(false));
|
||||
initTestbed();
|
||||
@@ -138,7 +138,7 @@ describe('BitstreamDownloadPageComponent', () => {
|
||||
});
|
||||
});
|
||||
describe('when the user is not authorized and not logged in', () => {
|
||||
beforeEach(async(() => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
init();
|
||||
(authService.isAuthenticated as jasmine.Spy).and.returnValue(observableOf(false));
|
||||
(authorizationService.isAuthorized as jasmine.Spy).and.returnValue(observableOf(false));
|
||||
|
@@ -7,10 +7,9 @@ import { Bitstream } from '../../core/shared/bitstream.model';
|
||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||
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 { TranslateService } from '@ngx-translate/core';
|
||||
import { Subscription } from 'rxjs/internal/Subscription';
|
||||
import { EPerson } from '../../core/eperson/models/eperson.model';
|
||||
import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
|
||||
import { ItemRequestDataService } from '../../core/data/item-request-data.service';
|
||||
|
@@ -25,7 +25,7 @@
|
||||
</div>
|
||||
<ul class="list-unstyled">
|
||||
<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>
|
||||
</ul>
|
||||
<div>
|
||||
|
@@ -2,7 +2,7 @@ import { BrowseByComponent } from './browse-by.component';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||
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 { SharedModule } from '../shared.module';
|
||||
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 { createSuccessfulRemoteDataObject$ } from '../remote-data.utils';
|
||||
import { storeModuleConfig } from '../../app.reducer';
|
||||
import { FindListOptions } from '../../core/data/request.models';
|
||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||
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 SpyObj = jasmine.SpyObj;
|
||||
|
||||
@listableObjectComponent(BrowseEntry, ViewMode.ListElement, DEFAULT_CONTEXT, 'custom')
|
||||
@Component({
|
||||
selector: 'ds-browse-entry-list-element',
|
||||
template: ''
|
||||
})
|
||||
class MockThemedBrowseEntryListElementComponent {}
|
||||
|
||||
describe('BrowseByComponent', () => {
|
||||
let comp: BrowseByComponent;
|
||||
let fixture: ComponentFixture<BrowseByComponent>;
|
||||
|
||||
let themeService: ThemeService;
|
||||
|
||||
const mockItems = [
|
||||
Object.assign(new Item(), {
|
||||
id: 'fakeId-1',
|
||||
@@ -59,9 +73,12 @@ describe('BrowseByComponent', () => {
|
||||
});
|
||||
const paginationService = new PaginationServiceStub(paginationConfig);
|
||||
|
||||
let themeService: SpyObj<ThemeService>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
themeService = jasmine.createSpyObj('themeService', {
|
||||
getThemeName: 'dspace',
|
||||
getThemeName$: observableOf('dspace'),
|
||||
});
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
@@ -82,6 +99,7 @@ describe('BrowseByComponent', () => {
|
||||
declarations: [],
|
||||
providers: [
|
||||
{provide: PaginationService, useValue: paginationService},
|
||||
{provide: MockThemedBrowseEntryListElementComponent},
|
||||
{ provide: ThemeService, useValue: themeService },
|
||||
],
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -8,6 +8,7 @@ import { Observable } from 'rxjs';
|
||||
import { ListableObject } from '../object-collection/shared/listable-object.model';
|
||||
import { getStartsWithComponent, StartsWithType } from '../starts-with/starts-with-decorator';
|
||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||
import { ViewMode } from '../../core/shared/view-mode.model';
|
||||
|
||||
@Component({
|
||||
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
|
||||
*/
|
||||
export class BrowseByComponent implements OnInit {
|
||||
|
||||
/**
|
||||
* ViewMode that should be passed to {@link ListableObjectComponentLoaderComponent}.
|
||||
*/
|
||||
viewMode: ViewMode = ViewMode.ListElement;
|
||||
|
||||
/**
|
||||
* The i18n message to display as title
|
||||
*/
|
||||
|
@@ -8,7 +8,7 @@ import { ChipsItem } from './models/chips-item.model';
|
||||
import { UploaderService } from '../uploader/uploader.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Options } from 'sortablejs';
|
||||
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-chips',
|
||||
|
@@ -11,8 +11,8 @@
|
||||
aria-labelledby="dropdownMenuButton"
|
||||
(scroll)="onScroll($event)"
|
||||
infiniteScroll
|
||||
[infiniteScrollDistance]="5"
|
||||
[infiniteScrollThrottle]="300"
|
||||
[infiniteScrollDistance]="1.5"
|
||||
[infiniteScrollThrottle]="0"
|
||||
[infiniteScrollUpDistance]="1.5"
|
||||
[fromRoot]="true"
|
||||
[scrollWindow]="false"
|
||||
@@ -21,7 +21,7 @@
|
||||
<button class="dropdown-item disabled" *ngIf="searchListCollection?.length == 0 && !(isLoading | async)">
|
||||
{{'submission.sections.general.no-collection' | translate}}
|
||||
</button>
|
||||
<ng-container *ngIf="searchListCollection?.length > 0 && !(isLoading | async)">
|
||||
<ng-container *ngIf="searchListCollection?.length > 0">
|
||||
<button *ngFor="let listItem of searchListCollection"
|
||||
class="dropdown-item collection-item"
|
||||
title="{{ listItem.collection.name }}"
|
||||
|
@@ -223,20 +223,20 @@ export class CollectionDropdownComponent implements OnInit, OnDestroy {
|
||||
switchMap((collectionsRD: RemoteData<PaginatedList<Collection>>) => {
|
||||
this.searchComplete.emit();
|
||||
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.emitSelectionEvents(collectionsRD);
|
||||
return observableFrom(collectionsRD.payload.page).pipe(
|
||||
mergeMap((collection: Collection) => collection.parentCommunity.pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
map((community: Community) => ({
|
||||
communities: [{ id: community.id, name: community.name }],
|
||||
collection: { id: collection.id, uuid: collection.id, name: collection.name }
|
||||
})
|
||||
))),
|
||||
reduce((acc: any, value: any) => [...acc, value], []),
|
||||
);
|
||||
}
|
||||
this.emitSelectionEvents(collectionsRD);
|
||||
return observableFrom(collectionsRD.payload.page).pipe(
|
||||
mergeMap((collection: Collection) => collection.parentCommunity.pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
map((community: Community) => ({
|
||||
communities: [{ id: community.id, name: community.name }],
|
||||
collection: { id: collection.id, uuid: collection.id, name: collection.name }
|
||||
})
|
||||
))),
|
||||
reduce((acc: any, value: any) => [...acc, value], []),
|
||||
);
|
||||
} else {
|
||||
this.hasNextPage = false;
|
||||
return observableOf([]);
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||
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';
|
||||
|
||||
@Component({
|
||||
|
@@ -1,11 +1,10 @@
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
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 { VersionHistoryDataService } from '../../../core/data/version-history-data.service';
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
import { map, startWith, switchMap } from 'rxjs/operators';
|
||||
import { of } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-dso-page-version-button',
|
||||
|
@@ -3,7 +3,7 @@
|
||||
class="form-control"
|
||||
(click)="$event.stopPropagation();"
|
||||
placeholder="{{'dso-selector.placeholder' | translate: { type: typesString } }}"
|
||||
[formControl]="input" dsAutoFocus (keyup.enter)="selectSingleResult()">
|
||||
[formControl]="input" ngbAutofocus (keyup.enter)="selectSingleResult()">
|
||||
</div>
|
||||
<div class="dropdown-divider"></div>
|
||||
<div class="scrollable-menu list-group">
|
||||
|
@@ -1,9 +1,8 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
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 { of as observableOf } from 'rxjs';
|
||||
import { METADATA_EXPORT_SCRIPT_NAME, ScriptDataService } from '../../../../core/data/processes/script-data.service';
|
||||
import { Collection } from '../../../../core/shared/collection.model';
|
||||
import { Community } from '../../../../core/shared/community.model';
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user