diff --git a/.eslintrc.json b/.eslintrc.json
index 6d5aa89db7..b95b54b979 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -6,7 +6,8 @@
"eslint-plugin-import",
"eslint-plugin-jsdoc",
"eslint-plugin-deprecation",
- "eslint-plugin-unused-imports"
+ "unused-imports",
+ "eslint-plugin-lodash"
],
"overrides": [
{
@@ -202,7 +203,13 @@
"deprecation/deprecation": "warn",
"import/order": "off",
- "import/no-deprecated": "warn"
+ "import/no-deprecated": "warn",
+ "import/no-namespace": "error",
+ "unused-imports/no-unused-imports": "error",
+ "lodash/import-scope": [
+ "error",
+ "method"
+ ]
}
},
{
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index be15b0a507..e50105b879 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -1,7 +1,7 @@
## References
_Add references/links to any related issues or PRs. These may include:_
-* Fixes #[issue-number]
-* Requires DSpace/DSpace#[pr-number] (if a REST API PR is required to test this)
+* Fixes #`issue-number` (if this fixes an issue ticket)
+* Requires DSpace/DSpace#`pr-number` (if a REST API PR is required to test this)
## Description
Short summary of changes (1-2 sentences).
@@ -19,8 +19,10 @@ List of changes in this PR:
_This checklist provides a reminder of what we are going to look for when reviewing your PR. You need not complete this checklist prior to creating your PR (draft PRs are always welcome). If you are unsure about an item in the checklist, don't hesitate to ask. We're here to help!_
- [ ] My PR is small in size (e.g. less than 1,000 lines of code, not including comments & specs/tests), or I have provided reasons as to why that's not possible.
-- [ ] My PR passes [TSLint](https://palantir.github.io/tslint/) validation using `yarn run lint`
-- [ ] My PR doesn't introduce circular dependencies
+- [ ] My PR passes [ESLint](https://eslint.org/) validation using `yarn lint`
+- [ ] My PR doesn't introduce circular dependencies (verified via `yarn check-circ-deps`)
- [ ] My PR includes [TypeDoc](https://typedoc.org/) comments for _all new (or modified) public methods and classes_. It also includes TypeDoc for large or complex private methods.
- [ ] My PR passes all specs/tests and includes new/updated specs or tests based on the [Code Testing Guide](https://wiki.lyrasis.org/display/DSPACE/Code+Testing+Guide).
-- [ ] If my PR includes new, third-party dependencies (in `package.json`), I've made sure their licenses align with the [DSpace BSD License](https://github.com/DSpace/DSpace/blob/main/LICENSE) based on the [Licensing of Contributions](https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines#CodeContributionGuidelines-LicensingofContributions) documentation.
+- [ ] If my PR includes new libraries/dependencies (in `package.json`), I've made sure their licenses align with the [DSpace BSD License](https://github.com/DSpace/DSpace/blob/main/LICENSE) based on the [Licensing of Contributions](https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines#CodeContributionGuidelines-LicensingofContributions) documentation.
+- [ ] If my PR includes new features or configurations, I've provided basic technical documentation in the PR itself.
+- [ ] If my PR fixes an issue ticket, I've [linked them together](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue).
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 04d426d091..f3b7aff689 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -6,34 +6,39 @@ name: Build
# Run this Build for all pushes / PRs to current branch
on: [push, pull_request]
+permissions:
+ contents: read # to fetch code (actions/checkout)
+
jobs:
tests:
runs-on: ubuntu-latest
env:
# The ci step will test the dspace-angular code against DSpace REST.
# Direct that step to utilize a DSpace REST service that has been started in docker.
- DSPACE_REST_HOST: localhost
+ DSPACE_REST_HOST: 127.0.0.1
DSPACE_REST_PORT: 8080
DSPACE_REST_NAMESPACE: '/server'
DSPACE_REST_SSL: false
+ # Spin up UI on 127.0.0.1 to avoid host resolution issues in e2e tests with Node 18+
+ DSPACE_UI_HOST: 127.0.0.1
# When Chrome version is specified, we pin to a specific version of Chrome
# Comment this out to use the latest release
#CHROME_VERSION: "90.0.4430.212-1"
strategy:
# Create a matrix of Node versions to test against (in parallel)
matrix:
- node-version: [14.x, 16.x]
+ node-version: [16.x, 18.x]
# Do NOT exit immediately if one matrix job fails
fail-fast: false
# These are the actual CI steps to perform per job
steps:
# https://github.com/actions/checkout
- name: Checkout codebase
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
# https://github.com/actions/setup-node
- name: Install Node.js ${{ matrix.node-version }}
- uses: actions/setup-node@v2
+ uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
@@ -58,7 +63,7 @@ jobs:
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Cache Yarn dependencies
- uses: actions/cache@v2
+ uses: actions/cache@v3
with:
# Cache entire Yarn cache directory (see previous step)
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
@@ -85,7 +90,7 @@ jobs:
# Upload coverage reports to Codecov (for one version of Node only)
# https://github.com/codecov/codecov-action
- name: Upload coverage to Codecov.io
- uses: codecov/codecov-action@v2
+ uses: codecov/codecov-action@v3
if: matrix.node-version == '16.x'
# Using docker-compose start backend using CI configuration
@@ -100,7 +105,7 @@ jobs:
# https://github.com/cypress-io/github-action
# (NOTE: to run these e2e tests locally, just use 'ng e2e')
- name: Run e2e tests (integration tests)
- uses: cypress-io/github-action@v2
+ uses: cypress-io/github-action@v4
with:
# Run tests in Chrome, headless mode
browser: chrome
@@ -109,14 +114,14 @@ jobs:
start: yarn run serve:ssr
# Wait for backend & frontend to be available
# NOTE: We use the 'sites' REST endpoint to also ensure the database is ready
- wait-on: http://localhost:8080/server/api/core/sites, http://localhost:4000
+ wait-on: http://127.0.0.1:8080/server/api/core/sites, http://127.0.0.1:4000
# Wait for 2 mins max for everything to respond
wait-on-timeout: 120
# Cypress always creates a video of all e2e tests (whether they succeeded or failed)
# Save those in an Artifact
- name: Upload e2e test videos to Artifacts
- uses: actions/upload-artifact@v2
+ uses: actions/upload-artifact@v3
if: always()
with:
name: e2e-test-videos
@@ -125,7 +130,7 @@ jobs:
# If e2e tests fail, Cypress creates a screenshot of what happened
# Save those in an Artifact
- name: Upload e2e test failure screenshots to Artifacts
- uses: actions/upload-artifact@v2
+ uses: actions/upload-artifact@v3
if: failure()
with:
name: e2e-test-screenshots
@@ -144,7 +149,7 @@ jobs:
run: |
nohup yarn run serve:ssr &
printf 'Waiting for app to start'
- until curl --output /dev/null --silent --head --fail http://localhost:4000/home; do
+ until curl --output /dev/null --silent --head --fail http://127.0.0.1:4000/home; do
printf '.'
sleep 2
done
@@ -155,7 +160,7 @@ jobs:
# This step also prints entire HTML of homepage for easier debugging if grep fails.
- name: Verify SSR (server-side rendering)
run: |
- result=$(wget -O- -q http://localhost:4000/home)
+ result=$(wget -O- -q http://127.0.0.1:4000/home)
echo "$result"
echo "$result" | grep -oE " ]*>" | grep DSpace
diff --git a/.github/workflows/codescan.yml b/.github/workflows/codescan.yml
new file mode 100644
index 0000000000..35a2e2d24a
--- /dev/null
+++ b/.github/workflows/codescan.yml
@@ -0,0 +1,49 @@
+# DSpace CodeQL code scanning configuration for GitHub
+# https://docs.github.com/en/code-security/code-scanning
+#
+# NOTE: Code scanning must be run separate from our default build.yml
+# because CodeQL requires a fresh build with all tests *disabled*.
+name: "Code Scanning"
+
+# Run this code scan for all pushes / PRs to main branch. Also run once a week.
+on:
+ push:
+ branches: [ main ]
+ pull_request:
+ branches: [ main ]
+ # Don't run if PR is only updating static documentation
+ paths-ignore:
+ - '**/*.md'
+ - '**/*.txt'
+ schedule:
+ - cron: "37 0 * * 1"
+
+jobs:
+ analyze:
+ name: Analyze Code
+ runs-on: ubuntu-latest
+ # Limit permissions of this GitHub action. Can only write to security-events
+ permissions:
+ actions: read
+ contents: read
+ security-events: write
+
+ steps:
+ # https://github.com/actions/checkout
+ - name: Checkout repository
+ uses: actions/checkout@v3
+
+ # Initializes the CodeQL tools for scanning.
+ # https://github.com/github/codeql-action
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v2
+ with:
+ languages: javascript
+
+ # Autobuild attempts to build any compiled languages
+ - name: Autobuild
+ uses: github/codeql-action/autobuild@v2
+
+ # Perform GitHub Code Scanning.
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v2
\ No newline at end of file
diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
index 64303ca8bb..908c5c34fd 100644
--- a/.github/workflows/docker.yml
+++ b/.github/workflows/docker.yml
@@ -12,6 +12,9 @@ on:
- 'dspace-**'
pull_request:
+permissions:
+ contents: read # to fetch code (actions/checkout)
+
jobs:
docker:
# Ensure this job never runs on forked repos. It's only executed for 'dspace/dspace-angular'
@@ -39,11 +42,11 @@ jobs:
steps:
# https://github.com/actions/checkout
- name: Checkout codebase
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
# https://github.com/docker/setup-buildx-action
- name: Setup Docker Buildx
- uses: docker/setup-buildx-action@v1
+ uses: docker/setup-buildx-action@v2
# https://github.com/docker/setup-qemu-action
- name: Set up QEMU emulation to build for multiple architectures
@@ -53,7 +56,7 @@ jobs:
- 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
+ uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
@@ -65,7 +68,7 @@ jobs:
# 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
+ uses: docker/metadata-action@v4
with:
images: dspace/dspace-angular
tags: ${{ env.IMAGE_TAGS }}
@@ -74,7 +77,7 @@ jobs:
# https://github.com/docker/build-push-action
- name: Build and push 'dspace-angular' image
id: docker_build
- uses: docker/build-push-action@v2
+ uses: docker/build-push-action@v3
with:
context: .
file: ./Dockerfile
diff --git a/.github/workflows/issue_opened.yml b/.github/workflows/issue_opened.yml
index 6b9a273ab6..5d7c1c30f7 100644
--- a/.github/workflows/issue_opened.yml
+++ b/.github/workflows/issue_opened.yml
@@ -5,25 +5,22 @@ on:
issues:
types: [opened]
+permissions: {}
jobs:
automation:
runs-on: ubuntu-latest
steps:
# Add the new issue to a project board, if it needs triage
- # See https://github.com/marketplace/actions/create-project-card-action
- - name: Add issue to project board
+ # See https://github.com/actions/add-to-project
+ - name: Add issue to triage board
# Only add to project board if issue is flagged as "needs triage" or has no labels
# NOTE: By default we flag new issues as "needs triage" in our issue template
if: (contains(github.event.issue.labels.*.name, 'needs triage') || join(github.event.issue.labels.*.name) == '')
- uses: technote-space/create-project-card-action@v1
+ uses: actions/add-to-project@v0.3.0
# Note, the authentication token below is an ORG level Secret.
- # It must be created/recreated manually via a personal access token with "public_repo" and "admin:org" permissions
+ # It must be created/recreated manually via a personal access token with admin:org, project, public_repo permissions
# See: https://docs.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token#permissions-for-the-github_token
# This is necessary because the "DSpace Backlog" project is an org level project (i.e. not repo specific)
with:
- GITHUB_TOKEN: ${{ secrets.ORG_PROJECT_TOKEN }}
- PROJECT: DSpace Backlog
- COLUMN: Triage
- CHECK_ORG_PROJECT: true
- # Ignore errors
- continue-on-error: true
+ github-token: ${{ secrets.TRIAGE_PROJECT_TOKEN }}
+ project-url: https://github.com/orgs/DSpace/projects/24
diff --git a/.github/workflows/label_merge_conflicts.yml b/.github/workflows/label_merge_conflicts.yml
index dcbab18f1b..a840a4fd17 100644
--- a/.github/workflows/label_merge_conflicts.yml
+++ b/.github/workflows/label_merge_conflicts.yml
@@ -5,21 +5,32 @@ name: Check for merge conflicts
# NOTE: This means merge conflicts are only checked for when a PR is merged to main.
on:
push:
- branches:
- - main
+ branches: [ main ]
+ # So that the `conflict_label_name` is removed if conflicts are resolved,
+ # we allow this to run for `pull_request_target` so that github secrets are available.
+ pull_request_target:
+ types: [ synchronize ]
+
+permissions: {}
jobs:
triage:
+ # 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
+ permissions:
+ pull-requests: write
steps:
- # See: https://github.com/mschilde/auto-label-merge-conflicts/
+ # See: https://github.com/prince-chrismc/label-merge-conflicts-action
- name: Auto-label PRs with merge conflicts
- uses: mschilde/auto-label-merge-conflicts@v2.0
+ uses: prince-chrismc/label-merge-conflicts-action@v2
# Add "merge conflict" label if a merge conflict is detected. Remove it when resolved.
# Note, the authentication token is created automatically
# See: https://docs.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token
with:
- CONFLICT_LABEL_NAME: 'merge conflict'
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- # Ignore errors
- continue-on-error: true
+ conflict_label_name: 'merge conflict'
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ conflict_comment: |
+ Hi @${author},
+ Conflicts have been detected against the base branch.
+ Please [resolve these conflicts](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/addressing-merge-conflicts/about-merge-conflicts) as soon as you can. Thanks!
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000000..4e732302f4
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,46 @@
+# How to Contribute
+
+DSpace is a community built and supported project. We do not have a centralized development or support team, but have a dedicated group of volunteers who help us improve the software, documentation, resources, etc.
+
+* [Contribute new code via a Pull Request](#contribute-new-code-via-a-pull-request)
+* [Contribute documentation](#contribute-documentation)
+* [Help others on mailing lists or Slack](#help-others-on-mailing-lists-or-slack)
+* [Join a working or interest group](#join-a-working-or-interest-group)
+
+## Contribute new code via a Pull Request
+
+We accept [GitHub Pull Requests (PRs)](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork) at any time from anyone.
+Contributors to each release are recognized in our [Release Notes](https://wiki.lyrasis.org/display/DSDOC7x/Release+Notes).
+
+Code Contribution Checklist
+- [ ] PRs _should_ be smaller in size (ideally less than 1,000 lines of code, not including comments & tests)
+- [ ] PRs **must** pass [ESLint](https://eslint.org/) validation using `yarn lint`
+- [ ] PRs **must** not introduce circular dependencies (verified via `yarn check-circ-deps`)
+- [ ] PRs **must** include [TypeDoc](https://typedoc.org/) comments for _all new (or modified) public methods and classes_. Large or complex private methods should also have TypeDoc.
+- [ ] PRs **must** pass all automated pecs/tests and includes new/updated specs or tests based on the [Code Testing Guide](https://wiki.lyrasis.org/display/DSPACE/Code+Testing+Guide).
+- [ ] If a PR includes new libraries/dependencies (in `package.json`), then their software licenses **must** align with the [DSpace BSD License](https://github.com/DSpace/dspace-angular/blob/main/LICENSE) based on the [Licensing of Contributions](https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines#CodeContributionGuidelines-LicensingofContributions) documentation.
+- [ ] Basic technical documentation _should_ be provided for any new features or configuration, either in the PR itself or in the DSpace Wiki documentation.
+- [ ] If a PR fixes an issue ticket, please [link them together](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue).
+
+Additional details on the code contribution process can be found in our [Code Contribution Guidelines](https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines)
+
+## Contribute documentation
+
+DSpace Documentation is a collaborative effort in a shared Wiki. The latest documentation is at https://wiki.lyrasis.org/display/DSDOC7x
+
+If you find areas of the DSpace Documentation which you wish to improve, please request a Wiki account by emailing wikihelp@lyrasis.org.
+Once you have an account setup, contact @tdonohue (via [Slack](https://wiki.lyrasis.org/display/DSPACE/Slack) or email) for access to edit our Documentation.
+
+## Help others on mailing lists or Slack
+
+DSpace has our own [Slack](https://wiki.lyrasis.org/display/DSPACE/Slack) community and [Mailing Lists](https://wiki.lyrasis.org/display/DSPACE/Mailing+Lists) where discussions take place and questions are answered.
+Anyone is welcome to join and help others. We just ask you to follow our [Code of Conduct](https://www.lyrasis.org/about/Pages/Code-of-Conduct.aspx) (adopted via LYRASIS).
+
+## Join a working or interest group
+
+Most of the work in building/improving DSpace comes via [Working Groups](https://wiki.lyrasis.org/display/DSPACE/DSpace+Working+Groups) or [Interest Groups](https://wiki.lyrasis.org/display/DSPACE/DSpace+Interest+Groups).
+
+All working/interest groups are open to anyone to join and participate. A few key groups to be aware of include:
+
+* [DSpace 7 Working Group](https://wiki.lyrasis.org/display/DSPACE/DSpace+7+Working+Group) - This is the main (mostly volunteer) development team. We meet weekly to review our current development [project board](https://github.com/orgs/DSpace/projects), assigning tickets and/or PRs.
+* [DSpace Community Advisory Team (DCAT)](https://wiki.lyrasis.org/display/cmtygp/DSpace+Community+Advisory+Team) - This is an interest group for repository managers/administrators. We meet monthly to discuss DSpace, share tips & provide feedback back to developers.
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index a7c1640d0b..61d960e7d3 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,11 +1,15 @@
# This image will be published as dspace/dspace-angular
# See https://github.com/DSpace/dspace-angular/tree/main/docker for usage details
-FROM node:14-alpine
+FROM node:18-alpine
WORKDIR /app
ADD . /app/
EXPOSE 4000
+# Ensure Python and other build tools are available
+# These are needed to install some node modules, especially on linux/arm64
+RUN apk add --update python3 make g++ && rm -rf /var/cache/apk/*
+
# We run yarn install with an increased network timeout (5min) to avoid "ESOCKETTIMEDOUT" errors from hub.docker.com
# See, for example https://github.com/yarnpkg/yarn/issues/5540
RUN yarn install --network-timeout 300000
diff --git a/README.md b/README.md
index 837cb48004..c90dc1d08f 100644
--- a/README.md
+++ b/README.md
@@ -35,7 +35,7 @@ https://wiki.lyrasis.org/display/DSDOC7x/Installing+DSpace
Quick start
-----------
-**Ensure you're running [Node](https://nodejs.org) `v14.x` or `v16.x`, [npm](https://www.npmjs.com/) >= `v5.x` and [yarn](https://yarnpkg.com) == `v1.x`**
+**Ensure you're running [Node](https://nodejs.org) `v16.x` or `v18.x`, [npm](https://www.npmjs.com/) >= `v5.x` and [yarn](https://yarnpkg.com) == `v1.x`**
```bash
# clone the repo
@@ -90,7 +90,7 @@ Requirements
------------
- [Node.js](https://nodejs.org) and [yarn](https://yarnpkg.com)
-- Ensure you're running node `v14.x` or `v16.x` and yarn == `v1.x`
+- Ensure you're running node `v16.x` or `v18.x` and yarn == `v1.x`
If you have [`nvm`](https://github.com/creationix/nvm#install-script) or [`nvm-windows`](https://github.com/coreybutler/nvm-windows) installed, which is highly recommended, you can run `nvm install --lts && nvm use` to install and start using the latest Node LTS.
@@ -351,7 +351,7 @@ Documentation
Official DSpace documentation is available in the DSpace wiki at https://wiki.lyrasis.org/display/DSDOC7x/
-Some UI specific configuration documentation is also found in the [`./docs`](docs) folder of htis codebase.
+Some UI specific configuration documentation is also found in the [`./docs`](docs) folder of this codebase.
### Building code documentation
@@ -379,10 +379,10 @@ To get the most out of TypeScript, you'll need a TypeScript-aware editor. We've
- [Sublime Text](http://www.sublimetext.com/3)
- [Typescript-Sublime-Plugin](https://github.com/Microsoft/Typescript-Sublime-plugin#installation)
-Collaborating
+Contributing
-------------
-See [the guide on the wiki](https://wiki.lyrasis.org/display/DSPACE/DSpace+7+-+Angular+UI+Development#DSpace7-AngularUIDevelopment-Howtocontribute)
+See [Contributing documentation](CONTRIBUTING.md)
File Structure
--------------
diff --git a/angular.json b/angular.json
index 2ece0c5e7d..b32670ad77 100644
--- a/angular.json
+++ b/angular.json
@@ -25,12 +25,10 @@
}
},
"allowedCommonJsDependencies": [
- "angular2-text-mask",
"cerialize",
"core-js",
"lodash",
"jwt-decode",
- "url-parse",
"uuid",
"webfontloader",
"zone.js"
diff --git a/config/config.example.yml b/config/config.example.yml
index 27400f0041..9abf167b90 100644
--- a/config/config.example.yml
+++ b/config/config.example.yml
@@ -55,6 +55,8 @@ auth:
# Form settings
form:
+ # Sets the spellcheck textarea attribute value
+ spellCheck: true
# NOTE: Map server-side validators to comparative Angular form validators
validatorMap:
required: required
@@ -143,6 +145,9 @@ languages:
- code: nl
label: Nederlands
active: true
+ - code: pl
+ label: Polski
+ active: true
- code: pt-PT
label: Português
active: true
@@ -170,6 +175,10 @@ languages:
- code: el
label: Ελληνικά
active: true
+ - code: uk
+ label: Yкраї́нська
+ active: true
+
# Browse-By Pages
browseBy:
@@ -207,6 +216,11 @@ item:
undoTimeout: 10000 # 10 seconds
# Show the item access status label in items lists
showAccessStatuses: false
+ bitstream:
+ # Number of entries in the bitstream list in the item view page.
+ # Rounded to the nearest size in the list of selectable sizes on the
+ # settings menu. See pageSizeOptions in 'pagination-component-options.model.ts'.
+ pageSize: 5
# Collection Page Config
collection:
@@ -295,4 +309,4 @@ info:
# display in supported metadata fields. By default, only dc.description.abstract is supported.
markdown:
enabled: false
- mathjax: false
\ No newline at end of file
+ mathjax: false
diff --git a/cypress.json b/cypress.json
index 80358eb6dd..3adf7839c2 100644
--- a/cypress.json
+++ b/cypress.json
@@ -5,7 +5,7 @@
"screenshotsFolder": "cypress/screenshots",
"pluginsFile": "cypress/plugins/index.ts",
"fixturesFolder": "cypress/fixtures",
- "baseUrl": "http://localhost:4000",
+ "baseUrl": "http://127.0.0.1:4000",
"retries": {
"runMode": 2,
"openMode": 0
@@ -22,4 +22,4 @@
"DSPACE_TEST_SUBMIT_USER": "dspacedemo+submit@gmail.com",
"DSPACE_TEST_SUBMIT_USER_PASSWORD": "dspace"
}
-}
\ No newline at end of file
+}
diff --git a/cypress/integration/my-dspace.spec.ts b/cypress/integration/my-dspace.spec.ts
index fa923dbcbc..48f44eecb9 100644
--- a/cypress/integration/my-dspace.spec.ts
+++ b/cypress/integration/my-dspace.spec.ts
@@ -4,10 +4,11 @@ import { testA11y } from 'cypress/support/utils';
describe('My DSpace page', () => {
it('should display recent submissions and pass accessibility tests', () => {
- cy.login(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
-
cy.visit('/mydspace');
+ // This page is restricted, so we will be shown the login form. Fill it out & submit.
+ cy.loginViaForm(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
+
cy.get('ds-my-dspace-page').should('exist');
// At least one recent submission should be displayed
@@ -36,10 +37,11 @@ describe('My DSpace page', () => {
});
it('should have a working detailed view that passes accessibility tests', () => {
- cy.login(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
-
cy.visit('/mydspace');
+ // This page is restricted, so we will be shown the login form. Fill it out & submit.
+ cy.loginViaForm(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
+
cy.get('ds-my-dspace-page').should('exist');
// Click button in sidebar to display detailed view
@@ -61,9 +63,11 @@ describe('My DSpace page', () => {
// NOTE: Deleting existing submissions is exercised by submission.spec.ts
it('should let you start a new submission & edit in-progress submissions', () => {
- cy.login(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
cy.visit('/mydspace');
+ // This page is restricted, so we will be shown the login form. Fill it out & submit.
+ cy.loginViaForm(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
+
// Open the New Submission dropdown
cy.get('button[data-test="submission-dropdown"]').click();
// Click on the "Item" type in that dropdown
@@ -131,9 +135,11 @@ describe('My DSpace page', () => {
});
it('should let you import from external sources', () => {
- cy.login(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
cy.visit('/mydspace');
+ // This page is restricted, so we will be shown the login form. Fill it out & submit.
+ cy.loginViaForm(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
+
// Open the New Import dropdown
cy.get('button[data-test="import-dropdown"]').click();
// Click on the "Item" type in that dropdown
diff --git a/cypress/integration/submission.spec.ts b/cypress/integration/submission.spec.ts
index 009c50115b..9eef596b02 100644
--- a/cypress/integration/submission.spec.ts
+++ b/cypress/integration/submission.spec.ts
@@ -6,11 +6,12 @@ describe('New Submission page', () => {
// NOTE: We already test that new submissions can be started from MyDSpace in my-dspace.spec.ts
it('should create a new submission when using /submit path & pass accessibility', () => {
- cy.login(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
-
// Test that calling /submit with collection & entityType will create a new submission
cy.visit('/submit?collection=' + TEST_SUBMIT_COLLECTION_UUID + '&entityType=none');
+ // This page is restricted, so we will be shown the login form. Fill it out & submit.
+ cy.loginViaForm(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
+
// Should redirect to /workspaceitems, as we've started a new submission
cy.url().should('include', '/workspaceitems');
@@ -33,11 +34,12 @@ describe('New Submission page', () => {
});
it('should block submission & show errors if required fields are missing', () => {
- cy.login(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
-
// Create a new submission
cy.visit('/submit?collection=' + TEST_SUBMIT_COLLECTION_UUID + '&entityType=none');
+ // This page is restricted, so we will be shown the login form. Fill it out & submit.
+ cy.loginViaForm(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
+
// Attempt an immediate deposit without filling out any fields
cy.get('button#deposit').click();
@@ -92,11 +94,12 @@ describe('New Submission page', () => {
});
it('should allow for deposit if all required fields completed & file uploaded', () => {
- cy.login(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
-
// Create a new submission
cy.visit('/submit?collection=' + TEST_SUBMIT_COLLECTION_UUID + '&entityType=none');
+ // This page is restricted, so we will be shown the login form. Fill it out & submit.
+ cy.loginViaForm(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
+
// Fill out all required fields (Title, Date)
cy.get('input#dc_title').type('DSpace logo uploaded via e2e tests');
cy.get('input#dc_date_issued_year').type('2022');
diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts
index 30951d46f1..04c217aa0f 100644
--- a/cypress/support/commands.ts
+++ b/cypress/support/commands.ts
@@ -19,6 +19,14 @@ declare global {
* @param password password to login as
*/
login(email: string, password: string): typeof login;
+
+ /**
+ * Login via form before accessing the next page. Useful to fill out login
+ * form when a cy.visit() call is to an a page which requires authentication.
+ * @param email email to login as
+ * @param password password to login as
+ */
+ loginViaForm(email: string, password: string): typeof loginViaForm;
}
}
}
@@ -26,6 +34,8 @@ declare global {
/**
* Login user via REST API directly, and pass authentication token to UI via
* the UI's dsAuthInfo cookie.
+ * WARNING: WHILE THIS METHOD WORKS, OCCASIONALLY RANDOM AUTHENTICATION ERRORS OCCUR.
+ * At this time "loginViaForm()" seems more consistent/stable.
* @param email email to login as
* @param password password to login as
*/
@@ -81,3 +91,20 @@ function login(email: string, password: string): void {
}
// Add as a Cypress command (i.e. assign to 'cy.login')
Cypress.Commands.add('login', login);
+
+
+/**
+ * Login user via displayed login form
+ * @param email email to login as
+ * @param password password to login as
+ */
+ function loginViaForm(email: string, password: string): void {
+ // Enter email
+ cy.get('ds-log-in [data-test="email"]').type(email);
+ // Enter password
+ cy.get('ds-log-in [data-test="password"]').type(password);
+ // Click login button
+ cy.get('ds-log-in [data-test="login-button"]').click();
+}
+// Add as a Cypress command (i.e. assign to 'cy.loginViaForm')
+Cypress.Commands.add('loginViaForm', loginViaForm);
\ No newline at end of file
diff --git a/docker/docker-compose-ci.yml b/docker/docker-compose-ci.yml
index dbe9500499..ef84c14f43 100644
--- a/docker/docker-compose-ci.yml
+++ b/docker/docker-compose-ci.yml
@@ -24,8 +24,8 @@ services:
# __D__ => "-" (e.g. google__D__metadata => google-metadata)
# dspace.dir, dspace.server.url and dspace.ui.url
dspace__P__dir: /dspace
- dspace__P__server__P__url: http://localhost:8080/server
- dspace__P__ui__P__url: http://localhost:4000
+ dspace__P__server__P__url: http://127.0.0.1:8080/server
+ dspace__P__ui__P__url: http://127.0.0.1:4000
# db.url: Ensure we are using the 'dspacedb' image for our database
db__P__url: 'jdbc:postgresql://dspacedb:5432/dspace'
# solr.server: Ensure we are using the 'dspacesolr' image for Solr
diff --git a/package.json b/package.json
index eb2059ef3a..52b089be37 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "dspace-angular",
- "version": "7.4.0",
+ "version": "7.5.0-next",
"scripts": {
"ng": "ng",
"config:watch": "nodemon",
@@ -54,18 +54,18 @@
"ts-node": "10.2.1"
},
"dependencies": {
- "@angular/animations": "~13.2.6",
+ "@angular/animations": "~13.3.12",
"@angular/cdk": "^13.2.6",
- "@angular/common": "~13.2.6",
- "@angular/compiler": "~13.2.6",
- "@angular/core": "~13.2.6",
- "@angular/forms": "~13.2.6",
- "@angular/localize": "13.2.6",
- "@angular/platform-browser": "~13.2.6",
- "@angular/platform-browser-dynamic": "~13.2.6",
- "@angular/platform-server": "~13.2.6",
- "@angular/router": "~13.2.6",
- "@babel/runtime": "^7.17.2",
+ "@angular/common": "~13.3.12",
+ "@angular/compiler": "~13.3.12",
+ "@angular/core": "~13.3.12",
+ "@angular/forms": "~13.3.12",
+ "@angular/localize": "13.3.12",
+ "@angular/platform-browser": "~13.3.12",
+ "@angular/platform-browser-dynamic": "~13.3.12",
+ "@angular/platform-server": "~13.3.12",
+ "@angular/router": "~13.3.12",
+ "@babel/runtime": "7.17.2",
"@kolkov/ngx-gallery": "^2.0.1",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
@@ -77,15 +77,15 @@
"@ngrx/store": "^13.0.2",
"@nguniversal/express-engine": "^13.0.2",
"@ngx-translate/core": "^13.0.0",
- "@nicky-lenaers/ngx-scroll-to": "^9.0.0",
+ "@nicky-lenaers/ngx-scroll-to": "^13.0.0",
"@types/grecaptcha": "^3.0.4",
"angular-idle-preload": "3.0.0",
"angulartics2": "^12.0.0",
"axios": "^0.27.2",
- "bootstrap": "4.3.1",
- "caniuse-lite": "^1.0.30001165",
+ "bootstrap": "^4.6.1",
"cerialize": "0.1.18",
"cli-progress": "^3.8.0",
+ "colors": "^1.4.0",
"compression": "^1.7.4",
"cookie-parser": "1.4.5",
"core-js": "^3.7.0",
@@ -95,98 +95,81 @@
"express": "^4.17.1",
"express-rate-limit": "^5.1.3",
"fast-json-patch": "^3.0.0-1",
- "file-saver": "^2.0.5",
"filesize": "^6.1.0",
- "font-awesome": "4.7.0",
"http-proxy-middleware": "^1.0.5",
- "https": "1.0.0",
"js-cookie": "2.2.1",
"js-yaml": "^4.1.0",
"json5": "^2.1.3",
"jsonschema": "1.4.0",
"jwt-decode": "^3.1.2",
- "klaro": "^0.7.10",
+ "klaro": "^0.7.18",
"lodash": "^4.17.21",
"markdown-it": "^13.0.1",
"markdown-it-mathjax3": "^4.3.1",
"mirador": "^3.3.0",
"mirador-dl-plugin": "^0.13.0",
"mirador-share-plugin": "^0.11.0",
- "moment": "^2.29.4",
"morgan": "^1.10.0",
"ng-mocks": "^13.1.1",
"ng2-file-upload": "1.4.0",
"ng2-nouislider": "^1.8.3",
"ngx-infinite-scroll": "^10.0.1",
- "ngx-moment": "^5.0.0",
"ngx-pagination": "5.0.0",
"ngx-sortablejs": "^11.1.0",
- "ngx-ui-switch": "^11.0.1",
+ "ngx-ui-switch": "^13.0.2",
"nouislider": "^14.6.3",
"pem": "1.14.4",
- "postcss-cli": "^9.1.0",
"prop-types": "^15.7.2",
"react-copy-to-clipboard": "^5.0.1",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.5.5",
"sanitize-html": "^2.7.2",
"sortablejs": "1.13.0",
- "tslib": "^2.0.0",
- "url-parse": "^1.5.6",
"uuid": "^8.3.2",
"webfontloader": "1.6.28",
"zone.js": "~0.11.5"
},
"devDependencies": {
"@angular-builders/custom-webpack": "~13.1.0",
- "@angular-devkit/build-angular": "~13.2.6",
+ "@angular-devkit/build-angular": "~13.3.10",
"@angular-eslint/builder": "13.1.0",
"@angular-eslint/eslint-plugin": "13.1.0",
"@angular-eslint/eslint-plugin-template": "13.1.0",
"@angular-eslint/schematics": "13.1.0",
"@angular-eslint/template-parser": "13.1.0",
- "@angular/cli": "~13.2.6",
- "@angular/compiler-cli": "~13.2.6",
- "@angular/language-service": "~13.2.6",
+ "@angular/cli": "~13.3.10",
+ "@angular/compiler-cli": "~13.3.12",
+ "@angular/language-service": "~13.3.12",
"@cypress/schematic": "^1.5.0",
- "@fortawesome/fontawesome-free": "^5.5.0",
+ "@fortawesome/fontawesome-free": "^6.2.1",
"@ngrx/store-devtools": "^13.0.2",
"@ngtools/webpack": "^13.2.6",
- "@nguniversal/builders": "^13.0.2",
+ "@nguniversal/builders": "^13.1.1",
"@types/deep-freeze": "0.1.2",
"@types/express": "^4.17.9",
- "@types/file-saver": "^2.0.1",
"@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",
"@types/sanitize-html": "^2.6.2",
"@typescript-eslint/eslint-plugin": "5.11.0",
"@typescript-eslint/parser": "5.11.0",
- "axe-core": "^4.3.3",
+ "axe-core": "^4.4.3",
"compression-webpack-plugin": "^9.2.0",
"copy-webpack-plugin": "^6.4.1",
"cross-env": "^7.0.3",
- "css-loader": "^6.2.0",
- "css-minimizer-webpack-plugin": "^3.4.1",
- "cssnano": "^5.0.6",
- "cypress": "9.5.1",
+ "cypress": "9.7.0",
"cypress-axe": "^0.14.0",
- "debug-loader": "^0.0.1",
"deep-freeze": "0.0.1",
- "dotenv": "^8.2.0",
"eslint": "^8.2.0",
"eslint-plugin-deprecation": "^1.3.2",
"eslint-plugin-import": "^2.25.4",
- "eslint-plugin-jsdoc": "^38.0.6",
+ "eslint-plugin-jsdoc": "^39.6.4",
+ "eslint-plugin-lodash": "^7.4.0",
"eslint-plugin-unused-imports": "^2.0.0",
"express-static-gzip": "^2.1.5",
- "fork-ts-checker-webpack-plugin": "^6.0.3",
- "html-loader": "^1.3.2",
"jasmine-core": "^3.8.0",
"jasmine-marbles": "0.9.2",
- "jasmine-spec-reporter": "~5.0.0",
"karma": "^6.3.14",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage-istanbul-reporter": "~3.0.2",
@@ -194,26 +177,20 @@
"karma-jasmine-html-reporter": "^1.5.0",
"karma-mocha-reporter": "2.2.5",
"ngx-mask": "^13.1.7",
- "nodemon": "^2.0.15",
+ "nodemon": "^2.0.20",
"postcss": "^8.1",
"postcss-apply": "0.12.0",
"postcss-import": "^14.0.0",
"postcss-loader": "^4.0.3",
"postcss-preset-env": "^7.4.2",
"postcss-responsive-type": "1.0.0",
- "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": "^8.0.2",
- "sass": "~1.32.6",
+ "sass": "~1.33.0",
"sass-loader": "^12.6.0",
"sass-resources-loader": "^2.1.1",
- "string-replace-loader": "^3.1.0",
- "terser-webpack-plugin": "^2.3.1",
- "ts-loader": "^5.2.0",
"ts-node": "^8.10.2",
"typescript": "~4.5.5",
"webpack": "^5.69.1",
diff --git a/scripts/base-href.ts b/scripts/base-href.ts
index aee547b46d..7212e1c516 100644
--- a/scripts/base-href.ts
+++ b/scripts/base-href.ts
@@ -1,4 +1,4 @@
-import * as fs from 'fs';
+import { existsSync, writeFileSync } from 'fs';
import { join } from 'path';
import { AppConfig } from '../src/config/app-config.interface';
@@ -16,7 +16,7 @@ const appConfig: AppConfig = buildAppConfig();
const angularJsonPath = join(process.cwd(), 'angular.json');
-if (!fs.existsSync(angularJsonPath)) {
+if (!existsSync(angularJsonPath)) {
console.error(`Error:\n${angularJsonPath} does not exist\n`);
process.exit(1);
}
@@ -30,7 +30,7 @@ try {
angularJson.projects['dspace-angular'].architect.build.options.baseHref = baseHref;
- fs.writeFileSync(angularJsonPath, JSON.stringify(angularJson, null, 2) + '\n');
+ writeFileSync(angularJsonPath, JSON.stringify(angularJson, null, 2) + '\n');
} catch (e) {
console.error(e);
}
diff --git a/scripts/env-to-yaml.ts b/scripts/env-to-yaml.ts
index c2dd1cf0ca..6e8153f4c1 100644
--- a/scripts/env-to-yaml.ts
+++ b/scripts/env-to-yaml.ts
@@ -1,5 +1,5 @@
-import * as fs from 'fs';
-import * as yaml from 'js-yaml';
+import { existsSync, writeFileSync } from 'fs';
+import { dump } from 'js-yaml';
import { join } from 'path';
/**
@@ -18,7 +18,7 @@ if (args[0] === undefined) {
const envFullPath = join(process.cwd(), args[0]);
-if (!fs.existsSync(envFullPath)) {
+if (!existsSync(envFullPath)) {
console.error(`Error:\n${envFullPath} does not exist\n`);
process.exit(1);
}
@@ -26,10 +26,10 @@ if (!fs.existsSync(envFullPath)) {
try {
const env = require(envFullPath).environment;
- const config = yaml.dump(env);
+ const config = dump(env);
if (args[1]) {
const ymlFullPath = join(process.cwd(), args[1]);
- fs.writeFileSync(ymlFullPath, config);
+ writeFileSync(ymlFullPath, config);
} else {
console.log(config);
}
diff --git a/scripts/serve.ts b/scripts/serve.ts
index 99bfc822d4..ee8570a45c 100644
--- a/scripts/serve.ts
+++ b/scripts/serve.ts
@@ -1,4 +1,4 @@
-import * as child from 'child_process';
+import { spawn } from 'child_process';
import { AppConfig } from '../src/config/app-config.interface';
import { buildAppConfig } from '../src/config/config.server';
@@ -9,7 +9,7 @@ const appConfig: AppConfig = buildAppConfig();
* Calls `ng serve` with the following arguments configured for the UI in the app config: host, port, nameSpace, ssl
* Any CLI arguments given to this script are patched through to `ng serve` as well.
*/
-child.spawn(
+spawn(
`ng serve --host ${appConfig.ui.host} --port ${appConfig.ui.port} --serve-path ${appConfig.ui.nameSpace} --ssl ${appConfig.ui.ssl} ${process.argv.slice(2).join(' ')} --configuration development`,
{ stdio: 'inherit', shell: true }
);
diff --git a/scripts/test-rest.ts b/scripts/test-rest.ts
index 51822cf939..9066777c42 100644
--- a/scripts/test-rest.ts
+++ b/scripts/test-rest.ts
@@ -1,5 +1,5 @@
-import * as http from 'http';
-import * as https from 'https';
+import { request } from 'http';
+import { request as https_request } from 'https';
import { AppConfig } from '../src/config/app-config.interface';
import { buildAppConfig } from '../src/config/config.server';
@@ -20,7 +20,7 @@ console.log(`...Testing connection to REST API at ${restUrl}...\n`);
// If SSL enabled, test via HTTPS, else via HTTP
if (appConfig.rest.ssl) {
- const req = https.request(restUrl, (res) => {
+ const req = https_request(restUrl, (res) => {
console.log(`RESPONSE: ${res.statusCode} ${res.statusMessage} \n`);
// We will keep reading data until the 'end' event fires.
// This ensures we don't just read the first chunk.
@@ -39,7 +39,7 @@ if (appConfig.rest.ssl) {
req.end();
} else {
- const req = http.request(restUrl, (res) => {
+ const req = request(restUrl, (res) => {
console.log(`RESPONSE: ${res.statusCode} ${res.statusMessage} \n`);
// We will keep reading data until the 'end' event fires.
// This ensures we don't just read the first chunk.
diff --git a/server.ts b/server.ts
index 81137ad56a..608c214076 100644
--- a/server.ts
+++ b/server.ts
@@ -19,14 +19,17 @@ import 'zone.js/node';
import 'reflect-metadata';
import 'rxjs';
-import axios from 'axios';
-import * as pem from 'pem';
-import * as https from 'https';
+/* eslint-disable import/no-namespace */
import * as morgan from 'morgan';
import * as express from 'express';
-import * as bodyParser from 'body-parser';
import * as compression from 'compression';
import * as expressStaticGzip from 'express-static-gzip';
+/* eslint-enable import/no-namespace */
+
+import axios from 'axios';
+import { createCertificate } from 'pem';
+import { createServer } from 'https';
+import { json } from 'body-parser';
import { existsSync, readFileSync } from 'fs';
import { join } from 'path';
@@ -110,7 +113,7 @@ export function app() {
* Add parser for request bodies
* See [morgan](https://github.com/expressjs/body-parser)
*/
- server.use(bodyParser.json());
+ server.use(json());
// Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine)
server.engine('html', (_, options, callback) =>
@@ -266,7 +269,7 @@ function serverStarted() {
* @param keys SSL credentials
*/
function createHttpsServer(keys) {
- https.createServer({
+ createServer({
key: keys.serviceKey,
cert: keys.certificate
}, app).listen(environment.ui.port, environment.ui.host, () => {
@@ -320,7 +323,7 @@ function start() {
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; // lgtm[js/disabling-certificate-validation]
- pem.createCertificate({
+ createCertificate({
days: 1,
selfSigned: true
}, (error, keys) => {
diff --git a/src/app/access-control/access-control.module.ts b/src/app/access-control/access-control.module.ts
index 891238bbed..afb92a9111 100644
--- a/src/app/access-control/access-control.module.ts
+++ b/src/app/access-control/access-control.module.ts
@@ -10,6 +10,16 @@ import { MembersListComponent } from './group-registry/group-form/members-list/m
import { SubgroupsListComponent } from './group-registry/group-form/subgroup-list/subgroups-list.component';
import { GroupsRegistryComponent } from './group-registry/groups-registry.component';
import { FormModule } from '../shared/form/form.module';
+import { DYNAMIC_ERROR_MESSAGES_MATCHER, DynamicErrorMessagesMatcher } from '@ng-dynamic-forms/core';
+import { AbstractControl } from '@angular/forms';
+
+/**
+ * Condition for displaying error messages on email form field
+ */
+export const ValidateEmailErrorStateMatcher: DynamicErrorMessagesMatcher =
+ (control: AbstractControl, model: any, hasFocus: boolean) => {
+ return (control.touched && !hasFocus) || (control.errors?.emailTaken && hasFocus);
+ };
@NgModule({
imports: [
@@ -26,6 +36,12 @@ import { FormModule } from '../shared/form/form.module';
GroupFormComponent,
SubgroupsListComponent,
MembersListComponent
+ ],
+ providers: [
+ {
+ provide: DYNAMIC_ERROR_MESSAGES_MATCHER,
+ useValue: ValidateEmailErrorStateMatcher
+ },
]
})
/**
diff --git a/src/app/access-control/group-registry/group-form/group-form.component.ts b/src/app/access-control/group-registry/group-form/group-form.component.ts
index b0178f1294..584b28ba1e 100644
--- a/src/app/access-control/group-registry/group-form/group-form.component.ts
+++ b/src/app/access-control/group-registry/group-form/group-form.component.ts
@@ -46,6 +46,7 @@ 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';
+import { environment } from '../../../../environments/environment';
@Component({
selector: 'ds-group-form',
@@ -194,6 +195,7 @@ export class GroupFormComponent implements OnInit, OnDestroy {
label: groupDescription,
name: 'groupDescription',
required: false,
+ spellCheck: environment.form.spellCheck,
});
this.formModel = [
this.groupName,
diff --git a/src/app/admin/admin-import-metadata-page/metadata-import-page.component.ts b/src/app/admin/admin-import-metadata-page/metadata-import-page.component.ts
index deb16c0d73..4236d152dc 100644
--- a/src/app/admin/admin-import-metadata-page/metadata-import-page.component.ts
+++ b/src/app/admin/admin-import-metadata-page/metadata-import-page.component.ts
@@ -1,12 +1,8 @@
import { Location } from '@angular/common';
-import { Component, OnInit } from '@angular/core';
+import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
-import { Observable } from 'rxjs';
-import { map, switchMap } from 'rxjs/operators';
-import { AuthService } from '../../core/auth/auth.service';
import { METADATA_IMPORT_SCRIPT_NAME, ScriptDataService } from '../../core/data/processes/script-data.service';
-import { EPerson } from '../../core/eperson/models/eperson.model';
import { ProcessParameter } from '../../process-page/processes/process-parameter.model';
import { isNotEmpty } from '../../shared/empty.util';
import { NotificationsService } from '../../shared/notifications/notifications.service';
diff --git a/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.html b/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.html
index 6569b2d4c8..c4304806ce 100644
--- a/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.html
+++ b/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.html
@@ -13,32 +13,34 @@
[paginationOptions]="pageConfig"
[pageInfoState]="(bitstreamFormats | async)?.payload"
[collectionSize]="(bitstreamFormats | async)?.payload?.totalElements"
- [hideGear]="true"
+ [hideGear]="false"
[hidePagerWhenSinglePage]="true">
diff --git a/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.spec.ts b/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.spec.ts
index 222804ca28..8a44240b7e 100644
--- a/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.spec.ts
+++ b/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.spec.ts
@@ -129,16 +129,19 @@ describe('BitstreamFormatsComponent', () => {
});
it('should contain the correct formats', () => {
- const unknownName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(1) td:nth-child(2)')).nativeElement;
+ const unknownName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(1) td:nth-child(3)')).nativeElement;
expect(unknownName.textContent).toBe('Unknown');
- const licenseName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(2) td:nth-child(2)')).nativeElement;
+ const UUID: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(1) td:nth-child(2)')).nativeElement;
+ expect(UUID.textContent).toBe('test-uuid-1');
+
+ const licenseName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(2) td:nth-child(3)')).nativeElement;
expect(licenseName.textContent).toBe('License');
- const ccLicenseName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(3) td:nth-child(2)')).nativeElement;
+ const ccLicenseName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(3) td:nth-child(3)')).nativeElement;
expect(ccLicenseName.textContent).toBe('CC License');
- const adobeName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(4) td:nth-child(2)')).nativeElement;
+ const adobeName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(4) td:nth-child(3)')).nativeElement;
expect(adobeName.textContent).toBe('Adobe PDF');
});
});
diff --git a/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.ts b/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.ts
index 3e2f3ce8fc..162bf2bdb2 100644
--- a/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.ts
+++ b/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.ts
@@ -1,12 +1,11 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
-import { combineLatest as observableCombineLatest, Observable, zip } from 'rxjs';
+import { combineLatest as observableCombineLatest, Observable} from 'rxjs';
import { RemoteData } from '../../../core/data/remote-data';
import { PaginatedList } from '../../../core/data/paginated-list.model';
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
import { BitstreamFormat } from '../../../core/shared/bitstream-format.model';
import { BitstreamFormatDataService } from '../../../core/data/bitstream-format-data.service';
import { map, mergeMap, switchMap, take, toArray } from 'rxjs/operators';
-import { hasValue } from '../../../shared/empty.util';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
@@ -29,21 +28,14 @@ export class BitstreamFormatsComponent implements OnInit, OnDestroy {
*/
bitstreamFormats: Observable>>;
- /**
- * The current pagination configuration for the page used by the FindAll method
- * Currently simply renders all bitstream formats
- */
- config: FindListOptions = Object.assign(new FindListOptions(), {
- elementsPerPage: 20
- });
-
/**
* The current pagination configuration for the page
* Currently simply renders all bitstream formats
*/
pageConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
id: 'rbp',
- pageSize: 20
+ pageSize: 20,
+ pageSizeOptions: [20, 40, 60, 80, 100]
});
constructor(private notificationsService: NotificationsService,
@@ -51,7 +43,7 @@ export class BitstreamFormatsComponent implements OnInit, OnDestroy {
private translateService: TranslateService,
private bitstreamFormatService: BitstreamFormatDataService,
private paginationService: PaginationService,
- ) {
+ ) {
}
@@ -149,7 +141,7 @@ export class BitstreamFormatsComponent implements OnInit, OnDestroy {
ngOnInit(): void {
- this.bitstreamFormats = this.paginationService.getFindListOptions(this.pageConfig.id, this.config).pipe(
+ this.bitstreamFormats = this.paginationService.getFindListOptions(this.pageConfig.id, this.pageConfig).pipe(
switchMap((findListOptions: FindListOptions) => {
return this.bitstreamFormatService.findAll(findListOptions);
})
diff --git a/src/app/admin/admin-registries/bitstream-formats/format-form/format-form.component.ts b/src/app/admin/admin-registries/bitstream-formats/format-form/format-form.component.ts
index 161cfa7ecf..142f6fb83d 100644
--- a/src/app/admin/admin-registries/bitstream-formats/format-form/format-form.component.ts
+++ b/src/app/admin/admin-registries/bitstream-formats/format-form/format-form.component.ts
@@ -15,6 +15,7 @@ import { Router } from '@angular/router';
import { hasValue, isEmpty } from '../../../../shared/empty.util';
import { TranslateService } from '@ngx-translate/core';
import { getBitstreamFormatsModuleRoute } from '../../admin-registries-routing-paths';
+import { environment } from '../../../../../environments/environment';
/**
* The component responsible for rendering the form to create/edit a bitstream format
@@ -90,6 +91,7 @@ export class FormatFormComponent implements OnInit {
name: 'description',
label: 'admin.registries.bitstream-formats.edit.description.label',
hint: 'admin.registries.bitstream-formats.edit.description.hint',
+ spellCheck: environment.form.spellCheck,
}),
new DynamicSelectModel({
diff --git a/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.spec.ts b/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.spec.ts
index 74bfc5f0a4..944288a7a5 100644
--- a/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.spec.ts
+++ b/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.spec.ts
@@ -19,10 +19,7 @@ import { RestResponse } from '../../../core/cache/response.models';
import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
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 { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
-import { FindListOptions } from '../../../core/data/find-list-options.model';
describe('MetadataRegistryComponent', () => {
let comp: MetadataRegistryComponent;
diff --git a/src/app/admin/admin-registries/metadata-schema/metadata-schema.component.html b/src/app/admin/admin-registries/metadata-schema/metadata-schema.component.html
index 49fed0c847..1a867928f5 100644
--- a/src/app/admin/admin-registries/metadata-schema/metadata-schema.component.html
+++ b/src/app/admin/admin-registries/metadata-schema/metadata-schema.component.html
@@ -25,6 +25,7 @@
+ {{'admin.registries.schema.fields.table.id' | translate}}
{{'admin.registries.schema.fields.table.field' | translate}}
{{'admin.registries.schema.fields.table.scopenote' | translate}}
@@ -39,6 +40,7 @@
(change)="selectMetadataField(field, $event)">
+ {{field.id}}
{{schema?.prefix}}.{{field.element}}. {{field.qualifier}}
{{field.scopeNote}}
diff --git a/src/app/admin/admin-registries/metadata-schema/metadata-schema.component.spec.ts b/src/app/admin/admin-registries/metadata-schema/metadata-schema.component.spec.ts
index c4116dc9e0..2b660a6363 100644
--- a/src/app/admin/admin-registries/metadata-schema/metadata-schema.component.spec.ts
+++ b/src/app/admin/admin-registries/metadata-schema/metadata-schema.component.spec.ts
@@ -23,11 +23,8 @@ import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
import { MetadataField } from '../../../core/metadata/metadata-field.model';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
import { VarDirective } from '../../../shared/utils/var.directive';
-import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
-import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model';
import { PaginationService } from '../../../core/pagination/pagination.service';
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
-import { FindListOptions } from '../../../core/data/find-list-options.model';
describe('MetadataSchemaComponent', () => {
let comp: MetadataSchemaComponent;
@@ -169,10 +166,10 @@ describe('MetadataSchemaComponent', () => {
});
it('should contain the correct fields', () => {
- const editorField: HTMLElement = fixture.debugElement.query(By.css('#metadata-fields tr:nth-child(1) td:nth-child(2)')).nativeElement;
+ const editorField: HTMLElement = fixture.debugElement.query(By.css('#metadata-fields tr:nth-child(1) td:nth-child(3)')).nativeElement;
expect(editorField.textContent).toBe('mock.contributor.editor');
- const illustratorField: HTMLElement = fixture.debugElement.query(By.css('#metadata-fields tr:nth-child(2) td:nth-child(2)')).nativeElement;
+ const illustratorField: HTMLElement = fixture.debugElement.query(By.css('#metadata-fields tr:nth-child(2) td:nth-child(3)')).nativeElement;
expect(illustratorField.textContent).toBe('mock.contributor.illustrator');
});
diff --git a/src/app/admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.spec.ts b/src/app/admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.spec.ts
index 14d5d38199..1e28b62626 100644
--- a/src/app/admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.spec.ts
+++ b/src/app/admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.spec.ts
@@ -2,7 +2,7 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { MenuService } from '../../../shared/menu/menu.service';
import { MenuServiceStub } from '../../../shared/testing/menu-service.stub';
-import { CSSVariableService } from '../../../shared/sass-helper/sass-helper.service';
+import { CSSVariableService } from '../../../shared/sass-helper/css-variable.service';
import { CSSVariableServiceStub } from '../../../shared/testing/css-variable-service.stub';
import { Component } from '@angular/core';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
diff --git a/src/app/admin/admin-sidebar/admin-sidebar.component.spec.ts b/src/app/admin/admin-sidebar/admin-sidebar.component.spec.ts
index b7fa396e80..88efd2a711 100644
--- a/src/app/admin/admin-sidebar/admin-sidebar.component.spec.ts
+++ b/src/app/admin/admin-sidebar/admin-sidebar.component.spec.ts
@@ -6,7 +6,7 @@ import { ScriptDataService } from '../../core/data/processes/script-data.service
import { AdminSidebarComponent } from './admin-sidebar.component';
import { MenuService } from '../../shared/menu/menu.service';
import { MenuServiceStub } from '../../shared/testing/menu-service.stub';
-import { CSSVariableService } from '../../shared/sass-helper/sass-helper.service';
+import { CSSVariableService } from '../../shared/sass-helper/css-variable.service';
import { CSSVariableServiceStub } from '../../shared/testing/css-variable-service.stub';
import { AuthServiceStub } from '../../shared/testing/auth-service.stub';
import { AuthService } from '../../core/auth/auth.service';
@@ -16,7 +16,6 @@ import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { RouterTestingModule } from '@angular/router/testing';
import { ActivatedRoute } from '@angular/router';
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
-import { FeatureID } from '../../core/data/feature-authorization/feature-id';
import createSpy = jasmine.createSpy;
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
import { Item } from '../../core/shared/item.model';
diff --git a/src/app/admin/admin-sidebar/admin-sidebar.component.ts b/src/app/admin/admin-sidebar/admin-sidebar.component.ts
index 8eb288daa2..d1aa05c1fc 100644
--- a/src/app/admin/admin-sidebar/admin-sidebar.component.ts
+++ b/src/app/admin/admin-sidebar/admin-sidebar.component.ts
@@ -5,7 +5,7 @@ import { AuthService } from '../../core/auth/auth.service';
import { slideSidebar } from '../../shared/animations/slide';
import { MenuComponent } from '../../shared/menu/menu.component';
import { MenuService } from '../../shared/menu/menu.service';
-import { CSSVariableService } from '../../shared/sass-helper/sass-helper.service';
+import { CSSVariableService } from '../../shared/sass-helper/css-variable.service';
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
import { MenuID } from '../../shared/menu/menu-id.model';
import { ActivatedRoute } from '@angular/router';
@@ -69,7 +69,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
*/
ngOnInit(): void {
super.ngOnInit();
- this.sidebarWidth = this.variableService.getVariable('sidebarItemsWidth');
+ this.sidebarWidth = this.variableService.getVariable('--ds-sidebar-items-width');
this.authService.isAuthenticated()
.subscribe((loggedIn: boolean) => {
if (loggedIn) {
diff --git a/src/app/admin/admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component.spec.ts b/src/app/admin/admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component.spec.ts
index b1f3a63c06..0f0181c3d5 100644
--- a/src/app/admin/admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component.spec.ts
+++ b/src/app/admin/admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component.spec.ts
@@ -3,7 +3,7 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { ExpandableAdminSidebarSectionComponent } from './expandable-admin-sidebar-section.component';
import { MenuService } from '../../../shared/menu/menu.service';
import { MenuServiceStub } from '../../../shared/testing/menu-service.stub';
-import { CSSVariableService } from '../../../shared/sass-helper/sass-helper.service';
+import { CSSVariableService } from '../../../shared/sass-helper/css-variable.service';
import { CSSVariableServiceStub } from '../../../shared/testing/css-variable-service.stub';
import { of as observableOf } from 'rxjs';
import { Component } from '@angular/core';
diff --git a/src/app/admin/admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component.ts b/src/app/admin/admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component.ts
index 7cd20b15d2..4555c0fa93 100644
--- a/src/app/admin/admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component.ts
+++ b/src/app/admin/admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component.ts
@@ -2,7 +2,7 @@ import { Component, Inject, Injector, OnInit } from '@angular/core';
import { rotate } from '../../../shared/animations/rotate';
import { AdminSidebarSectionComponent } from '../admin-sidebar-section/admin-sidebar-section.component';
import { slide } from '../../../shared/animations/slide';
-import { CSSVariableService } from '../../../shared/sass-helper/sass-helper.service';
+import { CSSVariableService } from '../../../shared/sass-helper/css-variable.service';
import { bgColor } from '../../../shared/animations/bgColor';
import { MenuService } from '../../../shared/menu/menu.service';
import { combineLatest as combineLatestObservable, Observable } from 'rxjs';
@@ -65,7 +65,7 @@ export class ExpandableAdminSidebarSectionComponent extends AdminSidebarSectionC
*/
ngOnInit(): void {
super.ngOnInit();
- this.sidebarActiveBg = this.variableService.getVariable('adminSidebarActiveBg');
+ this.sidebarActiveBg = this.variableService.getVariable('--ds-admin-sidebar-active-bg');
this.sidebarCollapsed = this.menuService.isMenuCollapsed(this.menuID);
this.sidebarPreviewCollapsed = this.menuService.isMenuPreviewCollapsed(this.menuID);
this.expanded = combineLatestObservable(this.active, this.sidebarCollapsed, this.sidebarPreviewCollapsed)
diff --git a/src/app/admin/admin.module.ts b/src/app/admin/admin.module.ts
index 0ddbefd253..dff2e506c3 100644
--- a/src/app/admin/admin.module.ts
+++ b/src/app/admin/admin.module.ts
@@ -10,6 +10,7 @@ import { AdminSearchModule } from './admin-search-page/admin-search.module';
import { AdminSidebarSectionComponent } from './admin-sidebar/admin-sidebar-section/admin-sidebar-section.component';
import { ExpandableAdminSidebarSectionComponent } from './admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component';
import { BatchImportPageComponent } from './admin-import-batch-page/batch-import-page.component';
+import { UploadModule } from '../shared/upload/upload.module';
const ENTRY_COMPONENTS = [
// put only entry components that use custom decorator
@@ -25,7 +26,8 @@ const ENTRY_COMPONENTS = [
AccessControlModule,
AdminSearchModule.withEntryComponents(),
AdminWorkflowModuleModule.withEntryComponents(),
- SharedModule
+ SharedModule,
+ UploadModule,
],
declarations: [
AdminCurationTasksComponent,
diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts
index 422ead99e1..e921c67ace 100644
--- a/src/app/app.component.spec.ts
+++ b/src/app/app.component.spec.ts
@@ -18,7 +18,7 @@ import { AngularticsProviderMock } from './shared/mocks/angulartics-provider.ser
import { AuthServiceMock } from './shared/mocks/auth.service.mock';
import { AuthService } from './core/auth/auth.service';
import { MenuService } from './shared/menu/menu.service';
-import { CSSVariableService } from './shared/sass-helper/sass-helper.service';
+import { CSSVariableService } from './shared/sass-helper/css-variable.service';
import { CSSVariableServiceStub } from './shared/testing/css-variable-service.stub';
import { MenuServiceStub } from './shared/testing/menu-service.stub';
import { HostWindowService } from './shared/host-window.service';
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index 7aa1043da1..ba7b738227 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -25,13 +25,12 @@ import { HostWindowState } from './shared/search/host-window.reducer';
import { NativeWindowRef, NativeWindowService } from './core/services/window.service';
import { isAuthenticationBlocking } from './core/auth/selectors';
import { AuthService } from './core/auth/auth.service';
-import { CSSVariableService } from './shared/sass-helper/sass-helper.service';
+import { CSSVariableService } from './shared/sass-helper/css-variable.service';
import { environment } from '../environments/environment';
import { models } from './core/core.module';
import { ThemeService } from './shared/theme-support/theme.service';
import { IdleModalComponent } from './shared/idle-modal/idle-modal.component';
import { distinctNext } from './core/shared/distinct-next';
-import { ModalBeforeDismiss } from './shared/interfaces/modal-before-dismiss.interface';
@Component({
selector: 'ds-app',
@@ -110,18 +109,8 @@ export class AppComponent implements OnInit, AfterViewInit {
}
private storeCSSVariables() {
- this.cssService.addCSSVariable('xlMin', '1200px');
- this.cssService.addCSSVariable('mdMin', '768px');
- this.cssService.addCSSVariable('lgMin', '576px');
- this.cssService.addCSSVariable('smMin', '0');
- this.cssService.addCSSVariable('adminSidebarActiveBg', '#0f1b28');
- this.cssService.addCSSVariable('sidebarItemsWidth', '250px');
- this.cssService.addCSSVariable('collapsedSidebarWidth', '53.234px');
- this.cssService.addCSSVariable('totalSidebarWidth', '303.234px');
- // const vars = variables.locals || {};
- // Object.keys(vars).forEach((name: string) => {
- // this.cssService.addCSSVariable(name, vars[name]);
- // })
+ this.cssService.clearCSSVariables();
+ this.cssService.addCSSVariables(this.cssService.getCSSVariablesFromStylesheets(this.document));
}
ngAfterViewInit() {
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index 392969d041..750d63beda 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -1,14 +1,12 @@
import { APP_BASE_HREF, CommonModule, DOCUMENT } from '@angular/common';
import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core';
-import { AbstractControl } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { EffectsModule } from '@ngrx/effects';
import { RouterStateSerializer, StoreRouterConnectingModule } from '@ngrx/router-store';
import { MetaReducer, StoreModule, USER_PROVIDED_META_REDUCERS } from '@ngrx/store';
-import { DYNAMIC_ERROR_MESSAGES_MATCHER, DYNAMIC_MATCHER_PROVIDERS, DynamicErrorMessagesMatcher } from '@ng-dynamic-forms/core';
import { TranslateModule } from '@ngx-translate/core';
import { ScrollToModule } from '@nicky-lenaers/ngx-scroll-to';
import { AppRoutingModule } from './app-routing.module';
@@ -28,7 +26,6 @@ import { XsrfInterceptor } from './core/xsrf/xsrf.interceptor';
import { LogInterceptor } from './core/log/log.interceptor';
import { EagerThemesModule } from '../themes/eager-themes.module';
import { APP_CONFIG, AppConfig } from '../config/app-config.interface';
-import { NgxMaskModule } from 'ngx-mask';
import { StoreDevModules } from '../config/store/devtools';
import { RootModule } from './root.module';
@@ -46,14 +43,6 @@ export function getMetaReducers(appConfig: AppConfig): MetaReducer[] {
return appConfig.debug ? [...appMetaReducers, ...debugMetaReducers] : appMetaReducers;
}
-/**
- * Condition for displaying error messages on email form field
- */
-export const ValidateEmailErrorStateMatcher: DynamicErrorMessagesMatcher =
- (control: AbstractControl, model: any, hasFocus: boolean) => {
- return (control.touched && !hasFocus) || (control.errors?.emailTaken && hasFocus);
- };
-
const IMPORTS = [
CommonModule,
SharedModule,
@@ -64,7 +53,6 @@ const IMPORTS = [
ScrollToModule.forRoot(),
NgbModule,
TranslateModule.forRoot(),
- NgxMaskModule.forRoot(),
EffectsModule.forRoot(appEffects),
StoreModule.forRoot(appReducers, storeModuleConfig),
StoreRouterConnectingModule.forRoot(),
@@ -113,11 +101,6 @@ const PROVIDERS = [
useClass: LogInterceptor,
multi: true
},
- {
- provide: DYNAMIC_ERROR_MESSAGES_MATCHER,
- useValue: ValidateEmailErrorStateMatcher
- },
- ...DYNAMIC_MATCHER_PROVIDERS,
];
const DECLARATIONS = [
diff --git a/src/app/app.reducer.ts b/src/app/app.reducer.ts
index 1d6e86463d..f84db92445 100644
--- a/src/app/app.reducer.ts
+++ b/src/app/app.reducer.ts
@@ -1,4 +1,4 @@
-import * as fromRouter from '@ngrx/router-store';
+import { routerReducer, RouterReducerState } from '@ngrx/router-store';
import { ActionReducerMap, createSelector, MemoizedSelector } from '@ngrx/store';
import {
ePeopleRegistryReducer,
@@ -35,7 +35,7 @@ import {
ObjectSelectionListState,
objectSelectionReducer
} from './shared/object-select/object-select.reducer';
-import { cssVariablesReducer, CSSVariablesState } from './shared/sass-helper/sass-helper.reducer';
+import { cssVariablesReducer, CSSVariablesState } from './shared/sass-helper/css-variable.reducer';
import { hostWindowReducer, HostWindowState } from './shared/search/host-window.reducer';
import {
@@ -53,7 +53,7 @@ import { MenusState } from './shared/menu/menus-state.model';
import { correlationIdReducer } from './correlation-id/correlation-id.reducer';
export interface AppState {
- router: fromRouter.RouterReducerState;
+ router: RouterReducerState;
hostWindow: HostWindowState;
forms: FormState;
metadataRegistry: MetadataRegistryState;
@@ -75,7 +75,7 @@ export interface AppState {
}
export const appReducers: ActionReducerMap = {
- router: fromRouter.routerReducer,
+ router: routerReducer,
hostWindow: hostWindowReducer,
forms: formReducer,
metadataRegistry: metadataRegistryReducer,
diff --git a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts
index 9a59df4b95..4906ebd0d4 100644
--- a/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts
+++ b/src/app/bitstream-page/edit-bitstream-page/edit-bitstream-page.component.ts
@@ -26,7 +26,7 @@ import {
import { FormGroup } from '@angular/forms';
import { TranslateService } from '@ngx-translate/core';
import { DynamicCustomSwitchModel } from '../../shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.model';
-import { cloneDeep } from 'lodash';
+import cloneDeep from 'lodash/cloneDeep';
import { BitstreamDataService } from '../../core/data/bitstream-data.service';
import {
getAllSucceededRemoteDataPayload,
diff --git a/src/app/bitstream-page/legacy-bitstream-url.resolver.spec.ts b/src/app/bitstream-page/legacy-bitstream-url.resolver.spec.ts
index 045582cb26..aec8cd22f4 100644
--- a/src/app/bitstream-page/legacy-bitstream-url.resolver.spec.ts
+++ b/src/app/bitstream-page/legacy-bitstream-url.resolver.spec.ts
@@ -1,5 +1,5 @@
import { LegacyBitstreamUrlResolver } from './legacy-bitstream-url.resolver';
-import { of as observableOf, EMPTY } from 'rxjs';
+import { EMPTY } from 'rxjs';
import { BitstreamDataService } from '../core/data/bitstream-data.service';
import { RemoteData } from '../core/data/remote-data';
import { TestScheduler } from 'rxjs/testing';
diff --git a/src/app/breadcrumbs/breadcrumbs.component.html b/src/app/breadcrumbs/breadcrumbs.component.html
index 51524fde48..bff792eeff 100644
--- a/src/app/breadcrumbs/breadcrumbs.component.html
+++ b/src/app/breadcrumbs/breadcrumbs.component.html
@@ -10,7 +10,7 @@
-
+
diff --git a/src/app/browse-by/browse-by-guard.spec.ts b/src/app/browse-by/browse-by-guard.spec.ts
index fc483d87e2..933c95a3cb 100644
--- a/src/app/browse-by/browse-by-guard.spec.ts
+++ b/src/app/browse-by/browse-by-guard.spec.ts
@@ -1,7 +1,6 @@
import { first } from 'rxjs/operators';
import { BrowseByGuard } from './browse-by-guard';
import { of as observableOf } from 'rxjs';
-import { BrowseDefinitionDataService } from '../core/browse/browse-definition-data.service';
import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
import { BrowseDefinition } from '../core/shared/browse-definition.model';
import { BrowseByDataType } from './browse-by-switcher/browse-by-decorator';
diff --git a/src/app/collection-page/collection-form/collection-form.models.ts b/src/app/collection-page/collection-form/collection-form.models.ts
index 37e9d8a9a0..1a009d95a1 100644
--- a/src/app/collection-page/collection-form/collection-form.models.ts
+++ b/src/app/collection-page/collection-form/collection-form.models.ts
@@ -1,5 +1,6 @@
import { DynamicFormControlModel, DynamicInputModel, DynamicTextAreaModel } from '@ng-dynamic-forms/core';
import { DynamicSelectModelConfig } from '@ng-dynamic-forms/core/lib/model/select/dynamic-select.model';
+import { environment } from '../../../environments/environment';
export const collectionFormEntityTypeSelectionConfig: DynamicSelectModelConfig = {
id: 'entityType',
@@ -26,21 +27,26 @@ export const collectionFormModels: DynamicFormControlModel[] = [
new DynamicTextAreaModel({
id: 'description',
name: 'dc.description',
+ spellCheck: environment.form.spellCheck,
}),
new DynamicTextAreaModel({
id: 'abstract',
name: 'dc.description.abstract',
+ spellCheck: environment.form.spellCheck,
}),
new DynamicTextAreaModel({
id: 'rights',
name: 'dc.rights',
+ spellCheck: environment.form.spellCheck,
}),
new DynamicTextAreaModel({
id: 'tableofcontents',
name: 'dc.description.tableofcontents',
+ spellCheck: environment.form.spellCheck,
}),
new DynamicTextAreaModel({
id: 'license',
name: 'dc.rights.license',
+ spellCheck: environment.form.spellCheck,
})
];
diff --git a/src/app/collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts b/src/app/collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts
index 8304b01daa..db844b588f 100644
--- a/src/app/collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts
+++ b/src/app/collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts
@@ -16,7 +16,7 @@ import { Collection } from '../../core/shared/collection.model';
import { RemoteData } from '../../core/data/remote-data';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
-import { ChangeDetectionStrategy, EventEmitter } from '@angular/core';
+import { EventEmitter } from '@angular/core';
import { HostWindowService } from '../../shared/host-window.service';
import { HostWindowServiceStub } from '../../shared/testing/host-window-service.stub';
import { By } from '@angular/platform-browser';
@@ -41,7 +41,7 @@ import {
} from '../../shared/remote-data.utils';
import { createPaginatedList } from '../../shared/testing/utils.test';
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
-import { MyDSpacePageComponent, SEARCH_CONFIG_SERVICE } from '../../my-dspace-page/my-dspace-page.component';
+import { SEARCH_CONFIG_SERVICE } from '../../my-dspace-page/my-dspace-page.component';
import { SearchConfigurationServiceStub } from '../../shared/testing/search-configuration-service.stub';
import { GroupDataService } from '../../core/eperson/group-data.service';
import { LinkHeadService } from '../../core/services/link-head.service';
diff --git a/src/app/collection-page/collection-page-routing.module.ts b/src/app/collection-page/collection-page-routing.module.ts
index 678c745c01..819ee8ca16 100644
--- a/src/app/collection-page/collection-page-routing.module.ts
+++ b/src/app/collection-page/collection-page-routing.module.ts
@@ -72,6 +72,7 @@ import { MenuItemType } from '../shared/menu/menu-item-type.model';
id: 'statistics_collection_:id',
active: true,
visible: true,
+ index: 2,
model: {
type: MenuItemType.LINK,
text: 'menu.section.statistics',
diff --git a/src/app/collection-page/collection-page.module.ts b/src/app/collection-page/collection-page.module.ts
index c35ebf9021..ff49b983ff 100644
--- a/src/app/collection-page/collection-page.module.ts
+++ b/src/app/collection-page/collection-page.module.ts
@@ -25,7 +25,7 @@ import { ComcolModule } from '../shared/comcol/comcol.module';
StatisticsModule.forRoot(),
EditItemPageModule,
CollectionFormModule,
- ComcolModule
+ ComcolModule,
],
declarations: [
CollectionPageComponent,
@@ -38,7 +38,7 @@ import { ComcolModule } from '../shared/comcol/comcol.module';
],
providers: [
SearchService,
- ]
+ ],
})
export class CollectionPageModule {
diff --git a/src/app/collection-page/edit-collection-page/collection-source/collection-source-controls/collection-source-controls.component.html b/src/app/collection-page/edit-collection-page/collection-source/collection-source-controls/collection-source-controls.component.html
index 75a9b22a12..4d7b3e657e 100644
--- a/src/app/collection-page/edit-collection-page/collection-source/collection-source-controls/collection-source-controls.component.html
+++ b/src/app/collection-page/edit-collection-page/collection-source/collection-source-controls/collection-source-controls.component.html
@@ -11,11 +11,11 @@
{{'collection.source.controls.harvest.last' | translate}}
- {{contentSource?.message ? contentSource?.message : 'collection.source.controls.harvest.no-information'|translate }}
+ {{contentSource?.lastHarvested ? contentSource?.lastHarvested : 'collection.source.controls.harvest.no-information'|translate }}
{{'collection.source.controls.harvest.message' | translate}}
- {{contentSource?.lastHarvested ? contentSource?.lastHarvested : 'collection.source.controls.harvest.no-information'|translate }}
+ {{contentSource?.message ? contentSource?.message: 'collection.source.controls.harvest.no-information'|translate }}
{
new DynamicTextAreaModel({
id: 'description',
name: 'dc.description',
+ spellCheck: environment.form.spellCheck,
}),
new DynamicTextAreaModel({
id: 'abstract',
name: 'dc.description.abstract',
+ spellCheck: environment.form.spellCheck,
}),
new DynamicTextAreaModel({
id: 'rights',
name: 'dc.rights',
+ spellCheck: environment.form.spellCheck,
}),
new DynamicTextAreaModel({
id: 'tableofcontents',
name: 'dc.description.tableofcontents',
+ spellCheck: environment.form.spellCheck,
}),
];
diff --git a/src/app/community-page/community-page-routing.module.ts b/src/app/community-page/community-page-routing.module.ts
index 25326448a8..4870d52dd9 100644
--- a/src/app/community-page/community-page-routing.module.ts
+++ b/src/app/community-page/community-page-routing.module.ts
@@ -55,6 +55,7 @@ import { MenuItemType } from '../shared/menu/menu-item-type.model';
id: 'statistics_community_:id',
active: true,
visible: true,
+ index: 2,
model: {
type: MenuItemType.LINK,
text: 'menu.section.statistics',
diff --git a/src/app/community-page/community-page.module.ts b/src/app/community-page/community-page.module.ts
index 7cf2c8db8a..1dd9e82499 100644
--- a/src/app/community-page/community-page.module.ts
+++ b/src/app/community-page/community-page.module.ts
@@ -36,7 +36,7 @@ const DECLARATIONS = [CommunityPageComponent,
CommunityPageRoutingModule,
StatisticsModule.forRoot(),
CommunityFormModule,
- ComcolModule
+ ComcolModule,
],
declarations: [
...DECLARATIONS
diff --git a/src/app/community-page/edit-community-page/edit-community-page.module.ts b/src/app/community-page/edit-community-page/edit-community-page.module.ts
index 2b0fc73f2a..0479ea6bc6 100644
--- a/src/app/community-page/edit-community-page/edit-community-page.module.ts
+++ b/src/app/community-page/edit-community-page/edit-community-page.module.ts
@@ -21,7 +21,7 @@ import { ComcolModule } from '../../shared/comcol/comcol.module';
EditCommunityPageRoutingModule,
CommunityFormModule,
ComcolModule,
- ResourcePoliciesModule
+ ResourcePoliciesModule,
],
declarations: [
EditCommunityPageComponent,
diff --git a/src/app/community-page/sub-collection-list/community-page-sub-collection-list.component.spec.ts b/src/app/community-page/sub-collection-list/community-page-sub-collection-list.component.spec.ts
index 6a9de52f1f..bca3c42a95 100644
--- a/src/app/community-page/sub-collection-list/community-page-sub-collection-list.component.spec.ts
+++ b/src/app/community-page/sub-collection-list/community-page-sub-collection-list.component.spec.ts
@@ -17,9 +17,6 @@ import { PageInfo } from '../../core/shared/page-info.model';
import { HostWindowService } from '../../shared/host-window.service';
import { HostWindowServiceStub } from '../../shared/testing/host-window-service.stub';
import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service';
-import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
-import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
-import { of as observableOf } from 'rxjs';
import { PaginationService } from '../../core/pagination/pagination.service';
import { getMockThemeService } from '../../shared/mocks/theme-service.mock';
import { ThemeService } from '../../shared/theme-support/theme.service';
@@ -29,7 +26,6 @@ import { GroupDataService } from '../../core/eperson/group-data.service';
import { LinkHeadService } from '../../core/services/link-head.service';
import { ConfigurationDataService } from '../../core/data/configuration-data.service';
import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service';
-import { SearchServiceStub } from '../../shared/testing/search-service.stub';
import { ConfigurationProperty } from '../../core/shared/configuration-property.model';
import { createPaginatedList } from '../../shared/testing/utils.test';
import { SearchConfigurationServiceStub } from '../../shared/testing/search-configuration-service.stub';
diff --git a/src/app/community-page/sub-community-list/community-page-sub-community-list.component.spec.ts b/src/app/community-page/sub-community-list/community-page-sub-community-list.component.spec.ts
index c75c5b6f6c..0a14fe6dd1 100644
--- a/src/app/community-page/sub-community-list/community-page-sub-community-list.component.spec.ts
+++ b/src/app/community-page/sub-community-list/community-page-sub-community-list.component.spec.ts
@@ -17,9 +17,6 @@ import { HostWindowService } from '../../shared/host-window.service';
import { HostWindowServiceStub } from '../../shared/testing/host-window-service.stub';
import { CommunityDataService } from '../../core/data/community-data.service';
import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service';
-import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
-import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
-import { of as observableOf } from 'rxjs';
import { PaginationService } from '../../core/pagination/pagination.service';
import { getMockThemeService } from '../../shared/mocks/theme-service.mock';
import { ThemeService } from '../../shared/theme-support/theme.service';
diff --git a/src/app/core/auth/selectors.ts b/src/app/core/auth/selectors.ts
index 1d002b3908..ce8d38d6ba 100644
--- a/src/app/core/auth/selectors.ts
+++ b/src/app/core/auth/selectors.ts
@@ -7,7 +7,6 @@ import { createSelector } from '@ngrx/store';
* notation packages up all of the exports into a single object.
*/
import { AuthState } from './auth.reducer';
-import { AppState } from '../../app.reducer';
import { CoreState } from '../core-state.model';
import { coreSelector } from '../core.selectors';
diff --git a/src/app/core/cache/object-cache.reducer.spec.ts b/src/app/core/cache/object-cache.reducer.spec.ts
index e346a00b61..919edc8e57 100644
--- a/src/app/core/cache/object-cache.reducer.spec.ts
+++ b/src/app/core/cache/object-cache.reducer.spec.ts
@@ -1,3 +1,4 @@
+// eslint-disable-next-line import/no-namespace
import * as deepFreeze from 'deep-freeze';
import { Operation } from 'fast-json-patch';
import { Item } from '../shared/item.model';
diff --git a/src/app/core/cache/server-sync-buffer.reducer.spec.ts b/src/app/core/cache/server-sync-buffer.reducer.spec.ts
index ce159500c6..51ba010c1e 100644
--- a/src/app/core/cache/server-sync-buffer.reducer.spec.ts
+++ b/src/app/core/cache/server-sync-buffer.reducer.spec.ts
@@ -1,3 +1,4 @@
+// eslint-disable-next-line import/no-namespace
import * as deepFreeze from 'deep-freeze';
import { RemoveFromObjectCacheAction } from './object-cache.actions';
import { serverSyncBufferReducer } from './server-sync-buffer.reducer';
diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts
index 3ba0b39a0e..ede23ba43b 100644
--- a/src/app/core/core.module.ts
+++ b/src/app/core/core.module.ts
@@ -2,15 +2,12 @@ import { CommonModule } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core';
-import { DynamicFormLayoutService, DynamicFormService, DynamicFormValidationService } from '@ng-dynamic-forms/core';
import { EffectsModule } from '@ngrx/effects';
import { Action, StoreConfig, StoreModule } from '@ngrx/store';
import { MyDSpaceGuard } from '../my-dspace-page/my-dspace.guard';
import { isNotEmpty } from '../shared/empty.util';
-import { FormBuilderService } from '../shared/form/builder/form-builder.service';
-import { FormService } from '../shared/form/form.service';
import { HostWindowService } from '../shared/host-window.service';
import { MenuService } from '../shared/menu/menu.service';
import { EndpointMockingRestService } from '../shared/mocks/dspace-rest/endpoint-mocking-rest.service';
@@ -23,10 +20,7 @@ import { NotificationsService } from '../shared/notifications/notifications.serv
import { SelectableListService } from '../shared/object-list/selectable-list/selectable-list.service';
import { ObjectSelectService } from '../shared/object-select/object-select.service';
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
-import { CSSVariableService } from '../shared/sass-helper/sass-helper.service';
import { SidebarService } from '../shared/sidebar/sidebar.service';
-import { UploaderService } from '../shared/uploader/uploader.service';
-import { SectionFormOperationsService } from '../submission/sections/form/section-form-operations.service';
import { AuthenticatedGuard } from './auth/authenticated.guard';
import { AuthStatus } from './auth/models/auth-status.model';
import { BrowseService } from './browse/browse.service';
@@ -138,9 +132,6 @@ import {
import { Registration } from './shared/registration.model';
import { MetadataSchemaDataService } from './data/metadata-schema-data.service';
import { MetadataFieldDataService } from './data/metadata-field-data.service';
-import {
- DsDynamicTypeBindRelationService
-} from '../shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service';
import { TokenResponseParsingService } from './auth/token-response-parsing.service';
import { SubmissionCcLicenseDataService } from './submission/submission-cc-license-data.service';
import { SubmissionCcLicence } from './submission/models/submission-cc-license.model';
@@ -150,7 +141,6 @@ import { VocabularyEntry } from './submission/vocabularies/models/vocabulary-ent
import { Vocabulary } from './submission/vocabularies/models/vocabulary.model';
import { VocabularyEntryDetail } from './submission/vocabularies/models/vocabulary-entry-detail.model';
import { VocabularyService } from './submission/vocabularies/vocabulary.service';
-import { VocabularyTreeviewService } from '../shared/vocabulary-treeview/vocabulary-treeview.service';
import { ConfigurationDataService } from './data/configuration-data.service';
import { ConfigurationProperty } from './shared/configuration-property.model';
import { ReloadGuard } from './reload/reload.guard';
@@ -211,12 +201,6 @@ const PROVIDERS = [
DSOResponseParsingService,
{ provide: MOCK_RESPONSE_MAP, useValue: mockResponseMap },
{ provide: DspaceRestService, useFactory: restServiceFactory, deps: [MOCK_RESPONSE_MAP, HttpClient] },
- DynamicFormLayoutService,
- DynamicFormService,
- DynamicFormValidationService,
- FormBuilderService,
- SectionFormOperationsService,
- FormService,
EPersonDataService,
LinkHeadService,
HALEndpointService,
@@ -245,19 +229,16 @@ const PROVIDERS = [
SubmissionResponseParsingService,
SubmissionJsonPatchOperationsService,
JsonPatchOperationsBuilder,
- UploaderService,
UUIDService,
NotificationsService,
WorkspaceitemDataService,
WorkflowItemDataService,
- UploaderService,
DSpaceObjectDataService,
ConfigurationDataService,
DSOChangeAnalyzer,
DefaultChangeAnalyzer,
ArrayMoveChangeAnalyzer,
ObjectSelectService,
- CSSVariableService,
MenuService,
ObjectUpdatesService,
SearchService,
@@ -268,7 +249,6 @@ const PROVIDERS = [
ClaimedTaskDataService,
PoolTaskDataService,
BitstreamDataService,
- DsDynamicTypeBindRelationService,
EntityTypeDataService,
ContentSourceResponseParsingService,
ItemTemplateDataService,
@@ -304,7 +284,6 @@ const PROVIDERS = [
VocabularyService,
VocabularyDataService,
VocabularyEntryDetailsDataService,
- VocabularyTreeviewService,
SequenceService,
GroupDataService,
FeedbackDataService,
diff --git a/src/app/core/data/base/data-service.decorator.spec.ts b/src/app/core/data/base/data-service.decorator.spec.ts
index ec4c1c8f6f..e09c531a56 100644
--- a/src/app/core/data/base/data-service.decorator.spec.ts
+++ b/src/app/core/data/base/data-service.decorator.spec.ts
@@ -10,6 +10,7 @@ import { ResourceType } from '../../shared/resource-type';
import { BaseDataService } from './base-data.service';
import { HALDataService } from './hal-data-service.interface';
import { dataService, getDataServiceFor } from './data-service.decorator';
+import { v4 as uuidv4 } from 'uuid';
class TestService extends BaseDataService {
}
@@ -28,7 +29,7 @@ let testType;
describe('@dataService/getDataServiceFor', () => {
beforeEach(() => {
- testType = new ResourceType('testType-' + new Date().getTime());
+ testType = new ResourceType(`testType-${uuidv4()}`);
});
it('should register a resourcetype for a dataservice', () => {
diff --git a/src/app/core/data/dso-change-analyzer.service.ts b/src/app/core/data/dso-change-analyzer.service.ts
index b79e83a3f3..a621895633 100644
--- a/src/app/core/data/dso-change-analyzer.service.ts
+++ b/src/app/core/data/dso-change-analyzer.service.ts
@@ -3,7 +3,7 @@ import { ChangeAnalyzer } from './change-analyzer';
import { Injectable } from '@angular/core';
import { DSpaceObject } from '../shared/dspace-object.model';
import { MetadataMap } from '../shared/metadata.models';
-import { cloneDeep } from 'lodash';
+import cloneDeep from 'lodash/cloneDeep';
/**
* A class to determine what differs between two
diff --git a/src/app/core/data/object-updates/object-updates.reducer.spec.ts b/src/app/core/data/object-updates/object-updates.reducer.spec.ts
index a51a1431bb..08944a073f 100644
--- a/src/app/core/data/object-updates/object-updates.reducer.spec.ts
+++ b/src/app/core/data/object-updates/object-updates.reducer.spec.ts
@@ -1,3 +1,4 @@
+// eslint-disable-next-line import/no-namespace
import * as deepFreeze from 'deep-freeze';
import {
AddFieldUpdateAction,
diff --git a/src/app/core/data/request.reducer.spec.ts b/src/app/core/data/request.reducer.spec.ts
index f6f557ad8f..05f074a96a 100644
--- a/src/app/core/data/request.reducer.spec.ts
+++ b/src/app/core/data/request.reducer.spec.ts
@@ -1,3 +1,4 @@
+// eslint-disable-next-line import/no-namespace
import * as deepFreeze from 'deep-freeze';
import {
RequestConfigureAction,
diff --git a/src/app/core/data/request.service.ts b/src/app/core/data/request.service.ts
index 2d5acb2cb3..16dc14dac4 100644
--- a/src/app/core/data/request.service.ts
+++ b/src/app/core/data/request.service.ts
@@ -4,7 +4,7 @@ import { HttpHeaders } from '@angular/common/http';
import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { filter, map, take, tap } from 'rxjs/operators';
-import { cloneDeep } from 'lodash';
+import cloneDeep from 'lodash/cloneDeep';
import { hasValue, isEmpty, isNotEmpty, hasNoValue } from '../../shared/empty.util';
import { ObjectCacheEntry } from '../cache/object-cache.reducer';
import { ObjectCacheService } from '../cache/object-cache.service';
diff --git a/src/app/shared/uploader/uploader.service.ts b/src/app/core/drag.service.ts
similarity index 53%
rename from src/app/shared/uploader/uploader.service.ts
rename to src/app/core/drag.service.ts
index 548de34f9c..d5f329d362 100644
--- a/src/app/shared/uploader/uploader.service.ts
+++ b/src/app/core/drag.service.ts
@@ -1,7 +1,17 @@
+/**
+ * The contents of this file are subject to the license and copyright
+ * detailed in the LICENSE and NOTICE files at the root of the source
+ * tree and available online at
+ *
+ * http://www.dspace.org/license/
+ */
+
import { Injectable } from '@angular/core';
-@Injectable()
-export class UploaderService {
+@Injectable({
+ providedIn: 'root'
+})
+export class DragService {
private _overrideDragOverPage = false;
public overrideDragOverPage() {
diff --git a/src/app/core/index/index.reducer.spec.ts b/src/app/core/index/index.reducer.spec.ts
index 867e4dbad8..774b9b2742 100644
--- a/src/app/core/index/index.reducer.spec.ts
+++ b/src/app/core/index/index.reducer.spec.ts
@@ -1,3 +1,4 @@
+// eslint-disable-next-line import/no-namespace
import * as deepFreeze from 'deep-freeze';
import { indexReducer, MetaIndexState } from './index.reducer';
diff --git a/src/app/core/index/index.selectors.ts b/src/app/core/index/index.selectors.ts
index 697616c321..e6ca04f79b 100644
--- a/src/app/core/index/index.selectors.ts
+++ b/src/app/core/index/index.selectors.ts
@@ -3,7 +3,6 @@ import { hasValue, isNotEmpty } from '../../shared/empty.util';
import { coreSelector } from '../core.selectors';
import { URLCombiner } from '../url-combiner/url-combiner';
import { IndexState, MetaIndexState } from './index.reducer';
-import * as parse from 'url-parse';
import { IndexName } from './index-name.model';
import { CoreState } from '../core-state.model';
@@ -21,17 +20,21 @@ import { CoreState } from '../core-state.model';
*/
export const getUrlWithoutEmbedParams = (url: string): string => {
if (isNotEmpty(url)) {
- const parsed = parse(url);
- if (isNotEmpty(parsed.query)) {
- const parts = parsed.query.split(/[?|&]/)
- .filter((part: string) => isNotEmpty(part))
- .filter((part: string) => !(part.startsWith('embed=') || part.startsWith('embed.size=')));
- let args = '';
- if (isNotEmpty(parts)) {
- args = `?${parts.join('&')}`;
+ try {
+ const parsed = new URL(url);
+ if (isNotEmpty(parsed.search)) {
+ const parts = parsed.search.split(/[?|&]/)
+ .filter((part: string) => isNotEmpty(part))
+ .filter((part: string) => !(part.startsWith('embed=') || part.startsWith('embed.size=')));
+ let args = '';
+ if (isNotEmpty(parts)) {
+ args = `?${parts.join('&')}`;
+ }
+ url = new URLCombiner(parsed.origin, parsed.pathname, args).toString();
+ return url;
}
- url = new URLCombiner(parsed.origin, parsed.pathname, args).toString();
- return url;
+ } catch (e) {
+ // Ignore parsing errors. By default, we return the original string below.
}
}
@@ -44,15 +47,19 @@ export const getUrlWithoutEmbedParams = (url: string): string => {
*/
export const getEmbedSizeParams = (url: string): { name: string, size: number }[] => {
if (isNotEmpty(url)) {
- const parsed = parse(url);
- if (isNotEmpty(parsed.query)) {
- return parsed.query.split(/[?|&]/)
- .filter((part: string) => isNotEmpty(part))
- .map((part: string) => part.match(/^embed.size=([^=]+)=(\d+)$/))
- .filter((matches: RegExpMatchArray) => hasValue(matches) && hasValue(matches[1]) && hasValue(matches[2]))
- .map((matches: RegExpMatchArray) => {
- return { name: matches[1], size: Number(matches[2]) };
- });
+ try {
+ const parsed = new URL(url);
+ if (isNotEmpty(parsed.search)) {
+ return parsed.search.split(/[?|&]/)
+ .filter((part: string) => isNotEmpty(part))
+ .map((part: string) => part.match(/^embed.size=([^=]+)=(\d+)$/))
+ .filter((matches: RegExpMatchArray) => hasValue(matches) && hasValue(matches[1]) && hasValue(matches[2]))
+ .map((matches: RegExpMatchArray) => {
+ return { name: matches[1], size: Number(matches[2]) };
+ });
+ }
+ } catch (e) {
+ // Ignore parsing errors. By default, we return an empty result below.
}
}
diff --git a/src/app/core/json-patch/json-patch-operations.reducer.spec.ts b/src/app/core/json-patch/json-patch-operations.reducer.spec.ts
index 1f98cf0920..34a68a8801 100644
--- a/src/app/core/json-patch/json-patch-operations.reducer.spec.ts
+++ b/src/app/core/json-patch/json-patch-operations.reducer.spec.ts
@@ -1,3 +1,4 @@
+// eslint-disable-next-line import/no-namespace
import * as deepFreeze from 'deep-freeze';
import {
diff --git a/src/app/core/locale/locale.service.ts b/src/app/core/locale/locale.service.ts
index 68d2839d42..16a35b8ae5 100644
--- a/src/app/core/locale/locale.service.ts
+++ b/src/app/core/locale/locale.service.ts
@@ -40,7 +40,7 @@ export class LocaleService {
protected translate: TranslateService,
protected authService: AuthService,
protected routeService: RouteService,
- @Inject(DOCUMENT) private document: any
+ @Inject(DOCUMENT) protected document: any
) {
}
diff --git a/src/app/core/locale/server-locale.service.ts b/src/app/core/locale/server-locale.service.ts
index f438643e49..556619b946 100644
--- a/src/app/core/locale/server-locale.service.ts
+++ b/src/app/core/locale/server-locale.service.ts
@@ -1,12 +1,31 @@
import { LANG_ORIGIN, LocaleService } from './locale.service';
-import { Injectable } from '@angular/core';
+import { Inject, Injectable } from '@angular/core';
import { combineLatest, Observable, of as observableOf } from 'rxjs';
import { map, mergeMap, take } from 'rxjs/operators';
-import { isEmpty, isNotEmpty } from '../../shared/empty.util';
+import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util';
+import { NativeWindowRef, NativeWindowService } from '../services/window.service';
+import { REQUEST } from '@nguniversal/express-engine/tokens';
+import { CookieService } from '../services/cookie.service';
+import { TranslateService } from '@ngx-translate/core';
+import { AuthService } from '../auth/auth.service';
+import { RouteService } from '../services/route.service';
+import { DOCUMENT } from '@angular/common';
@Injectable()
export class ServerLocaleService extends LocaleService {
+ constructor(
+ @Inject(NativeWindowService) protected _window: NativeWindowRef,
+ @Inject(REQUEST) protected req: Request,
+ protected cookie: CookieService,
+ protected translate: TranslateService,
+ protected authService: AuthService,
+ protected routeService: RouteService,
+ @Inject(DOCUMENT) protected document: any
+ ) {
+ super(_window, cookie, translate, authService, routeService, document);
+ }
+
/**
* Get the languages list of the user in Accept-Language format
*
@@ -50,6 +69,10 @@ export class ServerLocaleService extends LocaleService {
if (isNotEmpty(epersonLang)) {
languages.push(...epersonLang);
}
+ if (hasValue(this.req.headers['accept-language'])) {
+ languages.push(...this.req.headers['accept-language'].split(',')
+ );
+ }
return languages;
})
);
diff --git a/src/app/core/router/router.effects.ts b/src/app/core/router/router.effects.ts
index c2e3155eff..5df38aa1fc 100644
--- a/src/app/core/router/router.effects.ts
+++ b/src/app/core/router/router.effects.ts
@@ -1,8 +1,7 @@
import { filter, map, pairwise } from 'rxjs/operators';
import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
-import * as fromRouter from '@ngrx/router-store';
-import { RouterNavigationAction } from '@ngrx/router-store';
+import { RouterNavigationAction, ROUTER_NAVIGATION } from '@ngrx/router-store';
import { Router } from '@angular/router';
import { RouteUpdateAction } from './router.actions';
@@ -14,7 +13,7 @@ export class RouterEffects {
*/
routeChange$ = createEffect(() => this.actions$
.pipe(
- ofType(fromRouter.ROUTER_NAVIGATION),
+ ofType(ROUTER_NAVIGATION),
pairwise(),
map((actions: RouterNavigationAction[]) =>
actions.map((navigateAction) => {
diff --git a/src/app/core/services/route.service.ts b/src/app/core/services/route.service.ts
index b84bb40373..5381762570 100644
--- a/src/app/core/services/route.service.ts
+++ b/src/app/core/services/route.service.ts
@@ -4,7 +4,7 @@ import { ActivatedRoute, NavigationEnd, Params, Router, RouterStateSnapshot, } f
import { combineLatest, Observable } from 'rxjs';
import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
-import { isEqual } from 'lodash';
+import isEqual from 'lodash/isEqual';
import { AddParameterAction, SetParameterAction, SetParametersAction, SetQueryParameterAction, SetQueryParametersAction } from './route.actions';
import { coreSelector } from '../core.selectors';
diff --git a/src/app/core/shared/hal-endpoint.service.spec.ts b/src/app/core/shared/hal-endpoint.service.spec.ts
index 78a296496a..56e890b318 100644
--- a/src/app/core/shared/hal-endpoint.service.spec.ts
+++ b/src/app/core/shared/hal-endpoint.service.spec.ts
@@ -3,7 +3,7 @@ import { getMockRequestService } from '../../shared/mocks/request.service.mock';
import { RequestService } from '../data/request.service';
import { HALEndpointService } from './hal-endpoint.service';
import { EndpointMapRequest } from '../data/request.models';
-import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs';
+import { combineLatest as observableCombineLatest, of as observableOf } from 'rxjs';
import { environment } from '../../../environments/environment';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
diff --git a/src/app/core/shared/metadata.utils.ts b/src/app/core/shared/metadata.utils.ts
index 3fbeb205d2..87a90b53a3 100644
--- a/src/app/core/shared/metadata.utils.ts
+++ b/src/app/core/shared/metadata.utils.ts
@@ -5,7 +5,8 @@ import {
MetadataValueFilter,
MetadatumViewModel
} from './metadata.models';
-import { groupBy, sortBy } from 'lodash';
+import groupBy from 'lodash/groupBy';
+import sortBy from 'lodash/sortBy';
/**
* Utility class for working with DSpace object metadata.
diff --git a/src/app/core/shared/search/search.service.spec.ts b/src/app/core/shared/search/search.service.spec.ts
index 9399065ddf..fe5b495ab0 100644
--- a/src/app/core/shared/search/search.service.spec.ts
+++ b/src/app/core/shared/search/search.service.spec.ts
@@ -26,6 +26,8 @@ import { SearchConfigurationService } from './search-configuration.service';
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
import { RequestEntry } from '../../data/request-entry.model';
import { Angulartics2 } from 'angulartics2';
+import { SearchFilterConfig } from '../../../shared/search/models/search-filter-config.model';
+import anything = jasmine.anything;
@Component({ template: '' })
class DummyComponent {
@@ -36,7 +38,7 @@ describe('SearchService', () => {
let searchService: SearchService;
const router = new RouterStub();
const route = new ActivatedRouteStub();
- const searchConfigService = {paginationID: 'page-id'};
+ const searchConfigService = { paginationID: 'page-id' };
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
@@ -103,7 +105,8 @@ describe('SearchService', () => {
};
const paginationService = new PaginationServiceStub();
- const searchConfigService = {paginationID: 'page-id'};
+ const searchConfigService = { paginationID: 'page-id' };
+ const requestService = getMockRequestService();
beforeEach(() => {
TestBed.configureTestingModule({
@@ -119,7 +122,7 @@ describe('SearchService', () => {
providers: [
{ provide: Router, useValue: router },
{ provide: RouteService, useValue: routeServiceStub },
- { provide: RequestService, useValue: getMockRequestService() },
+ { provide: RequestService, useValue: requestService },
{ provide: RemoteDataBuildService, useValue: remoteDataBuildService },
{ provide: HALEndpointService, useValue: halService },
{ provide: CommunityDataService, useValue: {} },
@@ -138,13 +141,13 @@ describe('SearchService', () => {
it('should call the navigate method on the Router with view mode list parameter as a parameter when setViewMode is called', () => {
searchService.setViewMode(ViewMode.ListElement);
- expect(paginationService.updateRouteWithUrl).toHaveBeenCalledWith('page-id', ['/search'], {page: 1}, { view: ViewMode.ListElement }
+ expect(paginationService.updateRouteWithUrl).toHaveBeenCalledWith('page-id', ['/search'], { page: 1 }, { view: ViewMode.ListElement }
);
});
it('should call the navigate method on the Router with view mode grid parameter as a parameter when setViewMode is called', () => {
searchService.setViewMode(ViewMode.GridElement);
- expect(paginationService.updateRouteWithUrl).toHaveBeenCalledWith('page-id', ['/search'], {page: 1}, { view: ViewMode.GridElement }
+ expect(paginationService.updateRouteWithUrl).toHaveBeenCalledWith('page-id', ['/search'], { page: 1 }, { view: ViewMode.GridElement }
);
});
@@ -191,5 +194,23 @@ describe('SearchService', () => {
expect((searchService as any).rdb.buildFromHref).toHaveBeenCalledWith(endPoint);
});
});
+
+ describe('when getFacetValuesFor is called with a filterQuery', () => {
+ it('should add the encoded filterQuery to the args list', () => {
+ jasmine.getEnv().allowRespy(true);
+ const spyRequest = spyOn((searchService as any), 'request').and.stub();
+ spyOn(requestService, 'send').and.returnValue(true);
+ const searchFilterConfig = new SearchFilterConfig();
+ searchFilterConfig._links = {
+ self: {
+ href: 'https://demo.dspace.org/',
+ },
+ };
+
+ searchService.getFacetValuesFor(searchFilterConfig, 1, undefined, 'filter&Query');
+
+ expect(spyRequest).toHaveBeenCalledWith(anything(), 'https://demo.dspace.org?page=0&size=5&prefix=filter%26Query');
+ });
+ });
});
});
diff --git a/src/app/core/shared/search/search.service.ts b/src/app/core/shared/search/search.service.ts
index c8ce4b0348..9befd8ea13 100644
--- a/src/app/core/shared/search/search.service.ts
+++ b/src/app/core/shared/search/search.service.ts
@@ -3,7 +3,6 @@ import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
import { Injectable, OnDestroy } from '@angular/core';
import { map, switchMap, take } from 'rxjs/operators';
import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
-import { PaginatedList } from '../../data/paginated-list.model';
import { ResponseParsingService } from '../../data/parsing.service';
import { RemoteData } from '../../data/remote-data';
import { GetRequest } from '../../data/request.models';
@@ -271,7 +270,7 @@ export class SearchService implements OnDestroy {
let href;
let args: string[] = [];
if (hasValue(filterQuery)) {
- args.push(`prefix=${filterQuery}`);
+ args.push(`prefix=${encodeURIComponent(filterQuery)}`);
}
if (hasValue(searchOptions)) {
searchOptions = Object.assign(new PaginatedSearchOptions({}), searchOptions, {
diff --git a/src/app/core/utilities/equatable.spec.ts b/src/app/core/utilities/equatable.spec.ts
index d45010980e..037cea8cc5 100644
--- a/src/app/core/utilities/equatable.spec.ts
+++ b/src/app/core/utilities/equatable.spec.ts
@@ -1,6 +1,6 @@
/* eslint-disable max-classes-per-file */
import { EquatableObject, excludeFromEquals, fieldsForEquals } from './equals.decorators';
-import { cloneDeep } from 'lodash';
+import cloneDeep from 'lodash/cloneDeep';
class Dog extends EquatableObject {
public name: string;
diff --git a/src/app/correlation-id/correlation-id.service.spec.ts b/src/app/correlation-id/correlation-id.service.spec.ts
index 64a4d1068a..816c3694a5 100644
--- a/src/app/correlation-id/correlation-id.service.spec.ts
+++ b/src/app/correlation-id/correlation-id.service.spec.ts
@@ -1,7 +1,7 @@
import { CorrelationIdService } from './correlation-id.service';
import { CookieServiceMock } from '../shared/mocks/cookie.service.mock';
import { UUIDService } from '../core/shared/uuid.service';
-import { MockStore, provideMockStore } from '@ngrx/store/testing';
+import { MockStore } from '@ngrx/store/testing';
import { TestBed } from '@angular/core/testing';
import { Store, StoreModule } from '@ngrx/store';
import { appReducers, AppState, storeModuleConfig } from '../app.reducer';
diff --git a/src/app/curation-form/curation-form.component.ts b/src/app/curation-form/curation-form.component.ts
index 422c955037..4b67580e77 100644
--- a/src/app/curation-form/curation-form.component.ts
+++ b/src/app/curation-form/curation-form.component.ts
@@ -5,7 +5,7 @@ import { getFirstCompletedRemoteData } from '../core/shared/operators';
import { find, map } from 'rxjs/operators';
import { NotificationsService } from '../shared/notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core';
-import { hasValue, isEmpty, isNotEmpty, hasNoValue } from '../shared/empty.util';
+import { hasValue, isEmpty, isNotEmpty } from '../shared/empty.util';
import { RemoteData } from '../core/data/remote-data';
import { Router } from '@angular/router';
import { ProcessDataService } from '../core/data/processes/process-data.service';
diff --git a/src/app/home-page/recent-item-list/recent-item-list.component.html b/src/app/home-page/recent-item-list/recent-item-list.component.html
index 919b8646bb..cd14891b3f 100644
--- a/src/app/home-page/recent-item-list/recent-item-list.component.html
+++ b/src/app/home-page/recent-item-list/recent-item-list.component.html
@@ -6,8 +6,7 @@
- Load
- more...
+ {{'vocabulary-treeview.load-more' | translate }} ...
diff --git a/src/app/home-page/top-level-community-list/top-level-community-list.component.spec.ts b/src/app/home-page/top-level-community-list/top-level-community-list.component.spec.ts
index 5dbf1ff8a2..d1a3d3631f 100644
--- a/src/app/home-page/top-level-community-list/top-level-community-list.component.spec.ts
+++ b/src/app/home-page/top-level-community-list/top-level-community-list.component.spec.ts
@@ -17,9 +17,6 @@ import { HostWindowService } from '../../shared/host-window.service';
import { HostWindowServiceStub } from '../../shared/testing/host-window-service.stub';
import { CommunityDataService } from '../../core/data/community-data.service';
import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service';
-import { of as observableOf } from 'rxjs';
-import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
-import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { PaginationService } from '../../core/pagination/pagination.service';
import { getMockThemeService } from '../../shared/mocks/theme-service.mock';
import { ThemeService } from '../../shared/theme-support/theme.service';
diff --git a/src/app/init.service.ts b/src/app/init.service.ts
index a0cbb06b66..e2af88fa65 100644
--- a/src/app/init.service.ts
+++ b/src/app/init.service.ts
@@ -13,7 +13,7 @@ import { makeStateKey, TransferState } from '@angular/platform-browser';
import { APP_CONFIG, AppConfig } from '../config/app-config.interface';
import { environment } from '../environments/environment';
import { AppState } from './app.reducer';
-import { isEqual } from 'lodash';
+import isEqual from 'lodash/isEqual';
import { TranslateService } from '@ngx-translate/core';
import { LocaleService } from './core/locale/locale.service';
import { Angulartics2DSpace } from './statistics/angulartics/dspace-provider';
diff --git a/src/app/item-page/bitstreams/upload/upload-bitstream.component.ts b/src/app/item-page/bitstreams/upload/upload-bitstream.component.ts
index 1e5295a347..74019de7cc 100644
--- a/src/app/item-page/bitstreams/upload/upload-bitstream.component.ts
+++ b/src/app/item-page/bitstreams/upload/upload-bitstream.component.ts
@@ -4,7 +4,7 @@ import { RemoteData } from '../../../core/data/remote-data';
import { Item } from '../../../core/shared/item.model';
import { map, take, switchMap } from 'rxjs/operators';
import { ActivatedRoute, Router } from '@angular/router';
-import { UploaderOptions } from '../../../shared/uploader/uploader-options.model';
+import { UploaderOptions } from '../../../shared/upload/uploader/uploader-options.model';
import { hasValue, isEmpty, isNotEmpty } from '../../../shared/empty.util';
import { ItemDataService } from '../../../core/data/item-data.service';
import { AuthService } from '../../../core/auth/auth.service';
@@ -14,7 +14,7 @@ import { PaginatedList } from '../../../core/data/paginated-list.model';
import { Bundle } from '../../../core/shared/bundle.model';
import { BundleDataService } from '../../../core/data/bundle-data.service';
import { getFirstSucceededRemoteDataPayload, getFirstCompletedRemoteData } from '../../../core/shared/operators';
-import { UploaderComponent } from '../../../shared/uploader/uploader.component';
+import { UploaderComponent } from '../../../shared/upload/uploader/uploader.component';
import { RequestService } from '../../../core/data/request.service';
import { getBitstreamModuleRoute } from '../../../app-routing-paths';
import { getEntityEditRoute } from '../../item-page-routing-paths';
diff --git a/src/app/item-page/edit-item-page/item-authorizations/item-authorizations.component.spec.ts b/src/app/item-page/edit-item-page/item-authorizations/item-authorizations.component.spec.ts
index 2fe8a562c6..5d2afbaf4c 100644
--- a/src/app/item-page/edit-item-page/item-authorizations/item-authorizations.component.spec.ts
+++ b/src/app/item-page/edit-item-page/item-authorizations/item-authorizations.component.spec.ts
@@ -1,12 +1,11 @@
-import { Observable } from 'rxjs/internal/Observable';
import { waitForAsync, ComponentFixture, inject, TestBed } from '@angular/core/testing';
import { Component, NO_ERRORS_SCHEMA } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
-import { of as observableOf, of } from 'rxjs';
+import { of as observableOf } from 'rxjs';
import { TranslateModule } from '@ngx-translate/core';
import { cold } from 'jasmine-marbles';
-import { ItemAuthorizationsComponent, BitstreamMapValue } from './item-authorizations.component';
+import { ItemAuthorizationsComponent } from './item-authorizations.component';
import { Bitstream } from '../../../core/shared/bitstream.model';
import { Bundle } from '../../../core/shared/bundle.model';
import { Item } from '../../../core/shared/item.model';
@@ -14,8 +13,6 @@ import { LinkService } from '../../../core/cache/builders/link.service';
import { getMockLinkService } from '../../../shared/mocks/link-service.mock';
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
import { createPaginatedList, createTestComponent } from '../../../shared/testing/utils.test';
-import { PaginatedList, buildPaginatedList } from '../../../core/data/paginated-list.model';
-import { PageInfo } from '../../../core/shared/page-info.model';
describe('ItemAuthorizationsComponent test suite', () => {
let comp: ItemAuthorizationsComponent;
diff --git a/src/app/item-page/edit-item-page/item-authorizations/item-authorizations.component.ts b/src/app/item-page/edit-item-page/item-authorizations/item-authorizations.component.ts
index 8ed2f9a12e..c06bcad7f2 100644
--- a/src/app/item-page/edit-item-page/item-authorizations/item-authorizations.component.ts
+++ b/src/app/item-page/edit-item-page/item-authorizations/item-authorizations.component.ts
@@ -1,4 +1,4 @@
-import { isEqual } from 'lodash';
+import isEqual from 'lodash/isEqual';
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.spec.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.spec.ts
index 133b13cb27..7317eb93be 100644
--- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.spec.ts
+++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.spec.ts
@@ -17,10 +17,7 @@ import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-
import { createPaginatedList } from '../../../../../shared/testing/utils.test';
import { RequestService } from '../../../../../core/data/request.service';
import { PaginationService } from '../../../../../core/pagination/pagination.service';
-import { PaginationComponentOptions } from '../../../../../shared/pagination/pagination-component-options.model';
-import { SortDirection, SortOptions } from '../../../../../core/cache/models/sort-options.model';
import { PaginationServiceStub } from '../../../../../shared/testing/pagination-service.stub';
-import { FindListOptions } from '../../../../../core/data/find-list-options.model';
describe('PaginatedDragAndDropBitstreamListComponent', () => {
let comp: PaginatedDragAndDropBitstreamListComponent;
diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.ts
index d7fcc0cb76..fcb5c706ac 100644
--- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.ts
+++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.ts
@@ -1,6 +1,6 @@
import { Component, Input, OnChanges, OnInit, SimpleChanges, ViewChild, ViewContainerRef } from '@angular/core';
import { Bitstream } from '../../../../core/shared/bitstream.model';
-import { cloneDeep } from 'lodash';
+import cloneDeep from 'lodash/cloneDeep';
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
import { Observable } from 'rxjs';
import { BitstreamFormat } from '../../../../core/shared/bitstream-format.model';
diff --git a/src/app/item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.ts b/src/app/item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.ts
index f1ebecb84f..440ccd135f 100644
--- a/src/app/item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.ts
+++ b/src/app/item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.ts
@@ -5,7 +5,7 @@ import {
} from '../../../../core/shared/operators';
import { hasValue, isNotEmpty } from '../../../../shared/empty.util';
import { RegistryService } from '../../../../core/registry/registry.service';
-import { cloneDeep } from 'lodash';
+import cloneDeep from 'lodash/cloneDeep';
import { BehaviorSubject, Observable, of as observableOf } from 'rxjs';
import { map } from 'rxjs/operators';
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
diff --git a/src/app/item-page/edit-item-page/item-metadata/item-metadata.component.ts b/src/app/item-page/edit-item-page/item-metadata/item-metadata.component.ts
index 5030ef3bb3..3c1bad5114 100644
--- a/src/app/item-page/edit-item-page/item-metadata/item-metadata.component.ts
+++ b/src/app/item-page/edit-item-page/item-metadata/item-metadata.component.ts
@@ -3,7 +3,7 @@ import { Item } from '../../../core/shared/item.model';
import { ItemDataService } from '../../../core/data/item-data.service';
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
import { ActivatedRoute, Router } from '@angular/router';
-import { cloneDeep } from 'lodash';
+import cloneDeep from 'lodash/cloneDeep';
import { first, switchMap } from 'rxjs/operators';
import { getFirstCompletedRemoteData } from '../../../core/shared/operators';
import { RemoteData } from '../../../core/data/remote-data';
diff --git a/src/app/item-page/field-components/metadata-values/metadata-values.component.html b/src/app/item-page/field-components/metadata-values/metadata-values.component.html
index 824265c5c8..becb35e4bd 100644
--- a/src/app/item-page/field-components/metadata-values/metadata-values.component.html
+++ b/src/app/item-page/field-components/metadata-values/metadata-values.component.html
@@ -1,18 +1,16 @@
-
+
-
-
+
+
-
-
- {{value}}
-
+
+ {{value}}
diff --git a/src/app/item-page/full/field-components/file-section/full-file-section.component.spec.ts b/src/app/item-page/full/field-components/file-section/full-file-section.component.spec.ts
index 396e6c3216..6f0e97513f 100644
--- a/src/app/item-page/full/field-components/file-section/full-file-section.component.spec.ts
+++ b/src/app/item-page/full/field-components/file-section/full-file-section.component.spec.ts
@@ -16,11 +16,10 @@ import { MockBitstreamFormat1 } from '../../../../shared/mocks/item.mock';
import { By } from '@angular/platform-browser';
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service.stub';
-import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model';
-import { SortDirection, SortOptions } from '../../../../core/cache/models/sort-options.model';
import { PaginationService } from '../../../../core/pagination/pagination.service';
import { PaginationServiceStub } from '../../../../shared/testing/pagination-service.stub';
-import { FindListOptions } from '../../../../core/data/find-list-options.model';
+import { APP_CONFIG } from 'src/config/app-config.interface';
+import { environment } from 'src/environments/environment';
describe('FullFileSectionComponent', () => {
let comp: FullFileSectionComponent;
@@ -72,7 +71,8 @@ describe('FullFileSectionComponent', () => {
providers: [
{ provide: BitstreamDataService, useValue: bitstreamDataService },
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
- { provide: PaginationService, useValue: paginationService }
+ { provide: PaginationService, useValue: paginationService },
+ { provide: APP_CONFIG, useValue: environment },
],
schemas: [NO_ERRORS_SCHEMA]
diff --git a/src/app/item-page/full/field-components/file-section/full-file-section.component.ts b/src/app/item-page/full/field-components/file-section/full-file-section.component.ts
index e21c1a32eb..3be0d58c81 100644
--- a/src/app/item-page/full/field-components/file-section/full-file-section.component.ts
+++ b/src/app/item-page/full/field-components/file-section/full-file-section.component.ts
@@ -1,4 +1,4 @@
-import { Component, Input, OnInit } from '@angular/core';
+import { Component, Inject, Input, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { BitstreamDataService } from '../../../../core/data/bitstream-data.service';
@@ -14,6 +14,7 @@ import { NotificationsService } from '../../../../shared/notifications/notificat
import { TranslateService } from '@ngx-translate/core';
import { hasValue, isEmpty } from '../../../../shared/empty.util';
import { PaginationService } from '../../../../core/pagination/pagination.service';
+import { AppConfig, APP_CONFIG } from 'src/config/app-config.interface';
/**
* This component renders the file section of the item
@@ -34,26 +35,26 @@ export class FullFileSectionComponent extends FileSectionComponent implements On
originals$: Observable>>;
licenses$: Observable>>;
- pageSize = 5;
originalOptions = Object.assign(new PaginationComponentOptions(), {
id: 'obo',
currentPage: 1,
- pageSize: this.pageSize
+ pageSize: this.appConfig.item.bitstream.pageSize
});
licenseOptions = Object.assign(new PaginationComponentOptions(), {
id: 'lbo',
currentPage: 1,
- pageSize: this.pageSize
+ pageSize: this.appConfig.item.bitstream.pageSize
});
constructor(
bitstreamDataService: BitstreamDataService,
protected notificationsService: NotificationsService,
protected translateService: TranslateService,
- protected paginationService: PaginationService
+ protected paginationService: PaginationService,
+ @Inject(APP_CONFIG) protected appConfig: AppConfig
) {
- super(bitstreamDataService, notificationsService, translateService);
+ super(bitstreamDataService, notificationsService, translateService, appConfig);
}
ngOnInit(): void {
diff --git a/src/app/item-page/item-page-routing.module.ts b/src/app/item-page/item-page-routing.module.ts
index add2c3d768..ac14ca8402 100644
--- a/src/app/item-page/item-page-routing.module.ts
+++ b/src/app/item-page/item-page-routing.module.ts
@@ -67,6 +67,7 @@ import { OrcidPageGuard } from './orcid-page/orcid-page.guard';
id: 'statistics_item_:id',
active: true,
visible: true,
+ index: 2,
model: {
type: MenuItemType.LINK,
text: 'menu.section.statistics',
diff --git a/src/app/item-page/item-page.module.ts b/src/app/item-page/item-page.module.ts
index c4e86a37fb..de9f2f60c5 100644
--- a/src/app/item-page/item-page.module.ts
+++ b/src/app/item-page/item-page.module.ts
@@ -46,6 +46,7 @@ import { OrcidPageComponent } from './orcid-page/orcid-page.component';
import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap';
import { OrcidSyncSettingsComponent } from './orcid-page/orcid-sync-settings/orcid-sync-settings.component';
import { OrcidQueueComponent } from './orcid-page/orcid-queue/orcid-queue.component';
+import { UploadModule } from '../shared/upload/upload.module';
const ENTRY_COMPONENTS = [
@@ -94,7 +95,8 @@ const DECLARATIONS = [
JournalEntitiesModule.withEntryComponents(),
ResearchEntitiesModule.withEntryComponents(),
NgxGalleryModule,
- NgbAccordionModule
+ NgbAccordionModule,
+ UploadModule,
],
declarations: [
...DECLARATIONS,
diff --git a/src/app/item-page/simple/field-components/file-section/file-section.component.spec.ts b/src/app/item-page/simple/field-components/file-section/file-section.component.spec.ts
index 2d185aef9c..83f92d5af8 100644
--- a/src/app/item-page/simple/field-components/file-section/file-section.component.spec.ts
+++ b/src/app/item-page/simple/field-components/file-section/file-section.component.spec.ts
@@ -17,6 +17,8 @@ import { MetadataFieldWrapperComponent } from '../../../field-components/metadat
import { createPaginatedList } from '../../../../shared/testing/utils.test';
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service.stub';
+import { APP_CONFIG } from 'src/config/app-config.interface';
+import { environment } from 'src/environments/environment';
describe('FileSectionComponent', () => {
let comp: FileSectionComponent;
@@ -65,7 +67,8 @@ describe('FileSectionComponent', () => {
declarations: [FileSectionComponent, VarDirective, FileSizePipe, MetadataFieldWrapperComponent],
providers: [
{ provide: BitstreamDataService, useValue: bitstreamDataService },
- { provide: NotificationsService, useValue: new NotificationsServiceStub() }
+ { provide: NotificationsService, useValue: new NotificationsServiceStub() },
+ { provide: APP_CONFIG, useValue: environment }
],
schemas: [NO_ERRORS_SCHEMA]
diff --git a/src/app/item-page/simple/field-components/file-section/file-section.component.ts b/src/app/item-page/simple/field-components/file-section/file-section.component.ts
index d28b579996..08e792fc8b 100644
--- a/src/app/item-page/simple/field-components/file-section/file-section.component.ts
+++ b/src/app/item-page/simple/field-components/file-section/file-section.component.ts
@@ -1,4 +1,4 @@
-import { Component, Input, OnInit } from '@angular/core';
+import { Component, Inject, Input, OnInit } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { BitstreamDataService } from '../../../../core/data/bitstream-data.service';
@@ -10,6 +10,7 @@ import { PaginatedList } from '../../../../core/data/paginated-list.model';
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core';
import { getFirstCompletedRemoteData } from '../../../../core/shared/operators';
+import { AppConfig, APP_CONFIG } from 'src/config/app-config.interface';
/**
* This component renders the file section of the item
@@ -35,13 +36,15 @@ export class FileSectionComponent implements OnInit {
isLastPage: boolean;
- pageSize = 5;
+ pageSize: number;
constructor(
protected bitstreamDataService: BitstreamDataService,
protected notificationsService: NotificationsService,
- protected translateService: TranslateService
+ protected translateService: TranslateService,
+ @Inject(APP_CONFIG) protected appConfig: AppConfig
) {
+ this.pageSize = this.appConfig.item.bitstream.pageSize;
}
ngOnInit(): void {
diff --git a/src/app/item-page/simple/field-components/specific-field/generic/generic-item-page-field.component.ts b/src/app/item-page/simple/field-components/specific-field/generic/generic-item-page-field.component.ts
index 3b8d261dcc..9d818e2f7d 100644
--- a/src/app/item-page/simple/field-components/specific-field/generic/generic-item-page-field.component.ts
+++ b/src/app/item-page/simple/field-components/specific-field/generic/generic-item-page-field.component.ts
@@ -35,4 +35,10 @@ export class GenericItemPageFieldComponent extends ItemPageFieldComponent {
*/
@Input() label: string;
+ /**
+ * Whether the {@link MarkdownPipe} should be used to render this metadata.
+ */
+ @Input() enableMarkdown = false;
+
+
}
diff --git a/src/app/item-page/simple/item-types/publication/publication.component.html b/src/app/item-page/simple/item-types/publication/publication.component.html
index 392d5ddd0c..181480b789 100644
--- a/src/app/item-page/simple/item-types/publication/publication.component.html
+++ b/src/app/item-page/simple/item-types/publication/publication.component.html
@@ -76,7 +76,7 @@
-
-
+
+
diff --git a/src/app/my-dspace-page/collection-selector/collection-selector.component.spec.ts b/src/app/my-dspace-page/collection-selector/collection-selector.component.spec.ts
index ce54d326fc..af043b447b 100644
--- a/src/app/my-dspace-page/collection-selector/collection-selector.component.spec.ts
+++ b/src/app/my-dspace-page/collection-selector/collection-selector.component.spec.ts
@@ -128,10 +128,13 @@ describe('CollectionSelectorComponent', () => {
beforeEach(() => {
scheduler = getTestScheduler();
- fixture = TestBed.createComponent(CollectionSelectorComponent);
+ fixture = TestBed.overrideComponent(CollectionSelectorComponent, {
+ set: {
+ template: ' '
+ }
+ }).createComponent(CollectionSelectorComponent);
component = fixture.componentInstance;
fixture.detectChanges();
-
});
it('should create', () => {
diff --git a/src/app/my-dspace-page/my-dspace-configuration.service.spec.ts b/src/app/my-dspace-page/my-dspace-configuration.service.spec.ts
index b87c77c5e9..669a97764a 100644
--- a/src/app/my-dspace-page/my-dspace-configuration.service.spec.ts
+++ b/src/app/my-dspace-page/my-dspace-configuration.service.spec.ts
@@ -11,10 +11,6 @@ import { cold, hot } from 'jasmine-marbles';
import { MyDSpaceConfigurationValueType } from './my-dspace-configuration-value-type';
import { PaginationServiceStub } from '../shared/testing/pagination-service.stub';
import { Context } from '../core/shared/context.model';
-import { LinkService } from '../core/cache/builders/link.service';
-import { HALEndpointService } from '../core/shared/hal-endpoint.service';
-import { RequestService } from '../core/data/request.service';
-import { RemoteDataBuildService } from '../core/cache/builders/remote-data-build.service';
import { HALEndpointServiceStub } from '../shared/testing/hal-endpoint-service.stub';
import { getMockRemoteDataBuildService } from '../shared/mocks/remote-data-build.service.mock';
diff --git a/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.spec.ts b/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.spec.ts
index fb43c253eb..ed61fab1d6 100644
--- a/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.spec.ts
+++ b/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.spec.ts
@@ -16,10 +16,10 @@ import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
import { getMockScrollToService } from '../../shared/mocks/scroll-to-service.mock';
-import { UploaderService } from '../../shared/uploader/uploader.service';
+import { DragService } from '../../core/drag.service';
import { HostWindowService } from '../../shared/host-window.service';
import { HostWindowServiceStub } from '../../shared/testing/host-window-service.stub';
-import { UploaderComponent } from '../../shared/uploader/uploader.component';
+import { UploaderComponent } from '../../shared/upload/uploader/uploader.component';
import { HttpXsrfTokenExtractor } from '@angular/common/http';
import { CookieService } from '../../core/services/cookie.service';
import { CookieServiceMock } from '../../shared/mocks/cookie.service.mock';
@@ -59,7 +59,7 @@ describe('MyDSpaceNewSubmissionComponent test', () => {
NgbModal,
ChangeDetectorRef,
MyDSpaceNewSubmissionComponent,
- UploaderService,
+ DragService,
{ provide: HttpXsrfTokenExtractor, useValue: new HttpXsrfTokenExtractorMock('mock-token') },
{ provide: CookieService, useValue: new CookieServiceMock() },
{ provide: HostWindowService, useValue: new HostWindowServiceStub(800) },
diff --git a/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.ts b/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.ts
index b2ba6fe2af..0694fc63bf 100644
--- a/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.ts
+++ b/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.ts
@@ -8,13 +8,13 @@ import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { AuthService } from '../../core/auth/auth.service';
import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { NotificationsService } from '../../shared/notifications/notifications.service';
-import { UploaderOptions } from '../../shared/uploader/uploader-options.model';
+import { UploaderOptions } from '../../shared/upload/uploader/uploader-options.model';
import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
import { hasValue } from '../../shared/empty.util';
import { SearchResult } from '../../shared/search/models/search-result.model';
import { CollectionSelectorComponent } from '../collection-selector/collection-selector.component';
-import { UploaderComponent } from '../../shared/uploader/uploader.component';
-import { UploaderError } from '../../shared/uploader/uploader-error.model';
+import { UploaderComponent } from '../../shared/upload/uploader/uploader.component';
+import { UploaderError } from '../../shared/upload/uploader/uploader-error.model';
import { Router } from '@angular/router';
/**
diff --git a/src/app/my-dspace-page/my-dspace-page.module.ts b/src/app/my-dspace-page/my-dspace-page.module.ts
index 2ccddd87f7..6ad50af96a 100644
--- a/src/app/my-dspace-page/my-dspace-page.module.ts
+++ b/src/app/my-dspace-page/my-dspace-page.module.ts
@@ -14,6 +14,7 @@ import { MyDSpaceNewSubmissionDropdownComponent } from './my-dspace-new-submissi
import { MyDSpaceNewExternalDropdownComponent } from './my-dspace-new-submission/my-dspace-new-external-dropdown/my-dspace-new-external-dropdown.component';
import { ThemedMyDSpacePageComponent } from './themed-my-dspace-page.component';
import { SearchModule } from '../shared/search/search.module';
+import { UploadModule } from '../shared/upload/upload.module';
const DECLARATIONS = [
MyDSpacePageComponent,
@@ -30,7 +31,8 @@ const DECLARATIONS = [
SharedModule,
SearchModule,
MyDspacePageRoutingModule,
- MyDspaceSearchModule.withEntryComponents()
+ MyDspaceSearchModule.withEntryComponents(),
+ UploadModule,
],
declarations: DECLARATIONS,
providers: [
diff --git a/src/app/navbar/expandable-navbar-section/expandable-navbar-section.component.html b/src/app/navbar/expandable-navbar-section/expandable-navbar-section.component.html
index d8a517dc55..d04c22e60a 100644
--- a/src/app/navbar/expandable-navbar-section/expandable-navbar-section.component.html
+++ b/src/app/navbar/expandable-navbar-section/expandable-navbar-section.component.html
@@ -1,4 +1,4 @@
-
+
-
\ No newline at end of file
+
diff --git a/src/app/navbar/navbar-section/navbar-section.component.scss b/src/app/navbar/navbar-section/navbar-section.component.scss
index e69de29bb2..7af4ada8e6 100644
--- a/src/app/navbar/navbar-section/navbar-section.component.scss
+++ b/src/app/navbar/navbar-section/navbar-section.component.scss
@@ -0,0 +1,5 @@
+.navbar-section {
+ display: flex;
+ align-items: center;
+ height: 100%;
+}
diff --git a/src/app/navbar/navbar.component.html b/src/app/navbar/navbar.component.html
index b41f2ea706..bc1e04f513 100644
--- a/src/app/navbar/navbar.component.html
+++ b/src/app/navbar/navbar.component.html
@@ -1,10 +1,13 @@
-
+
+
+
+
diff --git a/src/app/navbar/navbar.component.scss b/src/app/navbar/navbar.component.scss
index 3a9a302b06..fed88c5c68 100644
--- a/src/app/navbar/navbar.component.scss
+++ b/src/app/navbar/navbar.component.scss
@@ -6,13 +6,14 @@ nav.navbar {
/** Mobile menu styling **/
@media screen and (max-width: map-get($grid-breakpoints, md)-0.02) {
.navbar {
- width: 100vw;
+ width: 100%;
background-color: var(--bs-white);
position: absolute;
overflow: hidden;
height: 0;
&.open {
- height: 100vh; //doesn't matter because wrapper is sticky
+ height: auto;
+ min-height: 100vh; //doesn't matter because wrapper is sticky
}
}
}
@@ -27,7 +28,7 @@ nav.navbar {
/* TODO remove when https://github.com/twbs/bootstrap/issues/24726 is fixed */
.navbar-expand-md.navbar-container {
@media screen and (max-width: map-get($grid-breakpoints, md)-0.02) {
- > .container {
+ > .navbar-inner-container {
padding: 0 var(--bs-spacer);
}
padding: 0;
diff --git a/src/app/navbar/navbar.component.spec.ts b/src/app/navbar/navbar.component.spec.ts
index 9a9d2210be..ada9be9d0b 100644
--- a/src/app/navbar/navbar.component.spec.ts
+++ b/src/app/navbar/navbar.component.spec.ts
@@ -22,9 +22,17 @@ import { Item } from '../core/shared/item.model';
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
import { ThemeService } from '../shared/theme-support/theme.service';
import { getMockThemeService } from '../shared/mocks/theme-service.mock';
+import { Store, StoreModule } from '@ngrx/store';
+import { AppState, storeModuleConfig } from '../app.reducer';
+import { authReducer } from '../core/auth/auth.reducer';
+import { provideMockStore } from '@ngrx/store/testing';
+import { AuthTokenInfo } from '../core/auth/models/auth-token-info.model';
+import { EPersonMock } from '../shared/testing/eperson.mock';
let comp: NavbarComponent;
let fixture: ComponentFixture;
+let store: Store;
+let initialState: any;
const authorizationService = jasmine.createSpyObj('authorizationService', {
isAuthorized: observableOf(true)
@@ -83,10 +91,24 @@ describe('NavbarComponent', () => {
}
),
];
+ initialState = {
+ core: {
+ auth: {
+ authenticated: true,
+ loaded: true,
+ blocking: false,
+ loading: false,
+ authToken: new AuthTokenInfo('test_token'),
+ userId: EPersonMock.id,
+ authMethods: []
+ }
+ }
+ };
TestBed.configureTestingModule({
imports: [
TranslateModule.forRoot(),
+ StoreModule.forRoot({ auth: authReducer }, storeModuleConfig),
NoopAnimationsModule,
ReactiveFormsModule,
RouterTestingModule],
@@ -99,6 +121,7 @@ describe('NavbarComponent', () => {
{ provide: ActivatedRoute, useValue: routeStub },
{ provide: BrowseService, useValue: { getBrowseDefinitions: createSuccessfulRemoteDataObject$(buildPaginatedList(undefined, browseDefinitions)) } },
{ provide: AuthorizationDataService, useValue: authorizationService },
+ provideMockStore({ initialState }),
],
schemas: [NO_ERRORS_SCHEMA]
})
@@ -107,7 +130,7 @@ describe('NavbarComponent', () => {
// synchronous beforeEach
beforeEach(() => {
-
+ store = TestBed.inject(Store);
fixture = TestBed.createComponent(NavbarComponent);
comp = fixture.componentInstance;
diff --git a/src/app/navbar/navbar.component.ts b/src/app/navbar/navbar.component.ts
index 62b9e7d770..0cfad9e431 100644
--- a/src/app/navbar/navbar.component.ts
+++ b/src/app/navbar/navbar.component.ts
@@ -8,6 +8,10 @@ import { ActivatedRoute } from '@angular/router';
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
import { MenuID } from '../shared/menu/menu-id.model';
import { ThemeService } from '../shared/theme-support/theme.service';
+import { Observable } from 'rxjs';
+import { select, Store } from '@ngrx/store';
+import { AppState } from '../app.reducer';
+import { isAuthenticated } from '../core/auth/selectors';
/**
* Component representing the public navbar
@@ -25,18 +29,29 @@ export class NavbarComponent extends MenuComponent {
*/
menuID = MenuID.PUBLIC;
+ /**
+ * Whether user is authenticated.
+ * @type {Observable}
+ */
+ public isAuthenticated$: Observable;
+
+ public isXsOrSm$: Observable;
+
constructor(protected menuService: MenuService,
protected injector: Injector,
public windowService: HostWindowService,
public browseService: BrowseService,
public authorizationService: AuthorizationDataService,
public route: ActivatedRoute,
- protected themeService: ThemeService
+ protected themeService: ThemeService,
+ private store: Store,
) {
super(menuService, injector, authorizationService, route, themeService);
}
ngOnInit(): void {
super.ngOnInit();
+ this.isXsOrSm$ = this.windowService.isXsOrSm();
+ this.isAuthenticated$ = this.store.pipe(select(isAuthenticated));
}
}
diff --git a/src/app/navbar/navbar.effects.spec.ts b/src/app/navbar/navbar.effects.spec.ts
index bcb105d5b2..787ecafeec 100644
--- a/src/app/navbar/navbar.effects.spec.ts
+++ b/src/app/navbar/navbar.effects.spec.ts
@@ -4,7 +4,7 @@ import { HostWindowResizeAction } from '../shared/host-window.actions';
import { Observable } from 'rxjs';
import { provideMockActions } from '@ngrx/effects/testing';
import { cold, hot } from 'jasmine-marbles';
-import * as fromRouter from '@ngrx/router-store';
+import { ROUTER_NAVIGATION } from '@ngrx/router-store';
import { CollapseMenuAction } from '../shared/menu/menu.actions';
import { MenuService } from '../shared/menu/menu.service';
import { MenuServiceStub } from '../shared/testing/menu-service.stub';
@@ -43,7 +43,7 @@ describe('NavbarEffects', () => {
describe('routeChange$', () => {
it('should return a COLLAPSE action in response to an UPDATE_LOCATION action', () => {
- actions = hot('--a-', { a: { type: fromRouter.ROUTER_NAVIGATION } });
+ actions = hot('--a-', { a: { type: ROUTER_NAVIGATION } });
const expected = cold('--b-', { b: new CollapseMenuAction(MenuID.PUBLIC) });
diff --git a/src/app/navbar/navbar.effects.ts b/src/app/navbar/navbar.effects.ts
index 43992a1c61..dc3a29e1bf 100644
--- a/src/app/navbar/navbar.effects.ts
+++ b/src/app/navbar/navbar.effects.ts
@@ -1,7 +1,7 @@
import { first, map, switchMap } from 'rxjs/operators';
import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
-import * as fromRouter from '@ngrx/router-store';
+import { ROUTER_NAVIGATION } from '@ngrx/router-store';
import { HostWindowActionTypes } from '../shared/host-window.actions';
import {
@@ -33,7 +33,7 @@ export class NavbarEffects {
*/
routeChange$ = createEffect(() => this.actions$
.pipe(
- ofType(fromRouter.ROUTER_NAVIGATION),
+ ofType(ROUTER_NAVIGATION),
map(() => new CollapseMenuAction(this.menuID))
));
/**
diff --git a/src/app/navbar/navbar.module.ts b/src/app/navbar/navbar.module.ts
index af2bf036bd..de3244099d 100644
--- a/src/app/navbar/navbar.module.ts
+++ b/src/app/navbar/navbar.module.ts
@@ -21,6 +21,7 @@ const effects = [
const ENTRY_COMPONENTS = [
// put only entry components that use custom decorator
NavbarSectionComponent,
+ ExpandableNavbarSectionComponent,
ThemedExpandableNavbarSectionComponent,
];
@@ -34,11 +35,9 @@ const ENTRY_COMPONENTS = [
CoreModule.forRoot()
],
declarations: [
+ ...ENTRY_COMPONENTS,
NavbarComponent,
ThemedNavbarComponent,
- NavbarSectionComponent,
- ExpandableNavbarSectionComponent,
- ThemedExpandableNavbarSectionComponent,
],
providers: [],
exports: [
diff --git a/src/app/process-page/detail/process-detail.component.spec.ts b/src/app/process-page/detail/process-detail.component.spec.ts
index e4ab7d1082..8749553eae 100644
--- a/src/app/process-page/detail/process-detail.component.spec.ts
+++ b/src/app/process-page/detail/process-detail.component.spec.ts
@@ -15,7 +15,6 @@ import {
} from '@angular/core/testing';
import { VarDirective } from '../../shared/utils/var.directive';
import { TranslateModule } from '@ngx-translate/core';
-import { RouterTestingModule } from '@angular/router/testing';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { ProcessDetailFieldComponent } from './process-detail-field/process-detail-field.component';
import { Process } from '../processes/process.model';
diff --git a/src/app/profile-page/profile-page-metadata-form/profile-page-metadata-form.component.spec.ts b/src/app/profile-page/profile-page-metadata-form/profile-page-metadata-form.component.spec.ts
index 9c0942d4ff..1dc1db26cc 100644
--- a/src/app/profile-page/profile-page-metadata-form/profile-page-metadata-form.component.spec.ts
+++ b/src/app/profile-page/profile-page-metadata-form/profile-page-metadata-form.component.spec.ts
@@ -8,7 +8,7 @@ import { EPerson } from '../../core/eperson/models/eperson.model';
import { FormBuilderService } from '../../shared/form/builder/form-builder.service';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { EPersonDataService } from '../../core/eperson/eperson-data.service';
-import { cloneDeep } from 'lodash';
+import cloneDeep from 'lodash/cloneDeep';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
describe('ProfilePageMetadataFormComponent', () => {
diff --git a/src/app/profile-page/profile-page-metadata-form/profile-page-metadata-form.component.ts b/src/app/profile-page/profile-page-metadata-form/profile-page-metadata-form.component.ts
index 44ec406e80..474e6fb025 100644
--- a/src/app/profile-page/profile-page-metadata-form/profile-page-metadata-form.component.ts
+++ b/src/app/profile-page/profile-page-metadata-form/profile-page-metadata-form.component.ts
@@ -11,7 +11,7 @@ import { TranslateService } from '@ngx-translate/core';
import { hasValue, isNotEmpty } from '../../shared/empty.util';
import { LangConfig } from '../../../config/lang-config.interface';
import { EPersonDataService } from '../../core/eperson/eperson-data.service';
-import { cloneDeep } from 'lodash';
+import cloneDeep from 'lodash/cloneDeep';
import { getRemoteDataPayload, getFirstSucceededRemoteData } from '../../core/shared/operators';
import { FormBuilderService } from '../../shared/form/builder/form-builder.service';
import { NotificationsService } from '../../shared/notifications/notifications.service';
diff --git a/src/app/register-email-form/register-email-form.component.spec.ts b/src/app/register-email-form/register-email-form.component.spec.ts
index bac922c73b..cf3b4b13d2 100644
--- a/src/app/register-email-form/register-email-form.component.spec.ts
+++ b/src/app/register-email-form/register-email-form.component.spec.ts
@@ -95,6 +95,10 @@ describe('RegisterEmailComponent', () => {
comp.form.patchValue({email: 'valid@email.org'});
expect(comp.form.invalid).toBeFalse();
});
+ it('should be valid when uppercase letters are used', () => {
+ comp.form.patchValue({email: 'VALID@email.org'});
+ expect(comp.form.invalid).toBeFalse();
+ });
});
describe('register', () => {
it('should send a registration to the service and on success display a message and return to home', () => {
diff --git a/src/app/register-email-form/register-email-form.component.ts b/src/app/register-email-form/register-email-form.component.ts
index ced87b9e75..561bd53e67 100644
--- a/src/app/register-email-form/register-email-form.component.ts
+++ b/src/app/register-email-form/register-email-form.component.ts
@@ -79,7 +79,9 @@ export class RegisterEmailFormComponent implements OnInit {
this.form = this.formBuilder.group({
email: new FormControl('', {
validators: [Validators.required,
- Validators.pattern('^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$')
+ // Regex pattern borrowed from HTML5 specs for a valid email address:
+ // https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address
+ Validators.pattern('^[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$')
],
})
});
diff --git a/src/app/register-page/registration.guard.spec.ts b/src/app/register-page/registration.guard.spec.ts
index 89eaff7a02..9fb8dd3a33 100644
--- a/src/app/register-page/registration.guard.spec.ts
+++ b/src/app/register-page/registration.guard.spec.ts
@@ -2,7 +2,6 @@ import { RegistrationGuard } from './registration.guard';
import { EpersonRegistrationService } from '../core/data/eperson-registration.service';
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
import { AuthService } from '../core/auth/auth.service';
-import { Location } from '@angular/common';
import {
createFailedRemoteDataObject$,
createSuccessfulRemoteDataObject,
diff --git a/src/app/root/root.component.html b/src/app/root/root.component.html
index 8c160e6535..bf49e507c0 100644
--- a/src/app/root/root.component.html
+++ b/src/app/root/root.component.html
@@ -1,9 +1,9 @@
-
-
-
+
+
diff --git a/src/app/root/root.component.spec.ts b/src/app/root/root.component.spec.ts
index 504bc34e34..ab148b8ebd 100644
--- a/src/app/root/root.component.spec.ts
+++ b/src/app/root/root.component.spec.ts
@@ -17,7 +17,7 @@ import { ActivatedRoute, Router } from '@angular/router';
import { RouterMock } from '../shared/mocks/router.mock';
import { MockActivatedRoute } from '../shared/mocks/active-router.mock';
import { MenuService } from '../shared/menu/menu.service';
-import { CSSVariableService } from '../shared/sass-helper/sass-helper.service';
+import { CSSVariableService } from '../shared/sass-helper/css-variable.service';
import { CSSVariableServiceStub } from '../shared/testing/css-variable-service.stub';
import { HostWindowService } from '../shared/host-window.service';
import { HostWindowServiceStub } from '../shared/testing/host-window-service.stub';
diff --git a/src/app/root/root.component.ts b/src/app/root/root.component.ts
index 472ba440c9..99b14e6458 100644
--- a/src/app/root/root.component.ts
+++ b/src/app/root/root.component.ts
@@ -10,7 +10,7 @@ import { MetadataService } from '../core/metadata/metadata.service';
import { HostWindowState } from '../shared/search/host-window.reducer';
import { NativeWindowRef, NativeWindowService } from '../core/services/window.service';
import { AuthService } from '../core/auth/auth.service';
-import { CSSVariableService } from '../shared/sass-helper/sass-helper.service';
+import { CSSVariableService } from '../shared/sass-helper/css-variable.service';
import { MenuService } from '../shared/menu/menu.service';
import { HostWindowService } from '../shared/host-window.service';
import { ThemeConfig } from '../../config/theme.model';
@@ -63,8 +63,8 @@ export class RootComponent implements OnInit {
ngOnInit() {
this.sidebarVisible = this.menuService.isMenuVisible(MenuID.ADMIN);
- this.collapsedSidebarWidth = this.cssService.getVariable('collapsedSidebarWidth');
- this.totalSidebarWidth = this.cssService.getVariable('totalSidebarWidth');
+ this.collapsedSidebarWidth = this.cssService.getVariable('--ds-collapsed-sidebar-width');
+ this.totalSidebarWidth = this.cssService.getVariable('--ds-total-sidebar-width');
const sidebarCollapsed = this.menuService.isMenuCollapsed(MenuID.ADMIN);
this.slideSidebarOver = combineLatestObservable([sidebarCollapsed, this.windowService.isXsOrSm()])
diff --git a/src/app/shared/animations/slide.ts b/src/app/shared/animations/slide.ts
index b396333fb4..310ddbbfde 100644
--- a/src/app/shared/animations/slide.ts
+++ b/src/app/shared/animations/slide.ts
@@ -12,7 +12,7 @@ export const slide = trigger('slide', [
export const slideMobileNav = trigger('slideMobileNav', [
- state('expanded', style({ height: '100vh' })),
+ state('expanded', style({ height: 'auto', 'min-height': '100vh' })),
state('collapsed', style({ height: 0 })),
diff --git a/src/app/shared/auth-nav-menu/auth-nav-menu.component.html b/src/app/shared/auth-nav-menu/auth-nav-menu.component.html
index c2b414b6f3..94cbd4368a 100644
--- a/src/app/shared/auth-nav-menu/auth-nav-menu.component.html
+++ b/src/app/shared/auth-nav-menu/auth-nav-menu.component.html
@@ -2,11 +2,11 @@
-
- {{ 'nav.login' | translate }}
-
+
{{ 'nav.login' | translate }}
@@ -19,16 +19,16 @@
-
+
-
-
+
(current)
diff --git a/src/app/shared/auth-nav-menu/user-menu/user-menu.component.html b/src/app/shared/auth-nav-menu/user-menu/user-menu.component.html
index 736d39d318..e730b0d85c 100644
--- a/src/app/shared/auth-nav-menu/user-menu/user-menu.component.html
+++ b/src/app/shared/auth-nav-menu/user-menu/user-menu.component.html
@@ -1,10 +1,13 @@
diff --git a/src/app/shared/auth-nav-menu/user-menu/user-menu.component.spec.ts b/src/app/shared/auth-nav-menu/user-menu/user-menu.component.spec.ts
index 983fe68274..5576b942b3 100644
--- a/src/app/shared/auth-nav-menu/user-menu/user-menu.component.spec.ts
+++ b/src/app/shared/auth-nav-menu/user-menu/user-menu.component.spec.ts
@@ -162,10 +162,24 @@ describe('UserMenuComponent', () => {
});
it('should display user name and email', () => {
- const user = 'User Test (test@test.com)';
+ const username = 'User Test';
+ const email = 'test@test.com';
const span = deUserMenu.query(By.css('.dropdown-item-text'));
expect(span).toBeDefined();
- expect(span.nativeElement.innerHTML).toBe(user);
+ expect(span.nativeElement.innerHTML).toContain(username);
+ expect(span.nativeElement.innerHTML).toContain(email);
+ });
+
+ it('should create logout component', () => {
+ const components = fixture.debugElement.query(By.css('[data-test="log-out-component"]'));
+ expect(components).toBeTruthy();
+ });
+
+ it('should not create logout component', () => {
+ component.inExpandableNavbar = true;
+ fixture.detectChanges();
+ const components = fixture.debugElement.query(By.css('[data-test="log-out-component"]'));
+ expect(components).toBeFalsy();
});
});
diff --git a/src/app/shared/auth-nav-menu/user-menu/user-menu.component.ts b/src/app/shared/auth-nav-menu/user-menu/user-menu.component.ts
index aa78be9749..22b076c31a 100644
--- a/src/app/shared/auth-nav-menu/user-menu/user-menu.component.ts
+++ b/src/app/shared/auth-nav-menu/user-menu/user-menu.component.ts
@@ -1,4 +1,4 @@
-import { Component, OnInit } from '@angular/core';
+import { Component, Input, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { select, Store } from '@ngrx/store';
@@ -20,6 +20,11 @@ import { getProfileModuleRoute } from '../../../app-routing-paths';
})
export class UserMenuComponent implements OnInit {
+ /**
+ * The input flag to show user details in navbar expandable menu
+ */
+ @Input() inExpandableNavbar = false;
+
/**
* True if the authentication is loading.
* @type {Observable
}
diff --git a/src/app/shared/collection-dropdown/themed-collection-dropdown.component.ts b/src/app/shared/collection-dropdown/themed-collection-dropdown.component.ts
new file mode 100644
index 0000000000..27c883099d
--- /dev/null
+++ b/src/app/shared/collection-dropdown/themed-collection-dropdown.component.ts
@@ -0,0 +1,33 @@
+import { CollectionDropdownComponent, CollectionListEntry } from './collection-dropdown.component';
+import { ThemedComponent } from '../theme-support/themed.component';
+import { Component, Input, Output, EventEmitter } from '@angular/core';
+
+@Component({
+ selector: 'ds-themed-collection-dropdown',
+ styleUrls: [],
+ templateUrl: '../../shared/theme-support/themed.component.html',
+})
+export class ThemedCollectionDropdownComponent extends ThemedComponent {
+
+ @Input() entityType: string;
+
+ @Output() searchComplete = new EventEmitter();
+
+ @Output() theOnlySelectable = new EventEmitter();
+
+ @Output() selectionChange = new EventEmitter();
+
+ protected inAndOutputNames: (keyof CollectionDropdownComponent & keyof this)[] = ['entityType', 'searchComplete', 'theOnlySelectable', 'selectionChange'];
+
+ protected getComponentName(): string {
+ return 'CollectionDropdownComponent';
+ }
+
+ protected importThemedComponent(themeName: string): Promise {
+ return import(`../../../themes/${themeName}/app/shared/collection-dropdown/collection-dropdown.component`);
+ }
+
+ protected importUnthemedComponent(): Promise {
+ return import(`./collection-dropdown.component`);
+ }
+}
diff --git a/src/app/shared/comcol/comcol-forms/comcol-form/comcol-form.component.ts b/src/app/shared/comcol/comcol-forms/comcol-form/comcol-form.component.ts
index 29be240753..23dfca8616 100644
--- a/src/app/shared/comcol/comcol-forms/comcol-form/comcol-form.component.ts
+++ b/src/app/shared/comcol/comcol-forms/comcol-form/comcol-form.component.ts
@@ -17,8 +17,8 @@ import { MetadataMap, MetadataValue } from '../../../../core/shared/metadata.mod
import { ResourceType } from '../../../../core/shared/resource-type';
import { hasValue, isNotEmpty } from '../../../empty.util';
import { NotificationsService } from '../../../notifications/notifications.service';
-import { UploaderOptions } from '../../../uploader/uploader-options.model';
-import { UploaderComponent } from '../../../uploader/uploader.component';
+import { UploaderOptions } from '../../../upload/uploader/uploader-options.model';
+import { UploaderComponent } from '../../../upload/uploader/uploader.component';
import { Operation } from 'fast-json-patch';
import { NoContent } from '../../../../core/shared/NoContent.model';
import { getFirstCompletedRemoteData } from '../../../../core/shared/operators';
diff --git a/src/app/shared/comcol/comcol-forms/delete-comcol-page/delete-comcol-page.component.spec.ts b/src/app/shared/comcol/comcol-forms/delete-comcol-page/delete-comcol-page.component.spec.ts
index bc73e4134b..1040e31c57 100644
--- a/src/app/shared/comcol/comcol-forms/delete-comcol-page/delete-comcol-page.component.spec.ts
+++ b/src/app/shared/comcol/comcol-forms/delete-comcol-page/delete-comcol-page.component.spec.ts
@@ -11,7 +11,6 @@ import { NO_ERRORS_SCHEMA } from '@angular/core';
import { DeleteComColPageComponent } from './delete-comcol-page.component';
import { NotificationsService } from '../../../notifications/notifications.service';
import { NotificationsServiceStub } from '../../../testing/notifications-service.stub';
-import { RequestService } from '../../../../core/data/request.service';
import { getTestScheduler } from 'jasmine-marbles';
import { ComColDataService } from '../../../../core/data/comcol-data.service';
import { createFailedRemoteDataObject$, createNoContentRemoteDataObject$ } from '../../../remote-data.utils';
diff --git a/src/app/shared/comcol/comcol.module.ts b/src/app/shared/comcol/comcol.module.ts
index 094387929a..efbcedf2c6 100644
--- a/src/app/shared/comcol/comcol.module.ts
+++ b/src/app/shared/comcol/comcol.module.ts
@@ -15,6 +15,7 @@ import { ThemedComcolPageBrowseByComponent } from './comcol-page-browse-by/theme
import { ComcolRoleComponent } from './comcol-forms/edit-comcol-page/comcol-role/comcol-role.component';
import { SharedModule } from '../shared.module';
import { FormModule } from '../form/form.module';
+import { UploadModule } from '../upload/upload.module';
const COMPONENTS = [
ComcolPageContentComponent,
@@ -28,9 +29,7 @@ const COMPONENTS = [
ComcolPageBrowseByComponent,
ThemedComcolPageBrowseByComponent,
ComcolRoleComponent,
-
ThemedComcolPageHandleComponent
-
];
@NgModule({
@@ -40,10 +39,12 @@ const COMPONENTS = [
imports: [
CommonModule,
FormModule,
- SharedModule
+ SharedModule,
+ UploadModule,
],
exports: [
- ...COMPONENTS
+ ...COMPONENTS,
+ UploadModule,
]
})
export class ComcolModule { }
diff --git a/src/app/shared/cookies/browser-klaro.service.spec.ts b/src/app/shared/cookies/browser-klaro.service.spec.ts
index 9db9caf364..7fd72b54b3 100644
--- a/src/app/shared/cookies/browser-klaro.service.spec.ts
+++ b/src/app/shared/cookies/browser-klaro.service.spec.ts
@@ -10,7 +10,8 @@ import { AuthService } from '../../core/auth/auth.service';
import { CookieService } from '../../core/services/cookie.service';
import { getTestScheduler } from 'jasmine-marbles';
import { MetadataValue } from '../../core/shared/metadata.models';
-import { clone, cloneDeep } from 'lodash';
+import clone from 'lodash/clone';
+import cloneDeep from 'lodash/cloneDeep';
import { ConfigurationDataService } from '../../core/data/configuration-data.service';
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../remote-data.utils';
import { ConfigurationProperty } from '../../core/shared/configuration-property.model';
@@ -100,7 +101,7 @@ describe('BrowserKlaroService', () => {
mockConfig = {
translations: {
- en: {
+ zz: {
purposes: {},
test: {
testeritis: testKey
@@ -158,8 +159,8 @@ describe('BrowserKlaroService', () => {
it('addAppMessages', () => {
service.addAppMessages();
- expect(mockConfig.translations.en[appName]).toBeDefined();
- expect(mockConfig.translations.en.purposes[purpose]).toBeDefined();
+ expect(mockConfig.translations.zz[appName]).toBeDefined();
+ expect(mockConfig.translations.zz.purposes[purpose]).toBeDefined();
});
it('translateConfiguration', () => {
diff --git a/src/app/shared/cookies/browser-klaro.service.ts b/src/app/shared/cookies/browser-klaro.service.ts
index c6819012d9..3e830811b3 100644
--- a/src/app/shared/cookies/browser-klaro.service.ts
+++ b/src/app/shared/cookies/browser-klaro.service.ts
@@ -1,5 +1,4 @@
-import { Injectable } from '@angular/core';
-import * as Klaro from 'klaro';
+import { Inject, Injectable, InjectionToken } from '@angular/core';
import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs';
import { AuthService } from '../../core/auth/auth.service';
import { TranslateService } from '@ngx-translate/core';
@@ -10,7 +9,8 @@ import { KlaroService } from './klaro.service';
import { hasValue, isEmpty, isNotEmpty } from '../empty.util';
import { CookieService } from '../../core/services/cookie.service';
import { EPersonDataService } from '../../core/eperson/eperson-data.service';
-import { cloneDeep, debounce } from 'lodash';
+import cloneDeep from 'lodash/cloneDeep';
+import debounce from 'lodash/debounce';
import { ANONYMOUS_STORAGE_NAME_KLARO, klaroConfiguration } from './klaro-configuration';
import { Operation } from 'fast-json-patch';
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
@@ -42,6 +42,17 @@ const cookiePurposeMessagePrefix = 'cookies.consent.purpose.';
*/
const updateDebounce = 300;
+/**
+ * By using this injection token instead of importing directly we can keep Klaro out of the main bundle
+ */
+const LAZY_KLARO = new InjectionToken>(
+ 'Lazily loaded Klaro',
+ {
+ providedIn: 'root',
+ factory: async () => (await import('klaro')),
+ }
+);
+
/**
* Browser implementation for the KlaroService, representing a service for handling Klaro consent preferences and UI
*/
@@ -64,7 +75,9 @@ export class BrowserKlaroService extends KlaroService {
private authService: AuthService,
private ePersonService: EPersonDataService,
private configService: ConfigurationDataService,
- private cookieService: CookieService) {
+ private cookieService: CookieService,
+ @Inject(LAZY_KLARO) private lazyKlaro: Promise,
+ ) {
super();
}
@@ -78,7 +91,7 @@ export class BrowserKlaroService extends KlaroService {
initialize() {
if (!environment.info.enablePrivacyStatement) {
delete this.klaroConfig.privacyPolicy;
- this.klaroConfig.translations.en.consentNotice.description = 'cookies.consent.content-notice.description.no-privacy';
+ this.klaroConfig.translations.zz.consentNotice.description = 'cookies.consent.content-notice.description.no-privacy';
}
const hideGoogleAnalytics$ = this.configService.findByPropertyName(this.GOOGLE_ANALYTICS_KEY).pipe(
@@ -134,8 +147,7 @@ export class BrowserKlaroService extends KlaroService {
this.translateConfiguration();
this.klaroConfig.services = this.filterConfigServices(servicesToHide);
-
- Klaro.setup(this.klaroConfig);
+ this.lazyKlaro.then(({ setup }) => setup(this.klaroConfig));
});
}
@@ -219,7 +231,7 @@ export class BrowserKlaroService extends KlaroService {
* Show the cookie consent form
*/
showSettings() {
- Klaro.show(this.klaroConfig);
+ this.lazyKlaro.then(({show}) => show(this.klaroConfig));
}
/**
@@ -227,12 +239,12 @@ export class BrowserKlaroService extends KlaroService {
*/
addAppMessages() {
this.klaroConfig.services.forEach((app) => {
- this.klaroConfig.translations.en[app.name] = {
+ this.klaroConfig.translations.zz[app.name] = {
title: this.getTitleTranslation(app.name),
description: this.getDescriptionTranslation(app.name)
};
app.purposes.forEach((purpose) => {
- this.klaroConfig.translations.en.purposes[purpose] = this.getPurposeTranslation(purpose);
+ this.klaroConfig.translations.zz.purposes[purpose] = this.getPurposeTranslation(purpose);
});
});
}
@@ -246,7 +258,7 @@ export class BrowserKlaroService extends KlaroService {
*/
this.translateService.setDefaultLang(environment.defaultLanguage);
- this.translate(this.klaroConfig.translations.en);
+ this.translate(this.klaroConfig.translations.zz);
}
/**
diff --git a/src/app/shared/cookies/klaro-configuration.ts b/src/app/shared/cookies/klaro-configuration.ts
index 8a9855bd89..a41b641dec 100644
--- a/src/app/shared/cookies/klaro-configuration.ts
+++ b/src/app/shared/cookies/klaro-configuration.ts
@@ -54,10 +54,46 @@ export const klaroConfiguration: any = {
https://github.com/KIProtect/klaro/tree/master/src/translations
*/
translations: {
- en: {
+ /*
+ The `zz` key contains default translations that will be used as fallback values.
+ This can e.g. be useful for defining a fallback privacy policy URL.
+ FOR DSPACE: We use 'zz' to map to our own i18n translations for klaro, see
+ translateConfiguration() in browser-klaro.service.ts. All the below i18n keys are specified
+ in your /src/assets/i18n/*.json5 translation pack.
+ */
+ zz: {
acceptAll: 'cookies.consent.accept-all',
acceptSelected: 'cookies.consent.accept-selected',
- app: {
+ close: 'cookies.consent.close',
+ consentModal: {
+ title: 'cookies.consent.content-modal.title',
+ description: 'cookies.consent.content-modal.description'
+ },
+ consentNotice: {
+ changeDescription: 'cookies.consent.update',
+ title: 'cookies.consent.content-notice.title',
+ description: 'cookies.consent.content-notice.description',
+ learnMore: 'cookies.consent.content-notice.learnMore',
+ },
+ decline: 'cookies.consent.decline',
+ ok: 'cookies.consent.ok',
+ poweredBy: 'Powered by Klaro!',
+ privacyPolicy: {
+ name: 'cookies.consent.content-modal.privacy-policy.name',
+ text: 'cookies.consent.content-modal.privacy-policy.text'
+ },
+ purposeItem: {
+ service: 'cookies.consent.content-modal.service',
+ services: 'cookies.consent.content-modal.services'
+ },
+ purposes: {
+ },
+ save: 'cookies.consent.save',
+ service: {
+ disableAll: {
+ description: 'cookies.consent.app.disable-all.description',
+ title: 'cookies.consent.app.disable-all.title'
+ },
optOut: {
description: 'cookies.consent.app.opt-out.description',
title: 'cookies.consent.app.opt-out.title'
@@ -65,26 +101,10 @@ export const klaroConfiguration: any = {
purpose: 'cookies.consent.app.purpose',
purposes: 'cookies.consent.app.purposes',
required: {
- description: 'cookies.consent.app.required.description',
- title: 'cookies.consent.app.required.title'
+ title: 'cookies.consent.app.required.title',
+ description: 'cookies.consent.app.required.description'
}
- },
- close: 'cookies.consent.close',
- decline: 'cookies.consent.decline',
- changeDescription: 'cookies.consent.update',
- consentNotice: {
- description: 'cookies.consent.content-notice.description',
- learnMore: 'cookies.consent.content-notice.learnMore'
- },
- consentModal: {
- description: 'cookies.consent.content-modal.description',
- privacyPolicy: {
- name: 'cookies.consent.content-modal.privacy-policy.name',
- text: 'cookies.consent.content-modal.privacy-policy.text'
- },
- title: 'cookies.consent.content-modal.title'
- },
- purposes: {}
+ }
}
},
services: [
diff --git a/src/app/shared/date.util.spec.ts b/src/app/shared/date.util.spec.ts
new file mode 100644
index 0000000000..4576ea497c
--- /dev/null
+++ b/src/app/shared/date.util.spec.ts
@@ -0,0 +1,107 @@
+import { dateToString, dateToNgbDateStruct, dateToISOFormat, isValidDate, yearFromString } from './date.util';
+
+describe('Date Utils', () => {
+
+ describe('dateToISOFormat', () => {
+ it('should convert Date to YYYY-MM-DDThh:mm:ssZ string', () => {
+ // NOTE: month is zero indexed which is why it increases by one
+ expect(dateToISOFormat(new Date(Date.UTC(2022, 5, 3)))).toEqual('2022-06-03T00:00:00Z');
+ });
+ it('should convert Date string to YYYY-MM-DDThh:mm:ssZ string', () => {
+ expect(dateToISOFormat('2022-06-03')).toEqual('2022-06-03T00:00:00Z');
+ });
+ it('should convert Month string to YYYY-MM-DDThh:mm:ssZ string', () => {
+ expect(dateToISOFormat('2022-06')).toEqual('2022-06-01T00:00:00Z');
+ });
+ it('should convert Year string to YYYY-MM-DDThh:mm:ssZ string', () => {
+ expect(dateToISOFormat('2022')).toEqual('2022-01-01T00:00:00Z');
+ });
+ it('should convert ISO Date string to YYYY-MM-DDThh:mm:ssZ string', () => {
+ // NOTE: Time is always zeroed out as proven by this test.
+ expect(dateToISOFormat('2022-06-03T03:24:04Z')).toEqual('2022-06-03T00:00:00Z');
+ });
+ it('should convert NgbDateStruct to YYYY-MM-DDThh:mm:ssZ string', () => {
+ // NOTE: month is zero indexed which is why it increases by one
+ const date = new Date(Date.UTC(2022, 5, 3));
+ expect(dateToISOFormat(dateToNgbDateStruct(date))).toEqual('2022-06-03T00:00:00Z');
+ });
+ });
+
+ describe('dateToString', () => {
+ it('should convert Date to YYYY-MM-DD string', () => {
+ // NOTE: month is zero indexed which is why it increases by one
+ expect(dateToString(new Date(Date.UTC(2022, 5, 3)))).toEqual('2022-06-03');
+ });
+ it('should convert Date with time to YYYY-MM-DD string', () => {
+ // NOTE: month is zero indexed which is why it increases by one
+ expect(dateToString(new Date(Date.UTC(2022, 5, 3, 3, 24, 0)))).toEqual('2022-06-03');
+ });
+ it('should convert Month only to YYYY-MM-DD string', () => {
+ // NOTE: month is zero indexed which is why it increases by one
+ expect(dateToString(new Date(Date.UTC(2022, 5)))).toEqual('2022-06-01');
+ });
+ it('should convert ISO Date to YYYY-MM-DD string', () => {
+ expect(dateToString(new Date('2022-06-03T03:24:00Z'))).toEqual('2022-06-03');
+ });
+ it('should convert NgbDateStruct to YYYY-MM-DD string', () => {
+ // NOTE: month is zero indexed which is why it increases by one
+ const date = new Date(Date.UTC(2022, 5, 3));
+ expect(dateToString(dateToNgbDateStruct(date))).toEqual('2022-06-03');
+ });
+ });
+
+
+ describe('isValidDate', () => {
+ it('should return false for null', () => {
+ expect(isValidDate(null)).toBe(false);
+ });
+ it('should return false for empty string', () => {
+ expect(isValidDate('')).toBe(false);
+ });
+ it('should return false for text', () => {
+ expect(isValidDate('test')).toBe(false);
+ });
+ it('should return true for YYYY', () => {
+ expect(isValidDate('2022')).toBe(true);
+ });
+ it('should return true for YYYY-MM', () => {
+ expect(isValidDate('2022-12')).toBe(true);
+ });
+ it('should return true for YYYY-MM-DD', () => {
+ expect(isValidDate('2022-06-03')).toBe(true);
+ });
+ it('should return true for YYYY-MM-DDTHH:MM:SS', () => {
+ expect(isValidDate('2022-06-03T10:20:30')).toBe(true);
+ });
+ it('should return true for YYYY-MM-DDTHH:MM:SSZ', () => {
+ expect(isValidDate('2022-06-03T10:20:30Z')).toBe(true);
+ });
+ it('should return false for a month that does not exist', () => {
+ expect(isValidDate('2022-13')).toBe(false);
+ });
+ it('should return false for a day that does not exist', () => {
+ expect(isValidDate('2022-02-60')).toBe(false);
+ });
+ it('should return false for a time that does not exist', () => {
+ expect(isValidDate('2022-02-60T10:60:20')).toBe(false);
+ });
+ });
+
+ describe('yearFromString', () => {
+ it('should return year from YYYY string', () => {
+ expect(yearFromString('2022')).toEqual(2022);
+ });
+ it('should return year from YYYY-MM string', () => {
+ expect(yearFromString('1970-06')).toEqual(1970);
+ });
+ it('should return year from YYYY-MM-DD string', () => {
+ expect(yearFromString('1914-10-23')).toEqual(1914);
+ });
+ it('should return year from YYYY-MM-DDTHH:MM:SSZ string', () => {
+ expect(yearFromString('1914-10-23T10:20:30Z')).toEqual(1914);
+ });
+ it('should return null if invalid date', () => {
+ expect(yearFromString('test')).toBeNull();
+ });
+ });
+});
diff --git a/src/app/shared/date.util.ts b/src/app/shared/date.util.ts
index 5f7ccb2438..5b74ed02d2 100644
--- a/src/app/shared/date.util.ts
+++ b/src/app/shared/date.util.ts
@@ -1,9 +1,8 @@
import { NgbDateStruct } from '@ng-bootstrap/ng-bootstrap';
-
-import { isObject } from 'lodash';
-import * as moment from 'moment';
-
-import { isNull, isUndefined } from './empty.util';
+import { formatInTimeZone } from 'date-fns-tz';
+import { isValid } from 'date-fns';
+import isObject from 'lodash/isObject';
+import { hasNoValue } from './empty.util';
/**
* Returns true if the passed value is a NgbDateStruct.
@@ -31,21 +30,7 @@ export function dateToISOFormat(date: Date | NgbDateStruct | string): string {
const dateObj: Date = (date instanceof Date) ? date :
((typeof date === 'string') ? ngbDateStructToDate(stringToNgbDateStruct(date)) : ngbDateStructToDate(date));
- let year = dateObj.getUTCFullYear().toString();
- let month = (dateObj.getUTCMonth() + 1).toString();
- let day = dateObj.getUTCDate().toString();
- let hour = dateObj.getHours().toString();
- let min = dateObj.getMinutes().toString();
- let sec = dateObj.getSeconds().toString();
-
- year = (year.length === 1) ? '0' + year : year;
- month = (month.length === 1) ? '0' + month : month;
- day = (day.length === 1) ? '0' + day : day;
- hour = (hour.length === 1) ? '0' + hour : hour;
- min = (min.length === 1) ? '0' + min : min;
- sec = (sec.length === 1) ? '0' + sec : sec;
- const dateStr = `${year}${month}${day}${hour}${min}${sec}`;
- return moment.utc(dateStr, 'YYYYMMDDhhmmss').format();
+ return formatInTimeZone(dateObj, 'UTC', "yyyy-MM-dd'T'HH:mm:ss'Z'");
}
/**
@@ -81,7 +66,7 @@ export function stringToNgbDateStruct(date: string): NgbDateStruct {
* the NgbDateStruct object
*/
export function dateToNgbDateStruct(date?: Date): NgbDateStruct {
- if (isNull(date) || isUndefined(date)) {
+ if (hasNoValue(date)) {
date = new Date();
}
@@ -102,16 +87,7 @@ export function dateToNgbDateStruct(date?: Date): NgbDateStruct {
*/
export function dateToString(date: Date | NgbDateStruct): string {
const dateObj: Date = (date instanceof Date) ? date : ngbDateStructToDate(date);
-
- let year = dateObj.getUTCFullYear().toString();
- let month = (dateObj.getUTCMonth() + 1).toString();
- let day = dateObj.getUTCDate().toString();
-
- year = (year.length === 1) ? '0' + year : year;
- month = (month.length === 1) ? '0' + month : month;
- day = (day.length === 1) ? '0' + day : day;
- const dateStr = `${year}-${month}-${day}`;
- return moment.utc(dateStr, 'YYYYMMDD').format('YYYY-MM-DD');
+ return formatInTimeZone(dateObj, 'UTC', 'yyyy-MM-dd');
}
/**
@@ -119,5 +95,15 @@ export function dateToString(date: Date | NgbDateStruct): string {
* @param date the string to be checked
*/
export function isValidDate(date: string) {
- return moment(date).isValid();
+ return (hasNoValue(date)) ? false : isValid(new Date(date));
}
+
+/**
+ * Parse given date string to a year number based on expected formats
+ * @param date the string to be parsed
+ * @param formats possible formats the string may align with. MUST be valid date-fns formats
+ */
+export function yearFromString(date: string) {
+ return isValidDate(date) ? new Date(date).getUTCFullYear() : null;
+}
+
diff --git a/src/app/shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component.ts b/src/app/shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component.ts
index ab48d058ca..cc1f9822d6 100644
--- a/src/app/shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component.ts
+++ b/src/app/shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component.ts
@@ -53,8 +53,9 @@ export class AuthorizedCollectionSelectorComponent extends DSOSelectorComponent
* Perform a search for authorized collections with the current query and page
* @param query Query to search objects for
* @param page Page to retrieve
+ * @param useCache Whether or not to use the cache
*/
- search(query: string, page: number): Observable>>> {
+ search(query: string, page: number, useCache: boolean = true): Observable>>> {
let searchListService$: Observable>> = null;
const findOptions: FindListOptions = {
currentPage: page,
@@ -69,7 +70,7 @@ export class AuthorizedCollectionSelectorComponent extends DSOSelectorComponent
findOptions);
} else {
searchListService$ = this.collectionDataService
- .getAuthorizedCollection(query, findOptions, true, false, followLink('parentCommunity'));
+ .getAuthorizedCollection(query, findOptions, useCache, false, followLink('parentCommunity'));
}
return searchListService$.pipe(
getFirstCompletedRemoteData(),
diff --git a/src/app/shared/dso-selector/dso-selector/dso-selector.component.html b/src/app/shared/dso-selector/dso-selector/dso-selector.component.html
index 8abb8ad558..c4f5dbc4cd 100644
--- a/src/app/shared/dso-selector/dso-selector/dso-selector.component.html
+++ b/src/app/shared/dso-selector/dso-selector/dso-selector.component.html
@@ -21,12 +21,12 @@
+ (click)="onClick(listEntry)" #listEntryElement>
+ [linkType]=linkTypes.None [context]="getContext(listEntry['id'])">
[]> = new BehaviorSubject(null);
+ listEntries$: BehaviorSubject = new BehaviorSubject(null);
/**
* The current page to load
@@ -116,11 +124,6 @@ export class DSOSelectorComponent implements OnInit, OnDestroy {
*/
linkTypes = CollectionElementLinkType;
- /**
- * Track whether the element has the mouse over it
- */
- isMouseOver = false;
-
/**
* Array to track all subscriptions and unsubscribe them onDestroy
* @type {Array}
@@ -182,24 +185,28 @@ export class DSOSelectorComponent implements OnInit, OnDestroy {
})
);
})
- ).subscribe((rd) => {
- this.loading = false;
- if (rd.hasSucceeded) {
- const currentEntries = this.listEntries$.getValue();
- if (hasNoValue(currentEntries)) {
- this.listEntries$.next(rd.payload.page);
- } else {
- this.listEntries$.next([...currentEntries, ...rd.payload.page]);
- }
- // Check if there are more pages available after the current one
- this.hasNextPage = rd.payload.totalElements > this.listEntries$.getValue().length;
- } else {
- this.listEntries$.next(null);
- this.hasNextPage = false;
- }
+ ).subscribe((rd: RemoteData>>) => {
+ this.updateList(rd);
}));
}
+ updateList(rd: RemoteData>>) {
+ this.loading = false;
+ const currentEntries = this.listEntries$.getValue();
+ if (rd.hasSucceeded) {
+ if (hasNoValue(currentEntries)) {
+ this.listEntries$.next(rd.payload.page);
+ } else {
+ this.listEntries$.next([...currentEntries, ...rd.payload.page]);
+ }
+ // Check if there are more pages available after the current one
+ this.hasNextPage = rd.payload.totalElements > this.listEntries$.getValue().length;
+ } else {
+ this.listEntries$.next([...(hasNoValue(currentEntries) ? [] : this.listEntries$.getValue()), new ListableNotificationObject(NotificationType.Error, 'dso-selector.results-could-not-be-retrieved', LISTABLE_NOTIFICATION_OBJECT.value)]);
+ this.hasNextPage = false;
+ }
+ }
+
/**
* Get a query to send for retrieving the current DSO
*/
@@ -211,8 +218,9 @@ export class DSOSelectorComponent implements OnInit, OnDestroy {
* Perform a search for the current query and page
* @param query Query to search objects for
* @param page Page to retrieve
+ * @param useCache Whether or not to use the cache
*/
- search(query: string, page: number): Observable>>> {
+ search(query: string, page: number, useCache: boolean = true): Observable>>> {
return this.searchService.search(
new PaginatedSearchOptions({
query: query,
@@ -220,7 +228,9 @@ export class DSOSelectorComponent implements OnInit, OnDestroy {
pagination: Object.assign({}, this.defaultPagination, {
currentPage: page
})
- })
+ }),
+ null,
+ useCache,
).pipe(
getFirstCompletedRemoteData()
);
@@ -262,7 +272,28 @@ export class DSOSelectorComponent implements OnInit, OnDestroy {
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
}
- getName(searchResult: SearchResult): string {
- return this.dsoNameService.getName(searchResult.indexableObject);
+ /**
+ * Handles the user clicks on the {@link ListableObject}s. When the {@link listableObject} is a
+ * {@link ListableObject} it will retry the error when the user clicks it. Otherwise it will emit the {@link onSelect}.
+ *
+ * @param listableObject The {@link ListableObject} to evaluate
+ */
+ onClick(listableObject: ListableObject): void {
+ if (hasValue((listableObject as SearchResult).indexableObject)) {
+ this.onSelect.emit((listableObject as SearchResult).indexableObject);
+ } else {
+ this.listEntries$.value.pop();
+ this.hasNextPage = true;
+ this.search(this.input.value ? this.input.value : '', this.currentPage$.value, false).pipe(
+ getFirstCompletedRemoteData(),
+ ).subscribe((rd: RemoteData>>) => {
+ this.updateList(rd);
+ });
+ }
+ }
+
+ getName(listableObject: ListableObject): string {
+ return hasValue((listableObject as SearchResult).indexableObject) ?
+ this.dsoNameService.getName((listableObject as SearchResult).indexableObject) : null;
}
}
diff --git a/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.html b/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.html
index 84fdd34c01..4a22672988 100644
--- a/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.html
+++ b/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.html
@@ -9,7 +9,7 @@
diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts
index e0e519aaa9..925fabe5e0 100644
--- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts
+++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts
@@ -79,6 +79,8 @@ import { FormService } from '../../form.service';
import { SubmissionService } from '../../../../submission/submission.service';
import { FormBuilderService } from '../form-builder.service';
import { NgxMaskModule } from 'ngx-mask';
+import { APP_CONFIG } from '../../../../../config/app-config.interface';
+import { environment } from '../../../../../environments/environment';
function getMockDsDynamicTypeBindRelationService(): DsDynamicTypeBindRelationService {
return jasmine.createSpyObj('DsDynamicTypeBindRelationService', {
@@ -230,7 +232,8 @@ describe('DsDynamicFormControlContainerComponent test suite', () => {
findById: () => observableOf(createSuccessfulRemoteDataObject(testWSI))
}
},
- { provide: NgZone, useValue: new NgZone({}) }
+ { provide: NgZone, useValue: new NgZone({}) },
+ { provide: APP_CONFIG, useValue: environment }
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
}).compileComponents().then(() => {
diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts
index 812735e69c..77ce65675a 100644
--- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts
+++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts
@@ -4,7 +4,7 @@ import {
Component,
ComponentFactoryResolver,
ContentChildren,
- EventEmitter,
+ EventEmitter, Inject,
Input,
NgZone,
OnChanges,
@@ -118,6 +118,8 @@ import { RelationshipOptions } from '../models/relationship-options.model';
import { FormBuilderService } from '../form-builder.service';
import { DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP } from './ds-dynamic-form-constants';
import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model';
+import { APP_CONFIG, AppConfig } from '../../../../../config/app-config.interface';
+import { itemLinksToFollow } from '../../../utils/relation-query.utils';
export function dsDynamicFormControlMapFn(model: DynamicFormControlModel): Type | null {
switch (model.type) {
@@ -231,6 +233,11 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
private showErrorMessagesPreviousStage: boolean;
+ /**
+ * Determines whether to request embedded thumbnail.
+ */
+ fetchThumbnail: boolean;
+
get componentType(): Type | null {
return dsDynamicFormControlMapFn(this.model);
}
@@ -253,9 +260,11 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
private ref: ChangeDetectorRef,
private formService: FormService,
private formBuilderService: FormBuilderService,
- private submissionService: SubmissionService
+ private submissionService: SubmissionService,
+ @Inject(APP_CONFIG) protected appConfig: AppConfig,
) {
super(ref, componentFactoryResolver, layoutService, validationService, dynamicFormComponentService, relationService);
+ this.fetchThumbnail = this.appConfig.browseBy.showThumbnails;
}
/**
@@ -285,7 +294,6 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
followLink('rightItem'),
followLink('relationshipType')
);
-
relationshipsRD$.pipe(
getFirstSucceededRemoteDataPayload(),
getPaginatedListPayload()
@@ -317,8 +325,10 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
}
if (hasValue(this.value) && this.value.isVirtual) {
- const relationship$ = this.relationshipService.findById(this.value.virtualValue, true, true, followLink('leftItem'), followLink('rightItem'), followLink('relationshipType'))
- .pipe(
+ const relationship$ = this.relationshipService.findById(this.value.virtualValue,
+ true,
+ true,
+ ... itemLinksToFollow(this.fetchThumbnail)).pipe(
getAllSucceededRemoteData(),
getRemoteDataPayload());
this.relationshipValue$ = observableCombineLatest([this.item$.pipe(take(1)), relationship$]).pipe(
diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service.spec.ts
index f8bc7ea886..7f0c7e2e35 100644
--- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service.spec.ts
+++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service.spec.ts
@@ -10,7 +10,7 @@ import {
} from '@ng-dynamic-forms/core';
import {
- mockInputWithTypeBindModel, MockRelationModel, mockDcTypeInputModel
+ mockInputWithTypeBindModel, MockRelationModel
} from '../../../mocks/form-models.mock';
import {DsDynamicTypeBindRelationService} from './ds-dynamic-type-bind-relation.service';
import {FormFieldMetadataValueObject} from '../models/form-field-metadata-value.model';
diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.scss b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.scss
index 1146e55750..8b13789179 100644
--- a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.scss
+++ b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.scss
@@ -1,3 +1 @@
-span.text-contents{
- padding: var(--bs-btn-padding-y) 0;
-}
+
diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-relation-list-element/existing-relation-list-element.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-relation-list-element/existing-relation-list-element.component.html
index ff91b18e1c..64ace2296e 100644
--- a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-relation-list-element/existing-relation-list-element.component.html
+++ b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-relation-list-element/existing-relation-list-element.component.html
@@ -1,12 +1,12 @@
-
+
-
+
diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-relation-list-element/existing-relation-list-element.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-relation-list-element/existing-relation-list-element.component.spec.ts
index d733f503bb..5a53599f91 100644
--- a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-relation-list-element/existing-relation-list-element.component.spec.ts
+++ b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-relation-list-element/existing-relation-list-element.component.spec.ts
@@ -69,7 +69,7 @@ describe('ExistingRelationListElementComponent', () => {
providers: [
{ provide: SelectableListService, useValue: selectionService },
{ provide: Store, useValue: store },
- { provide: SubmissionService, useClass: SubmissionServiceStub },
+ { provide: SubmissionService, useClass: SubmissionServiceStub }
],
schemas: [NO_ERRORS_SCHEMA]
})
diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-concat.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-concat.model.ts
index 1c568dbd32..1d6037a409 100644
--- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-concat.model.ts
+++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-concat.model.ts
@@ -1,4 +1,10 @@
-import { DynamicFormControlLayout, DynamicFormGroupModel, DynamicFormGroupModelConfig, serializable } from '@ng-dynamic-forms/core';
+import {
+ DynamicFormControlLayout,
+ DynamicFormControlRelation,
+ DynamicFormGroupModel,
+ DynamicFormGroupModelConfig,
+ serializable
+} from '@ng-dynamic-forms/core';
import { Subject } from 'rxjs';
@@ -16,6 +22,7 @@ export interface DynamicConcatModelConfig extends DynamicFormGroupModelConfig {
separator: string;
value?: any;
hint?: string;
+ typeBindRelations?: DynamicFormControlRelation[];
relationship?: RelationshipOptions;
repeatable: boolean;
required: boolean;
@@ -29,6 +36,8 @@ export class DynamicConcatModel extends DynamicFormGroupModel {
@serializable() separator: string;
@serializable() hasLanguages = false;
+ @serializable() typeBindRelations: DynamicFormControlRelation[];
+ @serializable() typeBindHidden = false;
@serializable() relationship?: RelationshipOptions;
@serializable() repeatable?: boolean;
@serializable() required?: boolean;
@@ -55,6 +64,7 @@ export class DynamicConcatModel extends DynamicFormGroupModel {
this.metadataValue = config.metadataValue;
this.valueUpdates = new Subject();
this.valueUpdates.subscribe((value: string) => this.value = value);
+ this.typeBindRelations = config.typeBindRelations ? config.typeBindRelations : [];
}
get value() {
diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list-checkbox-group.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list-checkbox-group.model.ts
index d4ddd0c3c8..7cffdfe801 100644
--- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list-checkbox-group.model.ts
+++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list-checkbox-group.model.ts
@@ -2,6 +2,7 @@ import { Subject } from 'rxjs';
import {
DynamicCheckboxGroupModel,
DynamicFormControlLayout,
+ DynamicFormControlRelation,
DynamicFormGroupModelConfig,
serializable
} from '@ng-dynamic-forms/core';
@@ -15,6 +16,7 @@ export interface DynamicListCheckboxGroupModelConfig extends DynamicFormGroupMod
groupLength?: number;
repeatable: boolean;
value?: any;
+ typeBindRelations?: DynamicFormControlRelation[];
}
export class DynamicListCheckboxGroupModel extends DynamicCheckboxGroupModel {
@@ -23,6 +25,7 @@ export class DynamicListCheckboxGroupModel extends DynamicCheckboxGroupModel {
@serializable() repeatable: boolean;
@serializable() groupLength: number;
@serializable() _value: VocabularyEntry[];
+ @serializable() typeBindRelations: DynamicFormControlRelation[];
isListGroup = true;
valueUpdates: Subject;
@@ -37,6 +40,7 @@ export class DynamicListCheckboxGroupModel extends DynamicCheckboxGroupModel {
this.valueUpdates = new Subject();
this.valueUpdates.subscribe((value: VocabularyEntry | VocabularyEntry[]) => this.value = value);
this.valueUpdates.next(config.value);
+ this.typeBindRelations = config.typeBindRelations ? config.typeBindRelations : [];
}
get hasAuthority(): boolean {
diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.ts
index bebef5860e..d44d24f8b8 100644
--- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.ts
+++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.ts
@@ -7,7 +7,7 @@ import {
DynamicFormLayoutService,
DynamicFormValidationService
} from '@ng-dynamic-forms/core';
-import { findKey } from 'lodash';
+import findKey from 'lodash/findKey';
import { hasValue, isNotEmpty } from '../../../../../empty.util';
import { DynamicListCheckboxGroupModel } from './dynamic-list-checkbox-group.model';
diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.spec.ts
index dd97bb74ab..27029ff2be 100644
--- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.spec.ts
+++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.spec.ts
@@ -20,7 +20,7 @@ import { FormFieldMetadataValueObject } from '../../../models/form-field-metadat
import { VocabularyEntry } from '../../../../../../core/submission/vocabularies/models/vocabulary-entry.model';
import { createTestComponent } from '../../../../../testing/utils.test';
import { DynamicLookupNameModel } from './dynamic-lookup-name.model';
-import { AuthorityConfidenceStateDirective } from '../../../../../authority-confidence/authority-confidence-state.directive';
+import { AuthorityConfidenceStateDirective } from '../../../../directives/authority-confidence-state.directive';
import { ObjNgFor } from '../../../../../utils/object-ngfor.pipe';
import {
mockDynamicFormLayoutService,
diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.spec.ts
index 1102ae2e74..cf417145a7 100644
--- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.spec.ts
+++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.spec.ts
@@ -21,11 +21,11 @@ import { DsDynamicOneboxComponent } from './dynamic-onebox.component';
import { DynamicOneboxModel } from './dynamic-onebox.model';
import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model';
import { createTestComponent } from '../../../../../testing/utils.test';
-import { AuthorityConfidenceStateDirective } from '../../../../../authority-confidence/authority-confidence-state.directive';
+import { AuthorityConfidenceStateDirective } from '../../../../directives/authority-confidence-state.directive';
import { ObjNgFor } from '../../../../../utils/object-ngfor.pipe';
import { VocabularyEntry } from '../../../../../../core/submission/vocabularies/models/vocabulary-entry.model';
import { createSuccessfulRemoteDataObject$ } from '../../../../../remote-data.utils';
-import { VocabularyTreeviewComponent } from '../../../../../vocabulary-treeview/vocabulary-treeview.component';
+import { VocabularyTreeviewComponent } from '../../../../vocabulary-treeview/vocabulary-treeview.component';
import {
mockDynamicFormLayoutService,
mockDynamicFormValidationService
diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.ts
index 9d2799177c..008328bf73 100644
--- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.ts
+++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.ts
@@ -30,7 +30,7 @@ import { VocabularyEntry } from '../../../../../../core/submission/vocabularies/
import { PageInfo } from '../../../../../../core/shared/page-info.model';
import { DsDynamicVocabularyComponent } from '../dynamic-vocabulary.component';
import { Vocabulary } from '../../../../../../core/submission/vocabularies/models/vocabulary.model';
-import { VocabularyTreeviewComponent } from '../../../../../vocabulary-treeview/vocabulary-treeview.component';
+import { VocabularyTreeviewComponent } from '../../../../vocabulary-treeview/vocabulary-treeview.component';
import { VocabularyEntryDetail } from '../../../../../../core/submission/vocabularies/models/vocabulary-entry-detail.model';
/**
diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.component.spec.ts
index 83baeaaeaa..733758fd27 100644
--- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.component.spec.ts
+++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.component.spec.ts
@@ -16,7 +16,7 @@ import { FormFieldModel } from '../../../models/form-field.model';
import { FormBuilderService } from '../../../form-builder.service';
import { FormService } from '../../../../form.service';
import { FormComponent } from '../../../../form.component';
-import { Chips } from '../../../../../chips/models/chips.model';
+import { Chips } from '../../../../chips/models/chips.model';
import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model';
import { DsDynamicInputModel } from '../ds-dynamic-input.model';
import { createTestComponent } from '../../../../../testing/utils.test';
diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.components.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.components.ts
index cd10b9a4a3..fd111e44c2 100644
--- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.components.ts
+++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.components.ts
@@ -11,17 +11,18 @@ import {
DynamicFormValidationService,
DynamicInputModel
} from '@ng-dynamic-forms/core';
-import { isEqual, isObject } from 'lodash';
+import isEqual from 'lodash/isEqual';
+import isObject from 'lodash/isObject';
import { DynamicRelationGroupModel } from './dynamic-relation-group.model';
import { FormBuilderService } from '../../../form-builder.service';
import { SubmissionFormsModel } from '../../../../../../core/config/models/config-submission-forms.model';
import { FormService } from '../../../../form.service';
import { FormComponent } from '../../../../form.component';
-import { Chips } from '../../../../../chips/models/chips.model';
+import { Chips } from '../../../../chips/models/chips.model';
import { hasValue, isEmpty, isNotEmpty, isNotNull } from '../../../../../empty.util';
import { shrinkInOut } from '../../../../../animations/shrink';
-import { ChipsItem } from '../../../../../chips/models/chips-item.model';
+import { ChipsItem } from '../../../../chips/models/chips-item.model';
import { hasOnlyEmptyProperties } from '../../../../../object.util';
import { VocabularyService } from '../../../../../../core/submission/vocabularies/vocabulary.service';
import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model';
diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.component.spec.ts
index 7b2c416699..162d9c3cec 100644
--- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.component.spec.ts
+++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.component.spec.ts
@@ -13,7 +13,7 @@ import { VocabularyService } from '../../../../../../core/submission/vocabularie
import { VocabularyServiceStub } from '../../../../../testing/vocabulary-service.stub';
import { DsDynamicTagComponent } from './dynamic-tag.component';
import { DynamicTagModel } from './dynamic-tag.model';
-import { Chips } from '../../../../../chips/models/chips.model';
+import { Chips } from '../../../../chips/models/chips.model';
import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model';
import { VocabularyEntry } from '../../../../../../core/submission/vocabularies/models/vocabulary-entry.model';
import { createTestComponent } from '../../../../../testing/utils.test';
diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.component.ts
index ef5e84e501..1c015be747 100644
--- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.component.ts
+++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.component.ts
@@ -5,11 +5,11 @@ import { DynamicFormLayoutService, DynamicFormValidationService } from '@ng-dyna
import { Observable, of as observableOf } from 'rxjs';
import { catchError, debounceTime, distinctUntilChanged, map, merge, switchMap, tap } from 'rxjs/operators';
import { NgbTypeahead, NgbTypeaheadSelectItemEvent } from '@ng-bootstrap/ng-bootstrap';
-import { isEqual } from 'lodash';
+import isEqual from 'lodash/isEqual';
import { VocabularyService } from '../../../../../../core/submission/vocabularies/vocabulary.service';
import { DynamicTagModel } from './dynamic-tag.model';
-import { Chips } from '../../../../../chips/models/chips.model';
+import { Chips } from '../../../../chips/models/chips.model';
import { hasValue, isNotEmpty } from '../../../../../empty.util';
import { environment } from '../../../../../../../environments/environment';
import { getFirstSucceededRemoteDataPayload } from '../../../../../../core/shared/operators';
diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.spec.ts
index 3c9b575027..cfffc5ddb7 100644
--- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.spec.ts
+++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.spec.ts
@@ -1,4 +1,6 @@
-import { DsDynamicLookupRelationExternalSourceTabComponent } from './dynamic-lookup-relation-external-source-tab.component';
+import {
+ DsDynamicLookupRelationExternalSourceTabComponent
+} from './dynamic-lookup-relation-external-source-tab.component';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { VarDirective } from '../../../../../utils/var.directive';
import { TranslateModule } from '@ngx-translate/core';
@@ -6,7 +8,7 @@ import { RouterTestingModule } from '@angular/router/testing';
import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core';
import { PaginatedSearchOptions } from '../../../../../search/models/paginated-search-options.model';
import { SearchConfigurationService } from '../../../../../../core/shared/search/search-configuration.service';
-import { of as observableOf } from 'rxjs';
+import { of as observableOf, EMPTY } from 'rxjs';
import {
createFailedRemoteDataObject$,
createPendingRemoteDataObject$,
@@ -22,11 +24,13 @@ import { SelectableListService } from '../../../../../object-list/selectable-lis
import { Item } from '../../../../../../core/shared/item.model';
import { Collection } from '../../../../../../core/shared/collection.model';
import { RelationshipOptions } from '../../../models/relationship-options.model';
-import { ExternalSourceEntryImportModalComponent } from './external-source-entry-import-modal/external-source-entry-import-modal.component';
import { createPaginatedList } from '../../../../../testing/utils.test';
import { PaginationService } from '../../../../../../core/pagination/pagination.service';
import { PaginationServiceStub } from '../../../../../testing/pagination-service.stub';
import { ItemType } from '../../../../../../core/shared/item-relationships/item-type.model';
+import {
+ ThemedExternalSourceEntryImportModalComponent
+} from './external-source-entry-import-modal/themed-external-source-entry-import-modal.component';
describe('DsDynamicLookupRelationExternalSourceTabComponent', () => {
let component: DsDynamicLookupRelationExternalSourceTabComponent;
@@ -187,12 +191,13 @@ describe('DsDynamicLookupRelationExternalSourceTabComponent', () => {
describe('import', () => {
beforeEach(() => {
- spyOn(modalService, 'open').and.returnValue(Object.assign({ componentInstance: Object.assign({ importedObject: new EventEmitter() }) }));
+ spyOn(modalService, 'open').and.returnValue(Object.assign({ componentInstance: Object.assign({ importedObject: new EventEmitter(), compRef$: EMPTY }) }));
+ component.modalRef = modalService.open(ThemedExternalSourceEntryImportModalComponent, { size: 'lg', container: 'ds-dynamic-lookup-relation-modal' });
component.import(externalEntries[0]);
});
it('should open a new ExternalSourceEntryImportModalComponent', () => {
- expect(modalService.open).toHaveBeenCalledWith(ExternalSourceEntryImportModalComponent, jasmine.any(Object));
+ expect(modalService.open).toHaveBeenCalledWith(ThemedExternalSourceEntryImportModalComponent, jasmine.any(Object));
});
});
});
diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.ts
index e5ea98e537..22fcc4e8bb 100644
--- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.ts
+++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.ts
@@ -1,4 +1,4 @@
-import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
+import { Component, EventEmitter, Input, OnDestroy, OnInit, Output, ComponentRef } from '@angular/core';
import { SEARCH_CONFIG_SERVICE } from '../../../../../../my-dspace-page/my-dspace-page.component';
import { SearchConfigurationService } from '../../../../../../core/shared/search/search-configuration.service';
import { Router } from '@angular/router';
@@ -16,7 +16,8 @@ import { PaginationComponentOptions } from '../../../../../pagination/pagination
import { RelationshipOptions } from '../../../models/relationship-options.model';
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
import { ExternalSourceEntryImportModalComponent } from './external-source-entry-import-modal/external-source-entry-import-modal.component';
-import { hasValue } from '../../../../../empty.util';
+import { ThemedExternalSourceEntryImportModalComponent } from './external-source-entry-import-modal/themed-external-source-entry-import-modal.component';
+import { hasValue, hasValueOperator } from '../../../../../empty.util';
import { SelectableListService } from '../../../../../object-list/selectable-list/selectable-list.service';
import { Item } from '../../../../../../core/shared/item.model';
import { Collection } from '../../../../../../core/shared/collection.model';
@@ -114,9 +115,9 @@ export class DsDynamicLookupRelationExternalSourceTabComponent implements OnInit
modalRef: NgbModalRef;
/**
- * Subscription to the modal's importedObject event-emitter
+ * Array to track all subscriptions and unsubscribe them onDestroy
*/
- importObjectSub: Subscription;
+ protected subs: Subscription[] = [];
/**
* The entity types compatible with the given external source
@@ -161,30 +162,40 @@ export class DsDynamicLookupRelationExternalSourceTabComponent implements OnInit
* @param entry The entry to import
*/
import(entry) {
- this.modalRef = this.modalService.open(ExternalSourceEntryImportModalComponent, {
+ this.modalRef = this.modalService.open(ThemedExternalSourceEntryImportModalComponent, {
size: 'lg',
container: 'ds-dynamic-lookup-relation-modal'
});
- const modalComp = this.modalRef.componentInstance;
- modalComp.externalSourceEntry = entry;
- modalComp.item = this.item;
- modalComp.collection = this.collection;
- modalComp.relationship = this.relationship;
- modalComp.label = this.label;
- modalComp.relatedEntityType = this.relatedEntityType;
- this.importObjectSub = modalComp.importedObject.subscribe((object) => {
+
+ const modalComp$ = this.modalRef.componentInstance.compRef$.pipe(
+ hasValueOperator(),
+ map((compRef: ComponentRef) => compRef.instance)
+ );
+
+ this.subs.push(modalComp$.subscribe((modalComp: ExternalSourceEntryImportModalComponent) => {
+ modalComp.externalSourceEntry = entry;
+ modalComp.item = this.item;
+ // modalComp.collection = this.collection;
+ modalComp.relationship = this.relationship;
+ modalComp.label = this.label;
+ modalComp.relatedEntityType = this.relatedEntityType;
+ }));
+
+ this.subs.push(modalComp$.pipe(
+ switchMap((modalComp: ExternalSourceEntryImportModalComponent) => modalComp.importedObject)
+ ).subscribe((object) => {
this.selectableListService.selectSingle(this.listId, object);
this.importedObject.emit(object);
- });
+ }));
}
/**
* Unsubscribe from open subscriptions
*/
ngOnDestroy(): void {
- if (hasValue(this.importObjectSub)) {
- this.importObjectSub.unsubscribe();
- }
+ this.subs
+ .filter((sub) => hasValue(sub))
+ .forEach((sub) => sub.unsubscribe());
}
/**
diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/themed-external-source-entry-import-modal.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/themed-external-source-entry-import-modal.component.ts
new file mode 100644
index 0000000000..26e6097c2d
--- /dev/null
+++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/themed-external-source-entry-import-modal.component.ts
@@ -0,0 +1,22 @@
+import { ExternalSourceEntryImportModalComponent } from './external-source-entry-import-modal.component';
+import { ThemedComponent } from '../../../../../../theme-support/themed.component';
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'ds-themed-external-source-entry-import-modal',
+ styleUrls: [],
+ templateUrl: '../../../../../../../shared/theme-support/themed.component.html',
+})
+export class ThemedExternalSourceEntryImportModalComponent extends ThemedComponent {
+ protected getComponentName(): string {
+ return 'ExternalSourceEntryImportModalComponent';
+ }
+
+ protected importThemedComponent(themeName: string): Promise {
+ return import(`../../../../../../../../themes/${themeName}/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component`);
+ }
+
+ protected importUnthemedComponent(): Promise {
+ return import(`./external-source-entry-import-modal.component`);
+ }
+}
diff --git a/src/app/shared/form/builder/form-builder.service.ts b/src/app/shared/form/builder/form-builder.service.ts
index a281942bc7..b3a78d4669 100644
--- a/src/app/shared/form/builder/form-builder.service.ts
+++ b/src/app/shared/form/builder/form-builder.service.ts
@@ -18,7 +18,9 @@ import {
DynamicPathable,
parseReviver,
} from '@ng-dynamic-forms/core';
-import { isObject, isString, mergeWith } from 'lodash';
+import isObject from 'lodash/isObject';
+import isString from 'lodash/isString';
+import mergeWith from 'lodash/mergeWith';
import {
hasNoValue,
diff --git a/src/app/shared/form/builder/models/form-field-previous-value-object.ts b/src/app/shared/form/builder/models/form-field-previous-value-object.ts
index ca4a47c089..2aa40f97ae 100644
--- a/src/app/shared/form/builder/models/form-field-previous-value-object.ts
+++ b/src/app/shared/form/builder/models/form-field-previous-value-object.ts
@@ -1,4 +1,4 @@
-import { isEqual } from 'lodash';
+import isEqual from 'lodash/isEqual';
export class FormFieldPreviousValueObject {
diff --git a/src/app/shared/form/builder/parsers/field-parser.ts b/src/app/shared/form/builder/parsers/field-parser.ts
index bd6820d4b3..c64de0e3ff 100644
--- a/src/app/shared/form/builder/parsers/field-parser.ts
+++ b/src/app/shared/form/builder/parsers/field-parser.ts
@@ -1,7 +1,12 @@
-import {Inject, InjectionToken} from '@angular/core';
+import { Inject, InjectionToken } from '@angular/core';
-import { uniqueId } from 'lodash';
-import {DynamicFormControlLayout, DynamicFormControlRelation, MATCH_VISIBLE, OR_OPERATOR} from '@ng-dynamic-forms/core';
+import uniqueId from 'lodash/uniqueId';
+import {
+ DynamicFormControlLayout,
+ DynamicFormControlRelation,
+ MATCH_VISIBLE,
+ OR_OPERATOR
+} from '@ng-dynamic-forms/core';
import { hasValue, isNotEmpty, isNotNull, isNotUndefined } from '../../../empty.util';
import { FormFieldModel } from '../models/form-field.model';
@@ -22,6 +27,12 @@ export const SUBMISSION_ID: InjectionToken = new InjectionToken(
export const CONFIG_DATA: InjectionToken = new InjectionToken('configData');
export const INIT_FORM_VALUES: InjectionToken = new InjectionToken('initFormValues');
export const PARSER_OPTIONS: InjectionToken = new InjectionToken('parserOptions');
+/**
+ * This pattern checks that a regex field uses the common ECMAScript format: `/{pattern}/{flags}`, in which the flags
+ * are part of the regex, or a simpler one with only pattern `/{pattern}/` or `{pattern}`.
+ * The regex itself is encapsulated inside a `RegExp` object, that will validate the pattern syntax.
+ */
+export const REGEX_FIELD_VALIDATOR = new RegExp('(\\/?)(.+)\\1([gimsuy]*)', 'i');
export abstract class FieldParser {
@@ -43,7 +54,7 @@ export abstract class FieldParser {
public abstract modelFactory(fieldValue?: FormFieldMetadataValueObject, label?: boolean): any;
public parse() {
- if (((this.getInitValueCount() > 1 && !this.configData.repeatable) || (this.configData.repeatable))
+ if (((this.getInitValueCount() > 1 && !this.configData.repeatable) || (this.configData.repeatable))
&& (this.configData.input.type !== ParserType.List)
&& (this.configData.input.type !== ParserType.Tag)
) {
@@ -315,6 +326,7 @@ export abstract class FieldParser {
* fields in type bind, made up of a 'match' outcome (make this field visible), an 'operator'
* (OR) and a 'when' condition (the bindValues array).
* @param configuredTypeBindValues array of types from the submission definition (CONFIG_DATA)
+ * @param typeField
* @private
* @return DynamicFormControlRelation[] array with one relation in it, for type bind matching to show a field
*/
@@ -343,8 +355,21 @@ export abstract class FieldParser {
return hasValue(this.configData.input.regex);
}
+ /**
+ * Adds pattern validation to `controlModel`, it uses the encapsulated `configData` to test the regex,
+ * contained in the input config, against the common `ECMAScript` standard validator {@link REGEX_FIELD_VALIDATOR},
+ * and creates an equivalent `RegExp` object that will be used during form-validation against the user-input.
+ * @param controlModel
+ * @protected
+ */
protected addPatternValidator(controlModel) {
- const regex = new RegExp(this.configData.input.regex);
+ const validatorMatcher = this.configData.input.regex.match(REGEX_FIELD_VALIDATOR);
+ let regex;
+ if (validatorMatcher != null && validatorMatcher.length > 3) {
+ regex = new RegExp(validatorMatcher[2], validatorMatcher[3]);
+ } else {
+ regex = new RegExp(this.configData.input.regex);
+ }
controlModel.validators = Object.assign({}, controlModel.validators, { pattern: regex });
controlModel.errorMessages = Object.assign(
{},
diff --git a/src/app/shared/form/builder/parsers/onebox-field-parser.spec.ts b/src/app/shared/form/builder/parsers/onebox-field-parser.spec.ts
index e7e68a6461..a4c71d1f42 100644
--- a/src/app/shared/form/builder/parsers/onebox-field-parser.spec.ts
+++ b/src/app/shared/form/builder/parsers/onebox-field-parser.spec.ts
@@ -4,6 +4,7 @@ import { DynamicQualdropModel } from '../ds-dynamic-form-ui/models/ds-dynamic-qu
import { DynamicOneboxModel } from '../ds-dynamic-form-ui/models/onebox/dynamic-onebox.model';
import { DsDynamicInputModel } from '../ds-dynamic-form-ui/models/ds-dynamic-input.model';
import { ParserOptions } from './parser-options';
+import { FieldParser } from './field-parser';
describe('OneboxFieldParser test suite', () => {
let field1: FormFieldModel;
@@ -101,4 +102,51 @@ describe('OneboxFieldParser test suite', () => {
expect(fieldModel instanceof DynamicOneboxModel).toBe(true);
});
+ describe('should handle a DynamicOneboxModel with regex', () => {
+ let regexField: FormFieldModel;
+ let parser: FieldParser;
+ let fieldModel: any;
+
+ beforeEach(() => {
+ regexField = {
+ input: { type: 'onebox', regex: '/[a-z]+/mi' },
+ label: 'Title',
+ mandatory: 'false',
+ repeatable: false,
+ hints: 'Enter the name of the events, if any.',
+ selectableMetadata: [
+ {
+ metadata: 'title',
+ controlledVocabulary: 'EVENTAuthority',
+ closed: false
+ }
+ ],
+ languageCodes: []
+ } as FormFieldModel;
+
+ parser = new OneboxFieldParser(submissionId, regexField, initFormValues, parserOptions);
+ fieldModel = parser.parse();
+ });
+
+ it('should have initialized pattern validator', () => {
+ expect(fieldModel instanceof DynamicOneboxModel).toBe(true);
+ expect(fieldModel.validators).not.toBeNull();
+ expect(fieldModel.validators.pattern).not.toBeNull();
+ });
+
+ it('should mark valid not case sensitive basic characters regex in multiline', () => {
+ let pattern = fieldModel.validators.pattern as RegExp;
+ expect(pattern.test('HELLO')).toBe(true);
+ expect(pattern.test('hello')).toBe(true);
+ expect(pattern.test('hello\nhello\nhello')).toBe(true);
+ expect(pattern.test('HeLlO')).toBe(true);
+ });
+
+ it('should be invalid for non-basic alphabet characters', () => {
+ let pattern = fieldModel.validators.pattern as RegExp;
+ expect(pattern.test('12345')).toBe(false);
+ expect(pattern.test('àèìòùáéíóú')).toBe(false);
+ });
+ });
+
});
diff --git a/src/app/shared/form/builder/parsers/row-parser.ts b/src/app/shared/form/builder/parsers/row-parser.ts
index 764f52ffdf..2818e37b25 100644
--- a/src/app/shared/form/builder/parsers/row-parser.ts
+++ b/src/app/shared/form/builder/parsers/row-parser.ts
@@ -1,7 +1,7 @@
import { Injectable, Injector } from '@angular/core';
import { DYNAMIC_FORM_CONTROL_TYPE_ARRAY, DynamicFormGroupModelConfig } from '@ng-dynamic-forms/core';
-import { uniqueId } from 'lodash';
+import uniqueId from 'lodash/uniqueId';
import { isEmpty } from '../../../empty.util';
import { DynamicRowGroupModel } from '../ds-dynamic-form-ui/models/ds-dynamic-row-group-model';
diff --git a/src/app/shared/form/builder/parsers/textarea-field-parser.ts b/src/app/shared/form/builder/parsers/textarea-field-parser.ts
index 740e94721f..548ce567c3 100644
--- a/src/app/shared/form/builder/parsers/textarea-field-parser.ts
+++ b/src/app/shared/form/builder/parsers/textarea-field-parser.ts
@@ -5,6 +5,7 @@ import {
DsDynamicTextAreaModel,
DsDynamicTextAreaModelConfig
} from '../ds-dynamic-form-ui/models/ds-dynamic-textarea.model';
+import { environment } from '../../../../../environments/environment';
export class TextareaFieldParser extends FieldParser {
@@ -20,6 +21,7 @@ export class TextareaFieldParser extends FieldParser {
};
textAreaModelConfig.rows = 10;
+ textAreaModelConfig.spellCheck = environment.form.spellCheck;
this.setValues(textAreaModelConfig, fieldValue);
const textAreaModel = new DsDynamicTextAreaModel(textAreaModelConfig, layout);
diff --git a/src/app/shared/chips/chips.component.html b/src/app/shared/form/chips/chips.component.html
similarity index 100%
rename from src/app/shared/chips/chips.component.html
rename to src/app/shared/form/chips/chips.component.html
diff --git a/src/app/shared/chips/chips.component.scss b/src/app/shared/form/chips/chips.component.scss
similarity index 100%
rename from src/app/shared/chips/chips.component.scss
rename to src/app/shared/form/chips/chips.component.scss
diff --git a/src/app/shared/chips/chips.component.spec.ts b/src/app/shared/form/chips/chips.component.spec.ts
similarity index 92%
rename from src/app/shared/chips/chips.component.spec.ts
rename to src/app/shared/form/chips/chips.component.spec.ts
index 6f9b948002..2b8a469bd1 100644
--- a/src/app/shared/chips/chips.component.spec.ts
+++ b/src/app/shared/form/chips/chips.component.spec.ts
@@ -3,17 +3,16 @@ import { ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/c
import { ComponentFixture, fakeAsync, inject, TestBed, tick, waitForAsync, } from '@angular/core/testing';
import { Chips } from './models/chips.model';
-import { UploaderService } from '../uploader/uploader.service';
import { ChipsComponent } from './chips.component';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { By } from '@angular/platform-browser';
-import { FormFieldMetadataValueObject } from '../form/builder/models/form-field-metadata-value.model';
-import { createTestComponent } from '../testing/utils.test';
-import { AuthorityConfidenceStateDirective } from '../authority-confidence/authority-confidence-state.directive';
+import { FormFieldMetadataValueObject } from '../builder/models/form-field-metadata-value.model';
+import { createTestComponent } from '../../testing/utils.test';
+import { AuthorityConfidenceStateDirective } from '../directives/authority-confidence-state.directive';
import { TranslateModule } from '@ngx-translate/core';
-import { ConfidenceType } from '../../core/shared/confidence-type';
+import { ConfidenceType } from '../../../core/shared/confidence-type';
import { SortablejsModule } from 'ngx-sortablejs';
-import { environment } from '../../../environments/environment';
+import { environment } from '../../../../environments/environment';
describe('ChipsComponent test suite', () => {
@@ -41,7 +40,6 @@ describe('ChipsComponent test suite', () => {
providers: [
ChangeDetectorRef,
ChipsComponent,
- UploaderService
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
});
diff --git a/src/app/shared/chips/chips.component.ts b/src/app/shared/form/chips/chips.component.ts
similarity index 93%
rename from src/app/shared/chips/chips.component.ts
rename to src/app/shared/form/chips/chips.component.ts
index 17a6b034ee..5166657582 100644
--- a/src/app/shared/chips/chips.component.ts
+++ b/src/app/shared/form/chips/chips.component.ts
@@ -1,11 +1,11 @@
import { ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges, } from '@angular/core';
import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap';
-import { isObject } from 'lodash';
+import isObject from 'lodash/isObject';
import { Chips } from './models/chips.model';
import { ChipsItem } from './models/chips-item.model';
-import { UploaderService } from '../uploader/uploader.service';
+import { DragService } from '../../../core/drag.service';
import { TranslateService } from '@ngx-translate/core';
import { Options } from 'sortablejs';
import { BehaviorSubject } from 'rxjs';
@@ -33,7 +33,7 @@ export class ChipsComponent implements OnChanges {
constructor(
private cdr: ChangeDetectorRef,
- private uploaderService: UploaderService,
+ private dragService: DragService,
private translate: TranslateService) {
this.options = {
@@ -76,12 +76,12 @@ export class ChipsComponent implements OnChanges {
onDragStart(index) {
this.isDragging.next(true);
- this.uploaderService.overrideDragOverPage();
+ this.dragService.overrideDragOverPage();
this.dragged = index;
}
onDragEnd(event) {
- this.uploaderService.allowDragOverPage();
+ this.dragService.allowDragOverPage();
this.dragged = -1;
this.chips.updateOrder();
this.isDragging.next(false);
diff --git a/src/app/shared/chips/models/chips-item.model.spec.ts b/src/app/shared/form/chips/models/chips-item.model.spec.ts
similarity index 94%
rename from src/app/shared/chips/models/chips-item.model.spec.ts
rename to src/app/shared/form/chips/models/chips-item.model.spec.ts
index c85ff83c31..27b4840d46 100644
--- a/src/app/shared/chips/models/chips-item.model.spec.ts
+++ b/src/app/shared/form/chips/models/chips-item.model.spec.ts
@@ -1,5 +1,5 @@
import { ChipsItem, ChipsItemIcon } from './chips-item.model';
-import { FormFieldMetadataValueObject } from '../../form/builder/models/form-field-metadata-value.model';
+import { FormFieldMetadataValueObject } from '../../builder/models/form-field-metadata-value.model';
describe('ChipsItem model test suite', () => {
let item: ChipsItem;
diff --git a/src/app/shared/chips/models/chips-item.model.ts b/src/app/shared/form/chips/models/chips-item.model.ts
similarity index 87%
rename from src/app/shared/chips/models/chips-item.model.ts
rename to src/app/shared/form/chips/models/chips-item.model.ts
index 5d0ff20e2f..277e6477ce 100644
--- a/src/app/shared/chips/models/chips-item.model.ts
+++ b/src/app/shared/form/chips/models/chips-item.model.ts
@@ -1,8 +1,9 @@
-import { isObject, uniqueId } from 'lodash';
-import { hasValue, isNotEmpty } from '../../empty.util';
-import { FormFieldMetadataValueObject } from '../../form/builder/models/form-field-metadata-value.model';
-import { ConfidenceType } from '../../../core/shared/confidence-type';
-import { PLACEHOLDER_PARENT_METADATA } from '../../form/builder/ds-dynamic-form-ui/ds-dynamic-form-constants';
+import isObject from 'lodash/isObject';
+import uniqueId from 'lodash/uniqueId';
+import { hasValue, isNotEmpty } from '../../../empty.util';
+import { FormFieldMetadataValueObject } from '../../builder/models/form-field-metadata-value.model';
+import { ConfidenceType } from '../../../../core/shared/confidence-type';
+import { PLACEHOLDER_PARENT_METADATA } from '../../builder/ds-dynamic-form-ui/ds-dynamic-form-constants';
export interface ChipsItemIcon {
metadata: string;
diff --git a/src/app/shared/chips/models/chips.model.spec.ts b/src/app/shared/form/chips/models/chips.model.spec.ts
similarity index 96%
rename from src/app/shared/chips/models/chips.model.spec.ts
rename to src/app/shared/form/chips/models/chips.model.spec.ts
index a6cf5faf81..c86ff55c6f 100644
--- a/src/app/shared/chips/models/chips.model.spec.ts
+++ b/src/app/shared/form/chips/models/chips.model.spec.ts
@@ -1,6 +1,6 @@
import { Chips } from './chips.model';
import { ChipsItem } from './chips-item.model';
-import { FormFieldMetadataValueObject } from '../../form/builder/models/form-field-metadata-value.model';
+import { FormFieldMetadataValueObject } from '../../builder/models/form-field-metadata-value.model';
describe('Chips model test suite', () => {
let items: any[];
diff --git a/src/app/shared/chips/models/chips.model.ts b/src/app/shared/form/chips/models/chips.model.ts
similarity index 87%
rename from src/app/shared/chips/models/chips.model.ts
rename to src/app/shared/form/chips/models/chips.model.ts
index c15badb976..daeb363d82 100644
--- a/src/app/shared/chips/models/chips.model.ts
+++ b/src/app/shared/form/chips/models/chips.model.ts
@@ -1,11 +1,13 @@
-import { findIndex, isEqual, isObject } from 'lodash';
+import findIndex from 'lodash/findIndex';
+import isEqual from 'lodash/isEqual';
+import isObject from 'lodash/isObject';
import { BehaviorSubject } from 'rxjs';
import { ChipsItem, ChipsItemIcon } from './chips-item.model';
-import { hasValue, isNotEmpty } from '../../empty.util';
-import { MetadataIconConfig } from '../../../../config/submission-config.interface';
-import { FormFieldMetadataValueObject } from '../../form/builder/models/form-field-metadata-value.model';
-import { VocabularyEntry } from '../../../core/submission/vocabularies/models/vocabulary-entry.model';
-import { PLACEHOLDER_PARENT_METADATA } from '../../form/builder/ds-dynamic-form-ui/ds-dynamic-form-constants';
+import { hasValue, isNotEmpty } from '../../../empty.util';
+import { MetadataIconConfig } from '../../../../../config/submission-config.interface';
+import { FormFieldMetadataValueObject } from '../../builder/models/form-field-metadata-value.model';
+import { VocabularyEntry } from '../../../../core/submission/vocabularies/models/vocabulary-entry.model';
+import { PLACEHOLDER_PARENT_METADATA } from '../../builder/ds-dynamic-form-ui/ds-dynamic-form-constants';
export class Chips {
chipsItems: BehaviorSubject;
diff --git a/src/app/shared/authority-confidence/authority-confidence-state.directive.ts b/src/app/shared/form/directives/authority-confidence-state.directive.ts
similarity index 82%
rename from src/app/shared/authority-confidence/authority-confidence-state.directive.ts
rename to src/app/shared/form/directives/authority-confidence-state.directive.ts
index 8999a643e8..49eee5ae8f 100644
--- a/src/app/shared/authority-confidence/authority-confidence-state.directive.ts
+++ b/src/app/shared/form/directives/authority-confidence-state.directive.ts
@@ -1,3 +1,11 @@
+/**
+ * The contents of this file are subject to the license and copyright
+ * detailed in the LICENSE and NOTICE files at the root of the source
+ * tree and available online at
+ *
+ * http://www.dspace.org/license/
+ */
+
import {
AfterViewInit,
Directive,
@@ -11,15 +19,15 @@ import {
SimpleChanges
} from '@angular/core';
-import { findIndex } from 'lodash';
+import findIndex from 'lodash/findIndex';
-import { VocabularyEntry } from '../../core/submission/vocabularies/models/vocabulary-entry.model';
-import { FormFieldMetadataValueObject } from '../form/builder/models/form-field-metadata-value.model';
-import { ConfidenceType } from '../../core/shared/confidence-type';
-import { isNotEmpty, isNull } from '../empty.util';
-import { ConfidenceIconConfig } from '../../../config/submission-config.interface';
-import { environment } from '../../../environments/environment';
-import { VocabularyEntryDetail } from '../../core/submission/vocabularies/models/vocabulary-entry-detail.model';
+import { VocabularyEntry } from '../../../core/submission/vocabularies/models/vocabulary-entry.model';
+import { FormFieldMetadataValueObject } from '../builder/models/form-field-metadata-value.model';
+import { ConfidenceType } from '../../../core/shared/confidence-type';
+import { isNotEmpty, isNull } from '../../empty.util';
+import { ConfidenceIconConfig } from '../../../../config/submission-config.interface';
+import { environment } from '../../../../environments/environment';
+import { VocabularyEntryDetail } from '../../../core/submission/vocabularies/models/vocabulary-entry-detail.model';
/**
* Directive to add to the element a bootstrap utility class based on metadata confidence value
diff --git a/src/app/shared/form/form.component.ts b/src/app/shared/form/form.component.ts
index 7c16be7542..086b5e1fd8 100644
--- a/src/app/shared/form/form.component.ts
+++ b/src/app/shared/form/form.component.ts
@@ -11,7 +11,7 @@ import {
DynamicFormLayout,
} from '@ng-dynamic-forms/core';
import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
-import { findIndex } from 'lodash';
+import findIndex from 'lodash/findIndex';
import { FormBuilderService } from './builder/form-builder.service';
import { hasValue, isNotEmpty, isNotNull, isNull } from '../empty.util';
diff --git a/src/app/shared/form/form.module.ts b/src/app/shared/form/form.module.ts
index 62ab5bd647..c3062b4231 100644
--- a/src/app/shared/form/form.module.ts
+++ b/src/app/shared/form/form.module.ts
@@ -2,10 +2,7 @@ import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormComponent } from './form.component';
import { DsDynamicFormComponent } from './builder/ds-dynamic-form-ui/ds-dynamic-form.component';
-import {
- DsDynamicFormControlContainerComponent,
- dsDynamicFormControlMapFn
-} from './builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component';
+import { DsDynamicFormControlContainerComponent, dsDynamicFormControlMapFn } from './builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component';
import { DsDynamicListComponent } from './builder/ds-dynamic-form-ui/models/list/dynamic-list.component';
import { DsDynamicLookupComponent } from './builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component';
import { DsDynamicDisabledComponent } from './builder/ds-dynamic-form-ui/models/disabled/dynamic-disabled.component';
@@ -24,12 +21,23 @@ import { DsDynamicLookupRelationExternalSourceTabComponent } from './builder/ds-
import { SharedModule } from '../shared.module';
import { TranslateModule } from '@ngx-translate/core';
import { SearchModule } from '../search/search.module';
-import { DYNAMIC_FORM_CONTROL_MAP_FN, DynamicFormsCoreModule } from '@ng-dynamic-forms/core';
+import { DYNAMIC_FORM_CONTROL_MAP_FN, DYNAMIC_MATCHER_PROVIDERS, DynamicFormLayoutService, DynamicFormsCoreModule, DynamicFormService, DynamicFormValidationService } from '@ng-dynamic-forms/core';
import { ExistingMetadataListElementComponent } from './builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component';
import { ExistingRelationListElementComponent } from './builder/ds-dynamic-form-ui/existing-relation-list-element/existing-relation-list-element.component';
import { ExternalSourceEntryImportModalComponent } from './builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component';
import { CustomSwitchComponent } from './builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component';
import { DynamicFormsNGBootstrapUIModule } from '@ng-dynamic-forms/ui-ng-bootstrap';
+import { ChipsComponent } from './chips/chips.component';
+import { NumberPickerComponent } from './number-picker/number-picker.component';
+import { AuthorityConfidenceStateDirective } from './directives/authority-confidence-state.directive';
+import { SortablejsModule } from 'ngx-sortablejs';
+import { VocabularyTreeviewComponent } from './vocabulary-treeview/vocabulary-treeview.component';
+import { VocabularyTreeviewService } from './vocabulary-treeview/vocabulary-treeview.service';
+import { FormBuilderService } from './builder/form-builder.service';
+import { DsDynamicTypeBindRelationService } from './builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service';
+import { FormService } from './form.service';
+import { NgxMaskModule } from 'ngx-mask';
+import { ThemedExternalSourceEntryImportModalComponent } from './builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/themed-external-source-entry-import-modal.component';
const COMPONENTS = [
CustomSwitchComponent,
@@ -53,12 +61,21 @@ const COMPONENTS = [
ExistingMetadataListElementComponent,
ExistingRelationListElementComponent,
ExternalSourceEntryImportModalComponent,
- FormComponent
+ FormComponent,
+ ChipsComponent,
+ NumberPickerComponent,
+ VocabularyTreeviewComponent,
+ ThemedExternalSourceEntryImportModalComponent
+];
+
+const DIRECTIVES = [
+ AuthorityConfidenceStateDirective,
];
@NgModule({
declarations: [
- ...COMPONENTS
+ ...COMPONENTS,
+ ...DIRECTIVES,
],
imports: [
CommonModule,
@@ -66,16 +83,27 @@ const COMPONENTS = [
DynamicFormsNGBootstrapUIModule,
SearchModule,
SharedModule,
- TranslateModule
+ TranslateModule,
+ SortablejsModule,
+ NgxMaskModule.forRoot(),
],
exports: [
- ...COMPONENTS
+ ...COMPONENTS,
+ ...DIRECTIVES,
],
providers: [
{
provide: DYNAMIC_FORM_CONTROL_MAP_FN,
useValue: dsDynamicFormControlMapFn
- }
+ },
+ ...DYNAMIC_MATCHER_PROVIDERS,
+ VocabularyTreeviewService,
+ DynamicFormLayoutService,
+ DynamicFormService,
+ DynamicFormValidationService,
+ FormBuilderService,
+ DsDynamicTypeBindRelationService,
+ FormService,
]
})
export class FormModule {
diff --git a/src/app/shared/form/form.reducer.ts b/src/app/shared/form/form.reducer.ts
index 22094b2e5d..c1e9b12efa 100644
--- a/src/app/shared/form/form.reducer.ts
+++ b/src/app/shared/form/form.reducer.ts
@@ -11,7 +11,8 @@ import {
FormStatusChangeAction
} from './form.actions';
import { hasValue } from '../empty.util';
-import { isEqual, uniqWith } from 'lodash';
+import isEqual from 'lodash/isEqual';
+import uniqWith from 'lodash/uniqWith';
export interface FormError {
message: string;
diff --git a/src/app/shared/form/form.service.ts b/src/app/shared/form/form.service.ts
index c4d6003abd..2dbf78f565 100644
--- a/src/app/shared/form/form.service.ts
+++ b/src/app/shared/form/form.service.ts
@@ -9,7 +9,7 @@ import { formObjectFromIdSelector } from './selectors';
import { FormBuilderService } from './builder/form-builder.service';
import { DynamicFormControlEvent, DynamicFormControlModel, DynamicFormGroupModel } from '@ng-dynamic-forms/core';
import { isEmpty, isNotUndefined } from '../empty.util';
-import { uniqueId } from 'lodash';
+import uniqueId from 'lodash/uniqueId';
import {
FormAddError,
FormAddTouchedAction,
diff --git a/src/app/shared/number-picker/number-picker.component.html b/src/app/shared/form/number-picker/number-picker.component.html
similarity index 100%
rename from src/app/shared/number-picker/number-picker.component.html
rename to src/app/shared/form/number-picker/number-picker.component.html
diff --git a/src/app/shared/number-picker/number-picker.component.scss b/src/app/shared/form/number-picker/number-picker.component.scss
similarity index 100%
rename from src/app/shared/number-picker/number-picker.component.scss
rename to src/app/shared/form/number-picker/number-picker.component.scss
diff --git a/src/app/shared/number-picker/number-picker.component.spec.ts b/src/app/shared/form/number-picker/number-picker.component.spec.ts
similarity index 96%
rename from src/app/shared/number-picker/number-picker.component.spec.ts
rename to src/app/shared/form/number-picker/number-picker.component.spec.ts
index 0cc073644e..d4484dbfa3 100644
--- a/src/app/shared/number-picker/number-picker.component.spec.ts
+++ b/src/app/shared/form/number-picker/number-picker.component.spec.ts
@@ -2,12 +2,11 @@
import { ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { ComponentFixture, inject, TestBed, waitForAsync, } from '@angular/core/testing';
-import { UploaderService } from '../uploader/uploader.service';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { By } from '@angular/platform-browser';
import { NumberPickerComponent } from './number-picker.component';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
-import { createTestComponent } from '../testing/utils.test';
+import { createTestComponent } from '../../testing/utils.test';
describe('NumberPickerComponent test suite', () => {
@@ -33,7 +32,6 @@ describe('NumberPickerComponent test suite', () => {
providers: [
ChangeDetectorRef,
NumberPickerComponent,
- UploaderService
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
});
diff --git a/src/app/shared/number-picker/number-picker.component.ts b/src/app/shared/form/number-picker/number-picker.component.ts
similarity index 98%
rename from src/app/shared/number-picker/number-picker.component.ts
rename to src/app/shared/form/number-picker/number-picker.component.ts
index 465e905713..0df1e050cd 100644
--- a/src/app/shared/number-picker/number-picker.component.ts
+++ b/src/app/shared/form/number-picker/number-picker.component.ts
@@ -1,6 +1,6 @@
import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output, SimpleChanges, } from '@angular/core';
import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR } from '@angular/forms';
-import { isEmpty } from '../empty.util';
+import { isEmpty } from '../../empty.util';
@Component({
selector: 'ds-number-picker',
diff --git a/src/app/shared/vocabulary-treeview/vocabulary-tree-flat-data-source.ts b/src/app/shared/form/vocabulary-treeview/vocabulary-tree-flat-data-source.ts
similarity index 100%
rename from src/app/shared/vocabulary-treeview/vocabulary-tree-flat-data-source.ts
rename to src/app/shared/form/vocabulary-treeview/vocabulary-tree-flat-data-source.ts
diff --git a/src/app/shared/vocabulary-treeview/vocabulary-tree-flattener.ts b/src/app/shared/form/vocabulary-treeview/vocabulary-tree-flattener.ts
similarity index 100%
rename from src/app/shared/vocabulary-treeview/vocabulary-tree-flattener.ts
rename to src/app/shared/form/vocabulary-treeview/vocabulary-tree-flattener.ts
diff --git a/src/app/shared/vocabulary-treeview/vocabulary-treeview-node.model.ts b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview-node.model.ts
similarity index 88%
rename from src/app/shared/vocabulary-treeview/vocabulary-treeview-node.model.ts
rename to src/app/shared/form/vocabulary-treeview/vocabulary-treeview-node.model.ts
index 63039e6c75..c167328cab 100644
--- a/src/app/shared/vocabulary-treeview/vocabulary-treeview-node.model.ts
+++ b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview-node.model.ts
@@ -1,7 +1,7 @@
/* eslint-disable max-classes-per-file */
import { BehaviorSubject } from 'rxjs';
-import { VocabularyEntryDetail } from '../../core/submission/vocabularies/models/vocabulary-entry-detail.model';
-import { PageInfo } from '../../core/shared/page-info.model';
+import { VocabularyEntryDetail } from '../../../core/submission/vocabularies/models/vocabulary-entry-detail.model';
+import { PageInfo } from '../../../core/shared/page-info.model';
export const LOAD_MORE = 'LOAD_MORE';
export const LOAD_MORE_ROOT = 'LOAD_MORE_ROOT';
diff --git a/src/app/shared/vocabulary-treeview/vocabulary-treeview.component.html b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.html
similarity index 100%
rename from src/app/shared/vocabulary-treeview/vocabulary-treeview.component.html
rename to src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.html
diff --git a/src/app/shared/vocabulary-treeview/vocabulary-treeview.component.scss b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.scss
similarity index 100%
rename from src/app/shared/vocabulary-treeview/vocabulary-treeview.component.scss
rename to src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.scss
diff --git a/src/app/shared/vocabulary-treeview/vocabulary-treeview.component.spec.ts b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.spec.ts
similarity index 91%
rename from src/app/shared/vocabulary-treeview/vocabulary-treeview.component.spec.ts
rename to src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.spec.ts
index 1bdf4b9df7..cf8fbd8c49 100644
--- a/src/app/shared/vocabulary-treeview/vocabulary-treeview.component.spec.ts
+++ b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.spec.ts
@@ -8,18 +8,18 @@ import { TranslateModule } from '@ngx-translate/core';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { provideMockStore } from '@ngrx/store/testing';
-import { createTestComponent } from '../testing/utils.test';
+import { createTestComponent } from '../../testing/utils.test';
import { VocabularyTreeviewComponent } from './vocabulary-treeview.component';
import { VocabularyTreeviewService } from './vocabulary-treeview.service';
-import { VocabularyEntryDetail } from '../../core/submission/vocabularies/models/vocabulary-entry-detail.model';
+import { VocabularyEntryDetail } from '../../../core/submission/vocabularies/models/vocabulary-entry-detail.model';
import { TreeviewFlatNode } from './vocabulary-treeview-node.model';
-import { FormFieldMetadataValueObject } from '../form/builder/models/form-field-metadata-value.model';
-import { VocabularyOptions } from '../../core/submission/vocabularies/models/vocabulary-options.model';
-import { PageInfo } from '../../core/shared/page-info.model';
-import { VocabularyEntry } from '../../core/submission/vocabularies/models/vocabulary-entry.model';
-import { AuthTokenInfo } from '../../core/auth/models/auth-token-info.model';
-import { authReducer } from '../../core/auth/auth.reducer';
-import { storeModuleConfig } from '../../app.reducer';
+import { FormFieldMetadataValueObject } from '../builder/models/form-field-metadata-value.model';
+import { VocabularyOptions } from '../../../core/submission/vocabularies/models/vocabulary-options.model';
+import { PageInfo } from '../../../core/shared/page-info.model';
+import { VocabularyEntry } from '../../../core/submission/vocabularies/models/vocabulary-entry.model';
+import { AuthTokenInfo } from '../../../core/auth/models/auth-token-info.model';
+import { authReducer } from '../../../core/auth/auth.reducer';
+import { storeModuleConfig } from '../../../app.reducer';
describe('VocabularyTreeviewComponent test suite', () => {
diff --git a/src/app/shared/vocabulary-treeview/vocabulary-treeview.component.ts b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.ts
similarity index 93%
rename from src/app/shared/vocabulary-treeview/vocabulary-treeview.component.ts
rename to src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.ts
index 9850947f60..408d656f42 100644
--- a/src/app/shared/vocabulary-treeview/vocabulary-treeview.component.ts
+++ b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.ts
@@ -7,17 +7,17 @@ import { Observable, Subscription } from 'rxjs';
import { select, Store } from '@ngrx/store';
import { TranslateService } from '@ngx-translate/core';
-import { VocabularyEntryDetail } from '../../core/submission/vocabularies/models/vocabulary-entry-detail.model';
-import { hasValue, isEmpty, isNotEmpty } from '../empty.util';
-import { isAuthenticated } from '../../core/auth/selectors';
+import { VocabularyEntryDetail } from '../../../core/submission/vocabularies/models/vocabulary-entry-detail.model';
+import { hasValue, isEmpty, isNotEmpty } from '../../empty.util';
+import { isAuthenticated } from '../../../core/auth/selectors';
import { VocabularyTreeviewService } from './vocabulary-treeview.service';
import { LOAD_MORE, LOAD_MORE_ROOT, TreeviewFlatNode, TreeviewNode } from './vocabulary-treeview-node.model';
-import { VocabularyOptions } from '../../core/submission/vocabularies/models/vocabulary-options.model';
-import { PageInfo } from '../../core/shared/page-info.model';
-import { VocabularyEntry } from '../../core/submission/vocabularies/models/vocabulary-entry.model';
+import { VocabularyOptions } from '../../../core/submission/vocabularies/models/vocabulary-options.model';
+import { PageInfo } from '../../../core/shared/page-info.model';
+import { VocabularyEntry } from '../../../core/submission/vocabularies/models/vocabulary-entry.model';
import { VocabularyTreeFlattener } from './vocabulary-tree-flattener';
import { VocabularyTreeFlatDataSource } from './vocabulary-tree-flat-data-source';
-import { CoreState } from '../../core/core-state.model';
+import { CoreState } from '../../../core/core-state.model';
/**
* Component that show a hierarchical vocabulary in a tree view
diff --git a/src/app/shared/vocabulary-treeview/vocabulary-treeview.service.spec.ts b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.service.spec.ts
similarity index 94%
rename from src/app/shared/vocabulary-treeview/vocabulary-treeview.service.spec.ts
rename to src/app/shared/form/vocabulary-treeview/vocabulary-treeview.service.spec.ts
index c1c64c80bd..752ef10fee 100644
--- a/src/app/shared/vocabulary-treeview/vocabulary-treeview.service.spec.ts
+++ b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.service.spec.ts
@@ -5,15 +5,15 @@ import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-transla
import { cold, getTestScheduler, hot } from 'jasmine-marbles';
import { VocabularyTreeviewService } from './vocabulary-treeview.service';
-import { VocabularyService } from '../../core/submission/vocabularies/vocabulary.service';
-import { TranslateLoaderMock } from '../mocks/translate-loader.mock';
-import { VocabularyOptions } from '../../core/submission/vocabularies/models/vocabulary-options.model';
+import { VocabularyService } from '../../../core/submission/vocabularies/vocabulary.service';
+import { TranslateLoaderMock } from '../../mocks/translate-loader.mock';
+import { VocabularyOptions } from '../../../core/submission/vocabularies/models/vocabulary-options.model';
import { LOAD_MORE_NODE, LOAD_MORE_ROOT_NODE, TreeviewFlatNode, TreeviewNode } from './vocabulary-treeview-node.model';
-import { PageInfo } from '../../core/shared/page-info.model';
-import { VocabularyEntryDetail } from '../../core/submission/vocabularies/models/vocabulary-entry-detail.model';
-import { buildPaginatedList } from '../../core/data/paginated-list.model';
-import { createSuccessfulRemoteDataObject } from '../remote-data.utils';
-import { VocabularyEntry } from '../../core/submission/vocabularies/models/vocabulary-entry.model';
+import { PageInfo } from '../../../core/shared/page-info.model';
+import { VocabularyEntryDetail } from '../../../core/submission/vocabularies/models/vocabulary-entry-detail.model';
+import { buildPaginatedList } from '../../../core/data/paginated-list.model';
+import { createSuccessfulRemoteDataObject } from '../../remote-data.utils';
+import { VocabularyEntry } from '../../../core/submission/vocabularies/models/vocabulary-entry.model';
import { expand, map, switchMap } from 'rxjs/operators';
import { from as observableFrom } from 'rxjs';
diff --git a/src/app/shared/vocabulary-treeview/vocabulary-treeview.service.ts b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.service.ts
similarity index 94%
rename from src/app/shared/vocabulary-treeview/vocabulary-treeview.service.ts
rename to src/app/shared/form/vocabulary-treeview/vocabulary-treeview.service.ts
index ad2525bb7f..8804716927 100644
--- a/src/app/shared/vocabulary-treeview/vocabulary-treeview.service.ts
+++ b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.service.ts
@@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable, of as observableOf } from 'rxjs';
import { map, merge, mergeMap, scan } from 'rxjs/operators';
-import { findIndex } from 'lodash';
+import findIndex from 'lodash/findIndex';
import {
LOAD_MORE_NODE,
@@ -10,17 +10,17 @@ import {
TreeviewFlatNode,
TreeviewNode
} from './vocabulary-treeview-node.model';
-import { VocabularyEntry } from '../../core/submission/vocabularies/models/vocabulary-entry.model';
-import { VocabularyService } from '../../core/submission/vocabularies/vocabulary.service';
-import { PageInfo } from '../../core/shared/page-info.model';
-import { isEmpty, isNotEmpty } from '../empty.util';
-import { VocabularyOptions } from '../../core/submission/vocabularies/models/vocabulary-options.model';
+import { VocabularyEntry } from '../../../core/submission/vocabularies/models/vocabulary-entry.model';
+import { VocabularyService } from '../../../core/submission/vocabularies/vocabulary.service';
+import { PageInfo } from '../../../core/shared/page-info.model';
+import { isEmpty, isNotEmpty } from '../../empty.util';
+import { VocabularyOptions } from '../../../core/submission/vocabularies/models/vocabulary-options.model';
import {
getFirstSucceededRemoteDataPayload,
getFirstSucceededRemoteListPayload
-} from '../../core/shared/operators';
-import { PaginatedList } from '../../core/data/paginated-list.model';
-import { VocabularyEntryDetail } from '../../core/submission/vocabularies/models/vocabulary-entry-detail.model';
+} from '../../../core/shared/operators';
+import { PaginatedList } from '../../../core/data/paginated-list.model';
+import { VocabularyEntryDetail } from '../../../core/submission/vocabularies/models/vocabulary-entry-detail.model';
/**
* A service that provides methods to deal with vocabulary tree
diff --git a/src/app/shared/host-window.reducer.spec.ts b/src/app/shared/host-window.reducer.spec.ts
index f0e7aa7245..f580c0e1da 100644
--- a/src/app/shared/host-window.reducer.spec.ts
+++ b/src/app/shared/host-window.reducer.spec.ts
@@ -1,3 +1,4 @@
+// eslint-disable-next-line import/no-namespace
import * as deepFreeze from 'deep-freeze';
import { hostWindowReducer } from './search/host-window.reducer';
import { HostWindowResizeAction } from './host-window.actions';
diff --git a/src/app/shared/host-window.service.ts b/src/app/shared/host-window.service.ts
index 443dd40b14..6d13d921e0 100644
--- a/src/app/shared/host-window.service.ts
+++ b/src/app/shared/host-window.service.ts
@@ -7,7 +7,7 @@ import { createSelector, select, Store } from '@ngrx/store';
import { hasValue } from './empty.util';
import { AppState } from '../app.reducer';
-import { CSSVariableService } from './sass-helper/sass-helper.service';
+import { CSSVariableService } from './sass-helper/css-variable.service';
export enum WidthCategory {
XS,
@@ -31,10 +31,10 @@ export class HostWindowService {
/* See _exposed_variables.scss */
variableService.getAllVariables()
.subscribe((variables) => {
- this.breakPoints.XL_MIN = parseInt(variables.xlMin, 10);
- this.breakPoints.LG_MIN = parseInt(variables.lgMin, 10);
- this.breakPoints.MD_MIN = parseInt(variables.mdMin, 10);
- this.breakPoints.SM_MIN = parseInt(variables.smMin, 10);
+ this.breakPoints.XL_MIN = parseInt(variables['--bs-xl-min'], 10);
+ this.breakPoints.LG_MIN = parseInt(variables['--bs-lg-min'], 10);
+ this.breakPoints.MD_MIN = parseInt(variables['--bs-md-min'], 10);
+ this.breakPoints.SM_MIN = parseInt(variables['--bs-sm-min'], 10);
});
}
diff --git a/src/app/shared/interfaces/modal-before-dismiss.interface.ts b/src/app/shared/interfaces/modal-before-dismiss.interface.ts
index fca28e1cff..f884432fb8 100644
--- a/src/app/shared/interfaces/modal-before-dismiss.interface.ts
+++ b/src/app/shared/interfaces/modal-before-dismiss.interface.ts
@@ -1,4 +1,3 @@
-import { NgbModalConfig, NgbModal } from '@ng-bootstrap/ng-bootstrap';
/**
* If a component implementing this interface is used to create a modal (i.e. it is passed to {@link NgbModal#open}),
diff --git a/src/app/shared/key-value-pair.model.ts b/src/app/shared/key-value-pair.model.ts
new file mode 100644
index 0000000000..6fb63070ad
--- /dev/null
+++ b/src/app/shared/key-value-pair.model.ts
@@ -0,0 +1,4 @@
+export interface KeyValuePair {
+ key: K;
+ value: V;
+}
diff --git a/src/app/shared/menu/menu-item/link-menu-item.component.ts b/src/app/shared/menu/menu-item/link-menu-item.component.ts
index c9a60f0c28..78d5b2fc6f 100644
--- a/src/app/shared/menu/menu-item/link-menu-item.component.ts
+++ b/src/app/shared/menu/menu-item/link-menu-item.component.ts
@@ -1,4 +1,4 @@
-import { Component, Inject, Input, OnInit } from '@angular/core';
+import { Component, Inject, OnInit } from '@angular/core';
import { LinkMenuItemModel } from './models/link.model';
import { rendersMenuItemForType } from '../menu-item.decorator';
import { isNotEmpty } from '../../empty.util';
diff --git a/src/app/shared/menu/menu-item/text-menu-item.component.ts b/src/app/shared/menu/menu-item/text-menu-item.component.ts
index af690d198c..25549f53a8 100644
--- a/src/app/shared/menu/menu-item/text-menu-item.component.ts
+++ b/src/app/shared/menu/menu-item/text-menu-item.component.ts
@@ -1,4 +1,4 @@
-import { Component, Inject, Input } from '@angular/core';
+import { Component, Inject } from '@angular/core';
import { TextMenuItemModel } from './models/text.model';
import { rendersMenuItemForType } from '../menu-item.decorator';
import { MenuItemType } from '../menu-item-type.model';
diff --git a/src/app/shared/menu/menu.module.ts b/src/app/shared/menu/menu.module.ts
index 1d186a9b7d..c007af517d 100644
--- a/src/app/shared/menu/menu.module.ts
+++ b/src/app/shared/menu/menu.module.ts
@@ -12,8 +12,11 @@ import { ExternalLinkMenuItemComponent } from './menu-item/external-link-menu-it
const COMPONENTS = [
MenuSectionComponent,
MenuComponent,
- LinkMenuItemComponent,
+];
+
+const ENTRY_COMPONENTS = [
TextMenuItemComponent,
+ LinkMenuItemComponent,
OnClickMenuItemComponent,
ExternalLinkMenuItemComponent,
];
@@ -32,10 +35,12 @@ const PROVIDERS = [
...MODULES
],
declarations: [
- ...COMPONENTS
+ ...COMPONENTS,
+ ...ENTRY_COMPONENTS,
],
providers: [
- ...PROVIDERS
+ ...PROVIDERS,
+ ...ENTRY_COMPONENTS,
],
exports: [
...COMPONENTS
diff --git a/src/app/shared/menu/menu.reducer.spec.ts b/src/app/shared/menu/menu.reducer.spec.ts
index 7ae05536af..2865e887fc 100644
--- a/src/app/shared/menu/menu.reducer.spec.ts
+++ b/src/app/shared/menu/menu.reducer.spec.ts
@@ -1,3 +1,4 @@
+// eslint-disable-next-line import/no-namespace
import * as deepFreeze from 'deep-freeze';
import {
ActivateMenuSectionAction,
diff --git a/src/app/shared/mocks/dspace-rest/endpoint-mocking-rest.service.ts b/src/app/shared/mocks/dspace-rest/endpoint-mocking-rest.service.ts
index 8d621ad4be..0358451557 100644
--- a/src/app/shared/mocks/dspace-rest/endpoint-mocking-rest.service.ts
+++ b/src/app/shared/mocks/dspace-rest/endpoint-mocking-rest.service.ts
@@ -7,7 +7,6 @@ import { RestRequestMethod } from '../../../core/data/rest-request-method';
import { RawRestResponse } from '../../../core/dspace-rest/raw-rest-response.model';
import { DspaceRestService, HttpOptions } from '../../../core/dspace-rest/dspace-rest.service';
import { MOCK_RESPONSE_MAP, ResponseMapMock } from './mocks/response-map.mock';
-import * as URL from 'url-parse';
import { environment } from '../../../../environments/environment';
/**
diff --git a/src/app/shared/mydspace-actions/claimed-task/approve/claimed-task-actions-approve.component.spec.ts b/src/app/shared/mydspace-actions/claimed-task/approve/claimed-task-actions-approve.component.spec.ts
index ce67da1349..f43316c4e1 100644
--- a/src/app/shared/mydspace-actions/claimed-task/approve/claimed-task-actions-approve.component.spec.ts
+++ b/src/app/shared/mydspace-actions/claimed-task/approve/claimed-task-actions-approve.component.spec.ts
@@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, Injector, NO_ERRORS_SCHEMA } from '@angular/core';
-import { async, ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
+import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { of, of as observableOf } from 'rxjs';
diff --git a/src/app/shared/notifications/notifications-board/notifications-board.component.spec.ts b/src/app/shared/notifications/notifications-board/notifications-board.component.spec.ts
index 1d3faabdaa..08b9585a8c 100644
--- a/src/app/shared/notifications/notifications-board/notifications-board.component.spec.ts
+++ b/src/app/shared/notifications/notifications-board/notifications-board.component.spec.ts
@@ -11,7 +11,7 @@ import { AppState } from '../../../app.reducer';
import { NotificationComponent } from '../notification/notification.component';
import { Notification } from '../models/notification.model';
import { NotificationType } from '../models/notification-type';
-import { uniqueId } from 'lodash';
+import uniqueId from 'lodash/uniqueId';
import { INotificationBoardOptions } from '../../../../config/notifications-config.interfaces';
import { NotificationsServiceStub } from '../../testing/notifications-service.stub';
import { cold } from 'jasmine-marbles';
diff --git a/src/app/shared/notifications/notifications-board/notifications-board.component.ts b/src/app/shared/notifications/notifications-board/notifications-board.component.ts
index f153d1009e..97ae09c1a6 100644
--- a/src/app/shared/notifications/notifications-board/notifications-board.component.ts
+++ b/src/app/shared/notifications/notifications-board/notifications-board.component.ts
@@ -10,7 +10,7 @@ import {
import { select, Store } from '@ngrx/store';
import { BehaviorSubject, Subscription } from 'rxjs';
-import { difference } from 'lodash';
+import difference from 'lodash/difference';
import { NotificationsService } from '../notifications.service';
import { AppState } from '../../../app.reducer';
diff --git a/src/app/shared/notifications/notifications.reducers.spec.ts b/src/app/shared/notifications/notifications.reducers.spec.ts
index b834797115..fde92e8891 100644
--- a/src/app/shared/notifications/notifications.reducers.spec.ts
+++ b/src/app/shared/notifications/notifications.reducers.spec.ts
@@ -9,7 +9,7 @@ import { NotificationOptions } from './models/notification-options.model';
import { NotificationAnimationsType } from './models/notification-animations-type';
import { NotificationType } from './models/notification-type';
import { Notification } from './models/notification.model';
-import { uniqueId } from 'lodash';
+import uniqueId from 'lodash/uniqueId';
import { ChangeDetectorRef } from '@angular/core';
import { storeModuleConfig } from '../../app.reducer';
diff --git a/src/app/shared/notifications/notifications.service.ts b/src/app/shared/notifications/notifications.service.ts
index 98272d4f43..d37d6a349b 100644
--- a/src/app/shared/notifications/notifications.service.ts
+++ b/src/app/shared/notifications/notifications.service.ts
@@ -4,7 +4,7 @@ import { of as observableOf } from 'rxjs';
import { first } from 'rxjs/operators';
import { Store } from '@ngrx/store';
import { TranslateService } from '@ngx-translate/core';
-import { uniqueId } from 'lodash';
+import uniqueId from 'lodash/uniqueId';
import { INotification, Notification } from './models/notification.model';
import { NotificationType } from './models/notification-type';
diff --git a/src/app/shared/object-collection/shared/listable-object/listable-object.decorator.spec.ts b/src/app/shared/object-collection/shared/listable-object/listable-object.decorator.spec.ts
index 7475aac967..f7d00510f6 100644
--- a/src/app/shared/object-collection/shared/listable-object/listable-object.decorator.spec.ts
+++ b/src/app/shared/object-collection/shared/listable-object/listable-object.decorator.spec.ts
@@ -1,7 +1,6 @@
/* eslint-disable max-classes-per-file */
-import { Item } from '../../../../core/shared/item.model';
import { ViewMode } from '../../../../core/shared/view-mode.model';
-import { getListableObjectComponent, listableObjectComponent } from './listable-object.decorator';
+import { DEFAULT_VIEW_MODE, getListableObjectComponent, listableObjectComponent } from './listable-object.decorator';
import { Context } from '../../../../core/shared/context.model';
import { environment } from '../../../../../environments/environment';
@@ -13,6 +12,10 @@ describe('ListableObject decorator function', () => {
const type3 = 'TestType3';
const typeAncestor = 'TestTypeAncestor';
const typeUnthemed = 'TestTypeUnthemed';
+ const typeLowPriority = 'TypeLowPriority';
+ const typeLowPriority2 = 'TypeLowPriority2';
+ const typeMidPriority = 'TypeMidPriority';
+ const typeHighPriority = 'TypeHighPriority';
class Test1List {
}
@@ -38,6 +41,21 @@ describe('ListableObject decorator function', () => {
class TestUnthemedComponent {
}
+ class TestDefaultLowPriorityComponent {
+ }
+
+ class TestLowPriorityComponent {
+ }
+
+ class TestDefaultMidPriorityComponent {
+ }
+
+ class TestMidPriorityComponent {
+ }
+
+ class TestHighPriorityComponent {
+ }
+
/* eslint-enable max-classes-per-file */
beforeEach(() => {
@@ -54,6 +72,15 @@ describe('ListableObject decorator function', () => {
listableObjectComponent(typeAncestor, ViewMode.ListElement, Context.Any, 'ancestor')(TestAncestorComponent);
listableObjectComponent(typeUnthemed, ViewMode.ListElement, Context.Any)(TestUnthemedComponent);
+ // Register component with different priorities for expected parameters:
+ // ViewMode.DetailedListElement, Context.Search, 'custom'
+ listableObjectComponent(typeLowPriority, DEFAULT_VIEW_MODE, undefined, undefined)(TestDefaultLowPriorityComponent);
+ listableObjectComponent(typeLowPriority, DEFAULT_VIEW_MODE, Context.Search, 'custom')(TestLowPriorityComponent);
+ listableObjectComponent(typeLowPriority2, DEFAULT_VIEW_MODE, undefined, undefined)(TestDefaultLowPriorityComponent);
+ listableObjectComponent(typeMidPriority, ViewMode.DetailedListElement, undefined, undefined)(TestDefaultMidPriorityComponent);
+ listableObjectComponent(typeMidPriority, ViewMode.DetailedListElement, undefined, 'custom')(TestMidPriorityComponent);
+ listableObjectComponent(typeHighPriority, ViewMode.DetailedListElement, Context.Search, undefined)(TestHighPriorityComponent);
+
ogEnvironmentThemes = environment.themes;
});
@@ -81,7 +108,7 @@ describe('ListableObject decorator function', () => {
});
});
- describe('If there isn\'nt an exact match', () => {
+ describe('If there isn\'t an exact match', () => {
describe('If there is a match for one of the entity types and the view mode', () => {
it('should return the class with the matching entity type and view mode and default context', () => {
const component = getListableObjectComponent([type3], ViewMode.ListElement, Context.Workspace);
@@ -152,4 +179,45 @@ describe('ListableObject decorator function', () => {
});
});
});
+
+ describe('priorities', () => {
+ beforeEach(() => {
+ environment.themes = [
+ {
+ name: 'custom',
+ }
+ ];
+ });
+
+ describe('If a component with default ViewMode contains specific context and/or theme', () => {
+ it('requesting a specific ViewMode should return the one with the requested context and/or theme', () => {
+ const component = getListableObjectComponent([typeLowPriority], ViewMode.DetailedListElement, Context.Search, 'custom');
+ expect(component).toEqual(TestLowPriorityComponent);
+ });
+ });
+
+ describe('If a component with default Context contains specific ViewMode and/or theme', () => {
+ it('requesting a specific Context should return the one with the requested view-mode and/or theme', () => {
+ const component = getListableObjectComponent([typeMidPriority], ViewMode.DetailedListElement, Context.Search, 'custom');
+ expect(component).toEqual(TestMidPriorityComponent);
+ });
+ });
+
+ describe('If multiple components exist, each containing a different default value for one of the requested parameters', () => {
+ it('the component with the latest default value in the list should be returned', () => {
+ let component = getListableObjectComponent([typeMidPriority, typeLowPriority], ViewMode.DetailedListElement, Context.Search, 'custom');
+ expect(component).toEqual(TestMidPriorityComponent);
+
+ component = getListableObjectComponent([typeLowPriority, typeMidPriority, typeHighPriority], ViewMode.DetailedListElement, Context.Search, 'custom');
+ expect(component).toEqual(TestHighPriorityComponent);
+ });
+ });
+
+ describe('If two components exist for two different types, both configured for the same view-mode, but one for a specific context and/or theme', () => {
+ it('requesting a component for that specific context and/or theme while providing both types should return the most relevant one', () => {
+ const component = getListableObjectComponent([typeLowPriority2, typeLowPriority], ViewMode.DetailedListElement, Context.Search, 'custom');
+ expect(component).toEqual(TestLowPriorityComponent);
+ });
+ });
+ });
});
diff --git a/src/app/shared/object-collection/shared/listable-object/listable-object.decorator.ts b/src/app/shared/object-collection/shared/listable-object/listable-object.decorator.ts
index b7f27d1553..e5654e63e0 100644
--- a/src/app/shared/object-collection/shared/listable-object/listable-object.decorator.ts
+++ b/src/app/shared/object-collection/shared/listable-object/listable-object.decorator.ts
@@ -11,6 +11,53 @@ export const DEFAULT_VIEW_MODE = ViewMode.ListElement;
export const DEFAULT_CONTEXT = Context.Any;
export const DEFAULT_THEME = '*';
+/**
+ * A class used to compare two matches and their relevancy to determine which of the two gains priority over the other
+ *
+ * "level" represents the index of the first default value that was used to find the match with:
+ * ViewMode being index 0, Context index 1 and theme index 2. Examples:
+ * - If a default value was used for context, but not view-mode and theme, the "level" will be 1
+ * - If a default value was used for view-mode and context, but not for theme, the "level" will be 0
+ * - If no default value was used for any of the fields, the "level" will be 3
+ *
+ * "relevancy" represents the amount of values that didn't require a default value to fall back on. Examples:
+ * - If a default value was used for theme, but not view-mode and context, the "relevancy" will be 2
+ * - If a default value was used for view-mode and context, but not for theme, the "relevancy" will be 1
+ * - If a default value was used for all fields, the "relevancy" will be 0
+ * - If no default value was used for any of the fields, the "relevancy" will be 3
+ *
+ * To determine which of two MatchRelevancies is the most relevant, we compare "level" and "relevancy" in that order.
+ * If any of the two is higher than the other, that match is most relevant. Examples:
+ * - { level: 1, relevancy: 1 } is more relevant than { level: 0, relevancy: 2 }
+ * - { level: 1, relevancy: 1 } is less relevant than { level: 1, relevancy: 2 }
+ * - { level: 1, relevancy: 1 } is more relevant than { level: 1, relevancy: 0 }
+ * - { level: 1, relevancy: 1 } is less relevant than { level: 2, relevancy: 0 }
+ * - { level: 1, relevancy: 1 } is more relevant than null
+ */
+class MatchRelevancy {
+ constructor(public match: any,
+ public level: number,
+ public relevancy: number) {
+ }
+
+ isMoreRelevantThan(otherMatch: MatchRelevancy): boolean {
+ if (hasNoValue(otherMatch)) {
+ return true;
+ }
+ if (otherMatch.level > this.level) {
+ return false;
+ }
+ if (otherMatch.level === this.level && otherMatch.relevancy > this.relevancy) {
+ return false;
+ }
+ return true;
+ }
+
+ isLessRelevantThan(otherMatch: MatchRelevancy): boolean {
+ return !this.isMoreRelevantThan(otherMatch);
+ }
+}
+
/**
* Factory to allow us to inject getThemeConfigFor so we can mock it in tests
*/
@@ -48,47 +95,70 @@ export function listableObjectComponent(objectType: string | GenericConstructor<
/**
* Getter to retrieve the matching listable object component
+ *
+ * Looping over the provided types, it'll attempt to find the best match depending on the {@link MatchRelevancy} returned by getMatch()
+ * The most relevant match between types is kept and eventually returned
+ *
* @param types The types of which one should match the listable component
* @param viewMode The view mode that should match the components
* @param context The context that should match the components
* @param theme The theme that should match the components
*/
export function getListableObjectComponent(types: (string | GenericConstructor)[], viewMode: ViewMode, context: Context = DEFAULT_CONTEXT, theme: string = DEFAULT_THEME) {
- let bestMatch;
- let bestMatchValue = 0;
+ let currentBestMatch: MatchRelevancy = null;
for (const type of types) {
const typeMap = map.get(type);
if (hasValue(typeMap)) {
- const typeModeMap = typeMap.get(viewMode);
- if (hasValue(typeModeMap)) {
- const contextMap = typeModeMap.get(context);
- if (hasValue(contextMap)) {
- const match = resolveTheme(contextMap, theme);
- if (hasValue(match)) {
- return match;
- }
- if (bestMatchValue < 3 && hasValue(contextMap.get(DEFAULT_THEME))) {
- bestMatchValue = 3;
- bestMatch = contextMap.get(DEFAULT_THEME);
- }
- }
- if (bestMatchValue < 2 &&
- hasValue(typeModeMap.get(DEFAULT_CONTEXT)) &&
- hasValue(typeModeMap.get(DEFAULT_CONTEXT).get(DEFAULT_THEME))) {
- bestMatchValue = 2;
- bestMatch = typeModeMap.get(DEFAULT_CONTEXT).get(DEFAULT_THEME);
- }
- }
- if (bestMatchValue < 1 &&
- hasValue(typeMap.get(DEFAULT_VIEW_MODE)) &&
- hasValue(typeMap.get(DEFAULT_VIEW_MODE).get(DEFAULT_CONTEXT)) &&
- hasValue(typeMap.get(DEFAULT_VIEW_MODE).get(DEFAULT_CONTEXT).get(DEFAULT_THEME))) {
- bestMatchValue = 1;
- bestMatch = typeMap.get(DEFAULT_VIEW_MODE).get(DEFAULT_CONTEXT).get(DEFAULT_THEME);
+ const match = getMatch(typeMap, [viewMode, context, theme], [DEFAULT_VIEW_MODE, DEFAULT_CONTEXT, DEFAULT_THEME]);
+ if (hasNoValue(currentBestMatch) || currentBestMatch.isLessRelevantThan(match)) {
+ currentBestMatch = match;
}
}
}
- return bestMatch;
+ return hasValue(currentBestMatch) ? currentBestMatch.match : null;
+}
+
+/**
+ * Find an object within a nested map, matching the provided keys as best as possible, falling back on defaults wherever
+ * needed.
+ *
+ * Starting off with a Map, it loops over the provided keys, going deeper into the map until it finds a value
+ * If at some point, no value is found, it'll attempt to use the default value for that index instead
+ * If the default value exists, the index is stored in the "level"
+ * If no default value exists, 1 is added to "relevancy"
+ * See {@link MatchRelevancy} what these represent
+ *
+ * @param typeMap a multi-dimensional map
+ * @param keys the keys of the multi-dimensional map to loop over. Each key represents a level within the map
+ * @param defaults the default values to use for each level, in case no value is found for the key at that index
+ * @returns matchAndLevel a {@link MatchRelevancy} object containing the match and its level of relevancy
+ */
+function getMatch(typeMap: Map, keys: any[], defaults: any[]): MatchRelevancy {
+ let currentMap = typeMap;
+ let level = -1;
+ let relevancy = 0;
+ for (let i = 0; i < keys.length; i++) {
+ // If we're currently checking the theme, resolve it first to take extended themes into account
+ let currentMatch = defaults[i] === DEFAULT_THEME ? resolveTheme(currentMap, keys[i]) : currentMap.get(keys[i]);
+ if (hasNoValue(currentMatch)) {
+ currentMatch = currentMap.get(defaults[i]);
+ if (level === -1) {
+ level = i;
+ }
+ } else {
+ relevancy++;
+ }
+ if (hasValue(currentMatch)) {
+ if (currentMatch instanceof Map) {
+ currentMap = currentMatch as Map;
+ } else {
+ return new MatchRelevancy(currentMatch, level > -1 ? level : i + 1, relevancy);
+ }
+ } else {
+ return null;
+ }
+ }
+ return null;
}
/**
diff --git a/src/app/shared/object-list/listable-notification-object/listable-notification-object.component.html b/src/app/shared/object-list/listable-notification-object/listable-notification-object.component.html
new file mode 100644
index 0000000000..d29199bae3
--- /dev/null
+++ b/src/app/shared/object-list/listable-notification-object/listable-notification-object.component.html
@@ -0,0 +1 @@
+{{ object?.message | translate }}
diff --git a/src/app/shared/object-list/listable-notification-object/listable-notification-object.component.scss b/src/app/shared/object-list/listable-notification-object/listable-notification-object.component.scss
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/app/shared/object-list/listable-notification-object/listable-notification-object.component.spec.ts b/src/app/shared/object-list/listable-notification-object/listable-notification-object.component.spec.ts
new file mode 100644
index 0000000000..3cf05f7fec
--- /dev/null
+++ b/src/app/shared/object-list/listable-notification-object/listable-notification-object.component.spec.ts
@@ -0,0 +1,43 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ListableNotificationObjectComponent } from './listable-notification-object.component';
+import { NotificationType } from '../../notifications/models/notification-type';
+import { ListableNotificationObject } from './listable-notification-object.model';
+import { By } from '@angular/platform-browser';
+import { TranslateModule } from '@ngx-translate/core';
+
+describe('ListableNotificationObjectComponent', () => {
+ let component: ListableNotificationObjectComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [
+ TranslateModule.forRoot(),
+ ],
+ declarations: [
+ ListableNotificationObjectComponent,
+ ],
+ }).compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ListableNotificationObjectComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ describe('ui', () => {
+ it('should display the given error message', () => {
+ component.object = new ListableNotificationObject(NotificationType.Error, 'test error message');
+ fixture.detectChanges();
+
+ const listableNotificationObject: Element = fixture.debugElement.query(By.css('.alert')).nativeElement;
+ expect(listableNotificationObject.className).toContain(NotificationType.Error);
+ expect(listableNotificationObject.innerHTML).toBe('test error message');
+ });
+ });
+
+ afterEach(() => {
+ fixture.debugElement.nativeElement.remove();
+ });
+});
diff --git a/src/app/shared/object-list/listable-notification-object/listable-notification-object.component.ts b/src/app/shared/object-list/listable-notification-object/listable-notification-object.component.ts
new file mode 100644
index 0000000000..ca23ee76a2
--- /dev/null
+++ b/src/app/shared/object-list/listable-notification-object/listable-notification-object.component.ts
@@ -0,0 +1,21 @@
+import { Component } from '@angular/core';
+import {
+ AbstractListableElementComponent
+} from '../../object-collection/shared/object-collection-element/abstract-listable-element.component';
+import { ListableNotificationObject } from './listable-notification-object.model';
+import { listableObjectComponent } from '../../object-collection/shared/listable-object/listable-object.decorator';
+import { ViewMode } from '../../../core/shared/view-mode.model';
+import { LISTABLE_NOTIFICATION_OBJECT } from './listable-notification-object.resource-type';
+
+/**
+ * The component for displaying a notifications inside an object list
+ */
+@listableObjectComponent(ListableNotificationObject, ViewMode.ListElement)
+@listableObjectComponent(LISTABLE_NOTIFICATION_OBJECT.value, ViewMode.ListElement)
+@Component({
+ selector: 'ds-listable-notification-object',
+ templateUrl: './listable-notification-object.component.html',
+ styleUrls: ['./listable-notification-object.component.scss'],
+})
+export class ListableNotificationObjectComponent extends AbstractListableElementComponent {
+}
diff --git a/src/app/shared/object-list/listable-notification-object/listable-notification-object.model.ts b/src/app/shared/object-list/listable-notification-object/listable-notification-object.model.ts
new file mode 100644
index 0000000000..163d9096a2
--- /dev/null
+++ b/src/app/shared/object-list/listable-notification-object/listable-notification-object.model.ts
@@ -0,0 +1,36 @@
+import { ListableObject } from '../../object-collection/shared/listable-object.model';
+import { typedObject } from '../../../core/cache/builders/build-decorators';
+import { TypedObject } from '../../../core/cache/typed-object.model';
+import { LISTABLE_NOTIFICATION_OBJECT } from './listable-notification-object.resource-type';
+import { GenericConstructor } from '../../../core/shared/generic-constructor';
+import { NotificationType } from '../../notifications/models/notification-type';
+import { ResourceType } from '../../../core/shared/resource-type';
+
+/**
+ * Object representing a notification message inside a list of objects
+ */
+@typedObject
+export class ListableNotificationObject extends ListableObject implements TypedObject {
+
+ static type: ResourceType = LISTABLE_NOTIFICATION_OBJECT;
+ type: ResourceType = LISTABLE_NOTIFICATION_OBJECT;
+
+ protected renderTypes: string[];
+
+ constructor(
+ public notificationType: NotificationType = NotificationType.Error,
+ public message: string = 'listable-notification-object.default-message',
+ ...renderTypes: string[]
+ ) {
+ super();
+ this.renderTypes = renderTypes;
+ }
+
+ /**
+ * Method that returns as which type of object this object should be rendered.
+ */
+ getRenderTypes(): (string | GenericConstructor)[] {
+ return [...this.renderTypes, this.constructor as GenericConstructor];
+ }
+
+}
diff --git a/src/app/shared/object-list/listable-notification-object/listable-notification-object.resource-type.ts b/src/app/shared/object-list/listable-notification-object/listable-notification-object.resource-type.ts
new file mode 100644
index 0000000000..ed458126bb
--- /dev/null
+++ b/src/app/shared/object-list/listable-notification-object/listable-notification-object.resource-type.ts
@@ -0,0 +1,9 @@
+import { ResourceType } from '../../../core/shared/resource-type';
+
+/**
+ * The resource type for {@link ListableNotificationObject}
+ *
+ * Needs to be in a separate file to prevent circular
+ * dependencies in webpack.
+ */
+export const LISTABLE_NOTIFICATION_OBJECT = new ResourceType('listable-notification-object');
diff --git a/src/app/shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component.spec.ts b/src/app/shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component.spec.ts
index 897ec43491..226c1be33e 100644
--- a/src/app/shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component.spec.ts
+++ b/src/app/shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component.spec.ts
@@ -11,7 +11,6 @@ import { createSuccessfulRemoteDataObject$ } from '../../remote-data.utils';
import { HALResource } from '../../../core/shared/hal-resource.model';
import { ChildHALResource } from '../../../core/shared/child-hal-resource.model';
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
-import { DSONameServiceMock } from '../../mocks/dso-name.service.mock';
export function createSidebarSearchListElementTests(
componentClass: any,
diff --git a/src/app/shared/object.util.ts b/src/app/shared/object.util.ts
index 1602fb9839..4f8954259e 100644
--- a/src/app/shared/object.util.ts
+++ b/src/app/shared/object.util.ts
@@ -1,5 +1,7 @@
import { isNotEmpty } from './empty.util';
-import { isEqual, isObject, transform } from 'lodash';
+import isEqual from 'lodash/isEqual';
+import isObject from 'lodash/isObject';
+import transform from 'lodash/transform';
/**
* Returns passed object without specified property
diff --git a/src/app/shared/pagination-drag-and-drop/abstract-paginated-drag-and-drop-list.component.spec.ts b/src/app/shared/pagination-drag-and-drop/abstract-paginated-drag-and-drop-list.component.spec.ts
index 7db53425d5..bac6b89583 100644
--- a/src/app/shared/pagination-drag-and-drop/abstract-paginated-drag-and-drop-list.component.spec.ts
+++ b/src/app/shared/pagination-drag-and-drop/abstract-paginated-drag-and-drop-list.component.spec.ts
@@ -11,8 +11,6 @@ import { createSuccessfulRemoteDataObject } from '../remote-data.utils';
import { createPaginatedList } from '../testing/utils.test';
import { ObjectValuesPipe } from '../utils/object-values-pipe';
import { PaginationService } from '../../core/pagination/pagination.service';
-import { PaginationComponentOptions } from '../pagination/pagination-component-options.model';
-import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { PaginationServiceStub } from '../testing/pagination-service.stub';
import { FieldUpdates } from '../../core/data/object-updates/field-updates.model';
diff --git a/src/app/shared/pagination/pagination.component.spec.ts b/src/app/shared/pagination/pagination.component.spec.ts
index 2d913da8a3..30ace4b2b9 100644
--- a/src/app/shared/pagination/pagination.component.spec.ts
+++ b/src/app/shared/pagination/pagination.component.spec.ts
@@ -32,8 +32,7 @@ import { SortDirection, SortOptions } from '../../core/cache/models/sort-options
import { createTestComponent } from '../testing/utils.test';
import { storeModuleConfig } from '../../app.reducer';
import { PaginationService } from '../../core/pagination/pagination.service';
-import { BehaviorSubject, of as observableOf } from 'rxjs';
-import { PaginationServiceStub } from '../testing/pagination-service.stub';
+import { BehaviorSubject } from 'rxjs';
import { FindListOptions } from '../../core/data/find-list-options.model';
function expectPages(fixture: ComponentFixture, pagesDef: string[]): void {
diff --git a/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.spec.ts b/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.spec.ts
index 91d9200c2d..cec67e721c 100644
--- a/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.spec.ts
+++ b/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.spec.ts
@@ -4,7 +4,7 @@ import { ChangeDetectorRef, Component, Injector, NO_ERRORS_SCHEMA } from '@angul
import { of as observableOf } from 'rxjs';
import { TranslateModule } from '@ngx-translate/core';
import { cold } from 'jasmine-marbles';
-import { uniqueId } from 'lodash';
+import uniqueId from 'lodash/uniqueId';
import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils';
import { createTestComponent } from '../../../testing/utils.test';
@@ -19,10 +19,8 @@ import { PaginationComponentOptions } from '../../../pagination/pagination-compo
import { buildPaginatedList } from '../../../../core/data/paginated-list.model';
import { PageInfo } from '../../../../core/shared/page-info.model';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
-import { SortDirection, SortOptions } from '../../../../core/cache/models/sort-options.model';
import { PaginationService } from '../../../../core/pagination/pagination.service';
import { PaginationServiceStub } from '../../../testing/pagination-service.stub';
-import { FindListOptions } from '../../../../core/data/find-list-options.model';
describe('EpersonGroupListComponent test suite', () => {
let comp: EpersonGroupListComponent;
diff --git a/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.ts b/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.ts
index b120c3e016..b859184845 100644
--- a/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.ts
+++ b/src/app/shared/resource-policies/form/eperson-group-list/eperson-group-list.component.ts
@@ -2,7 +2,7 @@ import { Component, EventEmitter, Injector, Input, OnDestroy, OnInit, Output } f
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
import { map } from 'rxjs/operators';
-import { uniqueId } from 'lodash';
+import uniqueId from 'lodash/uniqueId';
import { RemoteData } from '../../../../core/data/remote-data';
import { PaginatedList } from '../../../../core/data/paginated-list.model';
diff --git a/src/app/shared/rss-feed/rss.component.spec.ts b/src/app/shared/rss-feed/rss.component.spec.ts
index fd7f2c5321..61b54a1125 100644
--- a/src/app/shared/rss-feed/rss.component.spec.ts
+++ b/src/app/shared/rss-feed/rss.component.spec.ts
@@ -1,5 +1,4 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
-import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { ConfigurationDataService } from '../../core/data/configuration-data.service';
import { RemoteData } from '../../core/data/remote-data';
import { GroupDataService } from '../../core/eperson/group-data.service';
@@ -23,7 +22,6 @@ import { RouterMock } from '../mocks/router.mock';
describe('RssComponent', () => {
let comp: RSSComponent;
- let options: SortOptions;
let fixture: ComponentFixture;
let uuid: string;
let query: string;
@@ -63,7 +61,6 @@ describe('RssComponent', () => {
pageSize: 10,
currentPage: 1
}),
- sort: new SortOptions('dc.title', SortDirection.ASC),
}));
groupDataService = jasmine.createSpyObj('groupsDataService', {
findListByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])),
@@ -88,7 +85,6 @@ describe('RssComponent', () => {
}));
beforeEach(() => {
- options = new SortOptions('dc.title', SortDirection.DESC);
uuid = '2cfcf65e-0a51-4bcb-8592-b8db7b064790';
query = 'test';
fixture = TestBed.createComponent(RSSComponent);
@@ -96,18 +92,18 @@ describe('RssComponent', () => {
});
it('should formulate the correct url given params in url', () => {
- const route = comp.formulateRoute(uuid, 'opensearch', options, query);
- expect(route).toBe('/opensearch/search?format=atom&scope=2cfcf65e-0a51-4bcb-8592-b8db7b064790&sort=dc.title&sort_direction=DESC&query=test');
+ const route = comp.formulateRoute(uuid, 'opensearch/search', query);
+ expect(route).toBe('/opensearch/search?format=atom&scope=2cfcf65e-0a51-4bcb-8592-b8db7b064790&query=test');
});
it('should skip uuid if its null', () => {
- const route = comp.formulateRoute(null, 'opensearch', options, query);
- expect(route).toBe('/opensearch/search?format=atom&sort=dc.title&sort_direction=DESC&query=test');
+ const route = comp.formulateRoute(null, 'opensearch/search', query);
+ expect(route).toBe('/opensearch/search?format=atom&query=test');
});
it('should default to query * if none provided', () => {
- const route = comp.formulateRoute(null, 'opensearch', options, null);
- expect(route).toBe('/opensearch/search?format=atom&sort=dc.title&sort_direction=DESC&query=*');
+ const route = comp.formulateRoute(null, 'opensearch/search', null);
+ expect(route).toBe('/opensearch/search?format=atom&query=*');
});
});
diff --git a/src/app/shared/rss-feed/rss.component.ts b/src/app/shared/rss-feed/rss.component.ts
index 3fdb859bdc..8a33aeeb68 100644
--- a/src/app/shared/rss-feed/rss.component.ts
+++ b/src/app/shared/rss-feed/rss.component.ts
@@ -12,7 +12,6 @@ import { ConfigurationDataService } from '../../core/data/configuration-data.ser
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
import { environment } from '../../../../src/environments/environment';
import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service';
-import { SortOptions } from '../../core/cache/models/sort-options.model';
import { PaginationService } from '../../core/pagination/pagination.service';
import { Router } from '@angular/router';
import { map, switchMap } from 'rxjs/operators';
@@ -39,7 +38,6 @@ export class RSSComponent implements OnInit, OnDestroy {
uuid: string;
configuration$: Observable;
- sortOption$: Observable;
subs: Subscription[] = [];
@@ -93,7 +91,7 @@ export class RSSComponent implements OnInit, OnDestroy {
return null;
}
this.uuid = this.groupDataService.getUUIDFromString(this.router.url);
- const route = environment.rest.baseUrl + this.formulateRoute(this.uuid, openSearchUri, searchOptions.sort, searchOptions.query);
+ const route = environment.rest.baseUrl + this.formulateRoute(this.uuid, openSearchUri, searchOptions.query);
this.addLinks(route);
this.linkHeadService.addTag({
href: environment.rest.baseUrl + '/' + openSearchUri + '/service',
@@ -109,24 +107,20 @@ export class RSSComponent implements OnInit, OnDestroy {
* Function created a route given the different params available to opensearch
* @param uuid The uuid if a scope is present
* @param opensearch openSearch uri
- * @param sort The sort options for the opensearch request
* @param query The query string that was provided in the search
* @returns The combine URL to opensearch
*/
- formulateRoute(uuid: string, opensearch: string, sort: SortOptions, query: string): string {
- let route = 'search?format=atom';
+ formulateRoute(uuid: string, opensearch: string, query: string): string {
+ let route = '?format=atom';
if (uuid) {
route += `&scope=${uuid}`;
}
- if (sort && sort.direction && sort.field && sort.field !== 'id') {
- route += `&sort=${sort.field}&sort_direction=${sort.direction}`;
- }
if (query) {
route += `&query=${query}`;
} else {
route += `&query=*`;
}
- route = '/' + opensearch + '/' + route;
+ route = '/' + opensearch + route;
return route;
}
diff --git a/src/app/shared/sass-helper/sass-helper.actions.ts b/src/app/shared/sass-helper/css-variable.actions.ts
similarity index 51%
rename from src/app/shared/sass-helper/sass-helper.actions.ts
rename to src/app/shared/sass-helper/css-variable.actions.ts
index 144904646e..2d58a2978b 100644
--- a/src/app/shared/sass-helper/sass-helper.actions.ts
+++ b/src/app/shared/sass-helper/css-variable.actions.ts
@@ -1,5 +1,7 @@
+/* eslint-disable max-classes-per-file */
import { Action } from '@ngrx/store';
import { type } from '../ngrx/type';
+import { KeyValuePair } from '../key-value-pair.model';
/**
* For each action type in an action group, make a simple
@@ -11,6 +13,8 @@ import { type } from '../ngrx/type';
*/
export const CSSVariableActionTypes = {
ADD: type('dspace/css-variables/ADD'),
+ ADD_ALL: type('dspace/css-variables/ADD_ALL'),
+ CLEAR: type('dspace/css-variables/CLEAR'),
};
export class AddCSSVariableAction implements Action {
@@ -24,5 +28,17 @@ export class AddCSSVariableAction implements Action {
this.payload = {name, value};
}
}
+export class AddAllCSSVariablesAction implements Action {
+ type = CSSVariableActionTypes.ADD_ALL;
+ payload: KeyValuePair[];
-export type CSSVariableAction = AddCSSVariableAction;
+ constructor(variables: KeyValuePair[]) {
+ this.payload = variables;
+ }
+}
+
+export class ClearCSSVariablesAction implements Action {
+ type = CSSVariableActionTypes.CLEAR;
+}
+
+export type CSSVariableAction = AddCSSVariableAction | AddAllCSSVariablesAction | ClearCSSVariablesAction;
diff --git a/src/app/shared/sass-helper/css-variable.reducer.ts b/src/app/shared/sass-helper/css-variable.reducer.ts
new file mode 100644
index 0000000000..449a936b4e
--- /dev/null
+++ b/src/app/shared/sass-helper/css-variable.reducer.ts
@@ -0,0 +1,30 @@
+import { CSSVariableAction, CSSVariableActionTypes } from './css-variable.actions';
+import { KeyValuePair } from '../key-value-pair.model';
+
+export interface CSSVariablesState {
+ [name: string]: string;
+}
+
+const initialState: CSSVariablesState = Object.create({});
+
+/**
+ * Reducer that handles the state of CSS variables in the store
+ * @param state The current state of the store
+ * @param action The action to apply onto the current state of the store
+ */
+export function cssVariablesReducer(state = initialState, action: CSSVariableAction): CSSVariablesState {
+ switch (action.type) {
+ case CSSVariableActionTypes.ADD: {
+ const variable = action.payload;
+ return Object.assign({}, state, { [variable.name]: variable.value });
+ } case CSSVariableActionTypes.ADD_ALL: {
+ const variables = action.payload;
+ return Object.assign({}, state, ...variables.map(({ key, value }: KeyValuePair) => {return {[key]: value};}));
+ } case CSSVariableActionTypes.CLEAR: {
+ return initialState;
+ }
+ default: {
+ return state;
+ }
+ }
+}
diff --git a/src/app/shared/sass-helper/css-variable.service.spec.ts b/src/app/shared/sass-helper/css-variable.service.spec.ts
new file mode 100644
index 0000000000..559384a5d7
--- /dev/null
+++ b/src/app/shared/sass-helper/css-variable.service.spec.ts
@@ -0,0 +1,78 @@
+import { TestBed } from '@angular/core/testing';
+import { CSSVariableService } from './css-variable.service';
+import { MockStore, provideMockStore } from '@ngrx/store/testing';
+import { getTestScheduler } from 'jasmine-marbles';
+import { buildPaginatedList } from '../../core/data/paginated-list.model';
+import { PageInfo } from '../../core/shared/page-info.model';
+import { KeyValuePair } from '../key-value-pair.model';
+
+describe('CSSVariableService', () => {
+ let store: MockStore;
+
+ let service: CSSVariableService;
+ let initialState;
+ const varKey1 = '--test-1';
+ const varValue1 = 'test-value-1';
+ const varKey2 = '--test-2';
+ const varValue2 = 'test-value-2';
+ const varKey3 = '--test-3';
+ const varValue3 = 'test-value-3';
+ const queryInAll = 'test';
+ const queryFor3 = '3';
+
+ function init() {
+ initialState = {
+ ['cssVariables']: {
+ [varKey1]: varValue1,
+ [varKey2]: varValue2,
+ [varKey3]: varValue3,
+ }
+ };
+ }
+
+ beforeEach(() => {
+ init();
+ TestBed.configureTestingModule({
+ providers: [
+ CSSVariableService,
+ provideMockStore({ initialState }),
+ ],
+ });
+ service = TestBed.inject(CSSVariableService as any);
+ store = TestBed.inject(MockStore as any);
+ });
+
+ it('should create', () => {
+ expect(service).toBeTruthy();
+ });
+
+ describe('searchVariable', () => {
+ it('should return the right keys and variables in a paginated list for query that returns all 3 results', () => {
+ const currentPage = 1;
+ const pageSize = 5;
+ const pageInfo = new PageInfo({ currentPage, elementsPerPage: pageSize, totalPages: 1, totalElements: 3 });
+ const page: KeyValuePair[] = [{ key: varKey1, value: varValue1 }, { key: varKey2, value: varValue2 }, { key: varKey3, value: varValue3 }];
+ const result = buildPaginatedList(pageInfo, page);
+ getTestScheduler().expectObservable(service.searchVariable(queryInAll, { currentPage, pageSize } as any)).toBe('a', { a: result });
+ });
+
+ it('should return the right keys and variables in a paginated list for query that returns only the 3rd results', () => {
+ const currentPage = 1;
+ const pageSize = 5;
+ const pageInfo = new PageInfo({ currentPage, elementsPerPage: pageSize, totalPages: 1, totalElements: 1 });
+ const page: KeyValuePair[] = [{ key: varKey3, value: varValue3 }];
+ const result = buildPaginatedList(pageInfo, page);
+ getTestScheduler().expectObservable(service.searchVariable(queryFor3, { currentPage, pageSize } as any)).toBe('a', { a: result });
+ });
+
+ it('should return the right keys and variables in a paginated list that\'s not longer than the page size', () => {
+ const currentPage = 1;
+ const pageSize = 2;
+ const pageInfo = new PageInfo({ currentPage, elementsPerPage: pageSize, totalPages: 2, totalElements: 3 });
+ const page: KeyValuePair[] = [{ key: varKey1, value: varValue1 }, { key: varKey2, value: varValue2 }];
+ const result = buildPaginatedList(pageInfo, page);
+ getTestScheduler().expectObservable(service.searchVariable(queryInAll, { currentPage, pageSize } as any)).toBe('a', { a: result });
+ });
+ });
+
+});
diff --git a/src/app/shared/sass-helper/css-variable.service.ts b/src/app/shared/sass-helper/css-variable.service.ts
new file mode 100644
index 0000000000..0190a05036
--- /dev/null
+++ b/src/app/shared/sass-helper/css-variable.service.ts
@@ -0,0 +1,161 @@
+import { Injectable } from '@angular/core';
+import { AppState, keySelector } from '../../app.reducer';
+import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
+import { AddAllCSSVariablesAction, AddCSSVariableAction, ClearCSSVariablesAction } from './css-variable.actions';
+import { PaginationComponentOptions } from '../pagination/pagination-component-options.model';
+import { buildPaginatedList, PaginatedList } from '../../core/data/paginated-list.model';
+import { Observable } from 'rxjs';
+import { hasValue, isNotEmpty } from '../empty.util';
+import { KeyValuePair } from '../key-value-pair.model';
+import { PageInfo } from '../../core/shared/page-info.model';
+import { CSSVariablesState } from './css-variable.reducer';
+
+/**
+ * This service deals with adding and retrieving CSS variables to and from the store
+ */
+@Injectable({
+ providedIn: 'root'
+})
+export class CSSVariableService {
+ isSameDomain = (styleSheet) => {
+ // Internal style blocks won't have an href value
+ if (!styleSheet.href) {
+ return true;
+ }
+
+ return styleSheet.href.indexOf(window.location.origin) === 0;
+ };
+
+ /*
+ Determine if the given rule is a CSSStyleRule
+ See: https://developer.mozilla.org/en-US/docs/Web/API/CSSRule#Type_constants
+ */
+ isStyleRule = (rule) => rule.type === 1;
+
+ constructor(
+ protected store: Store) {
+ }
+
+ /**
+ * Adds a CSS variable to the store
+ * @param name The name/key of the CSS variable
+ * @param value The value of the CSS variable
+ */
+ addCSSVariable(name: string, value: string) {
+ this.store.dispatch(new AddCSSVariableAction(name, value));
+ }
+
+ /**
+ * Adds multiples CSS variables to the store
+ * @param variables The key-value pairs with the CSS variables to be added
+ */
+ addCSSVariables(variables: KeyValuePair[]) {
+ this.store.dispatch(new AddAllCSSVariablesAction(variables));
+ }
+
+ /**
+ * Clears all CSS variables ƒrom the store
+ */
+ clearCSSVariables() {
+ this.store.dispatch(new ClearCSSVariablesAction());
+ }
+
+ /**
+ * Returns the value of a specific CSS key
+ * @param name The name/key of the CSS value
+ */
+ getVariable(name: string): Observable {
+ return this.store.pipe(select(themeVariableByNameSelector(name)));
+ }
+
+ /**
+ * Returns the CSSVariablesState of the store containing all variables
+ */
+ getAllVariables(): Observable {
+ return this.store.pipe(select(themeVariablesSelector));
+ }
+
+ /**
+ * Method to find CSS variables by their partially supplying their key. Case sensitive. Returns a paginated list of KeyValuePairs with CSS variables that match the query.
+ * @param query The query to look for in the keys
+ * @param paginationOptions The pagination options for the requested page
+ */
+ searchVariable(query: string, paginationOptions: PaginationComponentOptions): Observable>> {
+ return this.store.pipe(select(themePaginatedVariablesByQuery(query, paginationOptions)));
+ }
+
+ /**
+ * Get all custom properties on a page
+ * @return array>
+ * ex; [{key: "--color-accent", value: "#b9f500"}, {key: "--color-text", value: "#252525"}, ...]
+ */
+ getCSSVariablesFromStylesheets(document: Document): KeyValuePair[] {
+ if (isNotEmpty(document.styleSheets)) {
+ // styleSheets is array-like, so we convert it to an array.
+ // Filter out any stylesheets not on this domain
+ return [...document.styleSheets]
+ .filter(this.isSameDomain)
+ .reduce(
+ (finalArr, sheet) =>
+ finalArr.concat(
+ // cssRules is array-like, so we convert it to an array
+ [...sheet.cssRules].filter(this.isStyleRule).reduce((propValArr, rule: any) => {
+ const props = [...rule.style]
+ .map((propName) => {
+ return {
+ key: propName.trim(),
+ value: rule.style.getPropertyValue(propName).trim()
+ } as KeyValuePair;
+ }
+ )
+ // Discard any props that don't start with "--". Custom props are required to.
+ .filter(({ key }: KeyValuePair) => key.indexOf('--') === 0);
+
+ return [...propValArr, ...props];
+ }, [])
+ ),
+ []
+ );
+ } else {
+ return [];
+ }
+ }
+}
+
+const themeVariablesSelector = (state: AppState) => state.cssVariables;
+
+const themeVariableByNameSelector = (name: string): MemoizedSelector => {
+ return keySelector(name, themeVariablesSelector);
+};
+
+// Split this up into two memoized selectors so the query search gets cached separately from the pagination,
+// since the entire list has to be retrieved every time anyway
+const themePaginatedVariablesByQuery = (query: string, pagination: PaginationComponentOptions): MemoizedSelector>> => {
+ return createSelector(themeVariablesByQuery(query), (pairs) => {
+ if (hasValue(pairs)) {
+ const { currentPage, pageSize } = pagination;
+ const startIndex = (currentPage - 1) * pageSize;
+ const endIndex = startIndex + pageSize;
+ const pairsPage = pairs.slice(startIndex, endIndex);
+ const totalPages = Math.ceil(pairs.length / pageSize);
+ const pageInfo = new PageInfo({ currentPage, elementsPerPage: pageSize, totalElements: pairs.length, totalPages });
+ return buildPaginatedList(pageInfo, pairsPage);
+ } else {
+ return undefined;
+ }
+ });
+};
+
+const themeVariablesByQuery = (query: string): MemoizedSelector[]> => {
+ return createSelector(themeVariablesSelector, (state) => {
+ if (hasValue(state)) {
+ return Object.keys(state)
+ .filter((key: string) => key.includes(query))
+ .map((key: string) => {
+ return { key, value: state[key] };
+ });
+ } else {
+ return undefined;
+ }
+ });
+};
diff --git a/src/app/shared/sass-helper/sass-helper.reducer.ts b/src/app/shared/sass-helper/sass-helper.reducer.ts
deleted file mode 100644
index 6f080619fa..0000000000
--- a/src/app/shared/sass-helper/sass-helper.reducer.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { CSSVariableAction, CSSVariableActionTypes } from './sass-helper.actions';
-
-export interface CSSVariablesState {
- [name: string]: string;
-}
-
-const initialState: CSSVariablesState = Object.create({});
-
-export function cssVariablesReducer(state = initialState, action: CSSVariableAction): CSSVariablesState {
- switch (action.type) {
- case CSSVariableActionTypes.ADD: {
- const variable = action.payload;
- const t = Object.assign({}, state, { [variable.name]: variable.value });
- return t;
- }
- default: {
- return state;
- }
- }
-}
diff --git a/src/app/shared/sass-helper/sass-helper.service.ts b/src/app/shared/sass-helper/sass-helper.service.ts
deleted file mode 100644
index 7cc83dab2d..0000000000
--- a/src/app/shared/sass-helper/sass-helper.service.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import { Injectable } from '@angular/core';
-import { AppState, keySelector } from '../../app.reducer';
-import { MemoizedSelector, select, Store } from '@ngrx/store';
-import { AddCSSVariableAction } from './sass-helper.actions';
-
-@Injectable()
-export class CSSVariableService {
- constructor(
- protected store: Store) {
- }
-
- addCSSVariable(name: string, value: string) {
- this.store.dispatch(new AddCSSVariableAction(name, value));
- }
-
- getVariable(name: string) {
- return this.store.pipe(select(themeVariableByNameSelector(name)));
- }
-
- getAllVariables() {
- return this.store.pipe(select(themeVariablesSelector));
- }
-
-}
-
-const themeVariablesSelector = (state: AppState) => state.cssVariables;
-
-const themeVariableByNameSelector = (name: string): MemoizedSelector => {
- return keySelector(name, themeVariablesSelector);
-};
diff --git a/src/app/shared/search-form/scope-selector-modal/scope-selector-modal.component.html b/src/app/shared/search-form/scope-selector-modal/scope-selector-modal.component.html
index bf5c15e963..e7165a9213 100644
--- a/src/app/shared/search-form/scope-selector-modal/scope-selector-modal.component.html
+++ b/src/app/shared/search-form/scope-selector-modal/scope-selector-modal.component.html
@@ -9,7 +9,7 @@
diff --git a/src/app/shared/search/search-filters/search-filter/search-filter.reducer.spec.ts b/src/app/shared/search/search-filters/search-filter/search-filter.reducer.spec.ts
index ae8108662d..aa64589d2e 100644
--- a/src/app/shared/search/search-filters/search-filter/search-filter.reducer.spec.ts
+++ b/src/app/shared/search/search-filters/search-filter/search-filter.reducer.spec.ts
@@ -1,3 +1,4 @@
+// eslint-disable-next-line import/no-namespace
import * as deepFreeze from 'deep-freeze';
import {
SearchFilterCollapseAction,
diff --git a/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.spec.ts b/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.spec.ts
index 44dda40d15..3a146f5059 100644
--- a/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.spec.ts
+++ b/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.spec.ts
@@ -31,7 +31,6 @@ describe('SearchRangeFilterComponent', () => {
let fixture: ComponentFixture;
const minSuffix = '.min';
const maxSuffix = '.max';
- const dateFormats = ['YYYY', 'YYYY-MM', 'YYYY-MM-DD'];
const filterName1 = 'test name';
const value1 = '2000 - 2012';
const value2 = '1992 - 2000';
diff --git a/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.ts b/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.ts
index fbd767284f..938f67412e 100644
--- a/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.ts
+++ b/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.ts
@@ -15,11 +15,11 @@ import {
} from '../../../../../core/shared/search/search-filter.service';
import { SearchService } from '../../../../../core/shared/search/search.service';
import { Router } from '@angular/router';
-import * as moment from 'moment';
import { SEARCH_CONFIG_SERVICE } from '../../../../../my-dspace-page/my-dspace-page.component';
import { SearchConfigurationService } from '../../../../../core/shared/search/search-configuration.service';
import { RouteService } from '../../../../../core/services/route.service';
import { hasValue } from '../../../../empty.util';
+import { yearFromString } from 'src/app/shared/date.util';
/**
* The suffix for a range filters' minimum in the frontend URL
@@ -31,11 +31,6 @@ export const RANGE_FILTER_MIN_SUFFIX = '.min';
*/
export const RANGE_FILTER_MAX_SUFFIX = '.max';
-/**
- * The date formats that are possible to appear in a date filter
- */
-const dateFormats = ['YYYY', 'YYYY-MM', 'YYYY-MM-DD'];
-
/**
* This component renders a simple item page.
* The route parameter 'id' is used to request the item it represents.
@@ -99,8 +94,8 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple
*/
ngOnInit(): void {
super.ngOnInit();
- this.min = moment(this.filterConfig.minValue, dateFormats).year() || this.min;
- this.max = moment(this.filterConfig.maxValue, dateFormats).year() || this.max;
+ this.min = yearFromString(this.filterConfig.minValue) || this.min;
+ this.max = yearFromString(this.filterConfig.maxValue) || this.max;
const iniMin = this.route.getQueryParameterValue(this.filterConfig.paramName + RANGE_FILTER_MIN_SUFFIX).pipe(startWith(undefined));
const iniMax = this.route.getQueryParameterValue(this.filterConfig.paramName + RANGE_FILTER_MAX_SUFFIX).pipe(startWith(undefined));
this.sub = observableCombineLatest(iniMin, iniMax).pipe(
diff --git a/src/app/shared/search/search-labels/search-label/search-label.component.spec.ts b/src/app/shared/search/search-labels/search-label/search-label.component.spec.ts
index 50bcbc6938..b2be2ae53f 100644
--- a/src/app/shared/search/search-labels/search-label/search-label.component.spec.ts
+++ b/src/app/shared/search/search-labels/search-label/search-label.component.spec.ts
@@ -12,11 +12,9 @@ import { SearchServiceStub } from '../../../testing/search-service.stub';
import { SearchConfigurationServiceStub } from '../../../testing/search-configuration-service.stub';
import { SearchService } from '../../../../core/shared/search/search.service';
import { PaginationComponentOptions } from '../../../pagination/pagination-component-options.model';
-import { SortDirection, SortOptions } from '../../../../core/cache/models/sort-options.model';
import { PaginationService } from '../../../../core/pagination/pagination.service';
import { SearchConfigurationService } from '../../../../core/shared/search/search-configuration.service';
import { PaginationServiceStub } from '../../../testing/pagination-service.stub';
-import { FindListOptions } from '../../../../core/data/find-list-options.model';
describe('SearchLabelComponent', () => {
let comp: SearchLabelComponent;
diff --git a/src/app/shared/search/search-switch-configuration/search-switch-configuration.component.ts b/src/app/shared/search/search-switch-configuration/search-switch-configuration.component.ts
index 1852277673..0e1b4f221b 100644
--- a/src/app/shared/search/search-switch-configuration/search-switch-configuration.component.ts
+++ b/src/app/shared/search/search-switch-configuration/search-switch-configuration.component.ts
@@ -10,7 +10,7 @@ import { MyDSpaceConfigurationValueType } from '../../../my-dspace-page/my-dspac
import { SearchConfigurationOption } from './search-configuration-option.model';
import { SearchService } from '../../../core/shared/search/search.service';
import { currentPath } from '../../utils/route.utils';
-import { findIndex } from 'lodash';
+import findIndex from 'lodash/findIndex';
@Component({
selector: 'ds-search-switch-configuration',
diff --git a/src/app/shared/search/search.component.ts b/src/app/shared/search/search.component.ts
index 2abd5290cb..c094e37ef2 100644
--- a/src/app/shared/search/search.component.ts
+++ b/src/app/shared/search/search.component.ts
@@ -3,7 +3,7 @@ import { Router } from '@angular/router';
import { BehaviorSubject, combineLatest, Observable, Subscription } from 'rxjs';
import { debounceTime, distinctUntilChanged, filter, map, switchMap } from 'rxjs/operators';
-import { uniqueId } from 'lodash';
+import uniqueId from 'lodash/uniqueId';
import { PaginatedList } from '../../core/data/paginated-list.model';
import { RemoteData } from '../../core/data/remote-data';
diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts
index 45e9764151..2ec8fb1553 100644
--- a/src/app/shared/shared.module.ts
+++ b/src/app/shared/shared.module.ts
@@ -17,9 +17,7 @@ import {
} from '@ng-bootstrap/ng-bootstrap';
import { MissingTranslationHandler, TranslateModule } from '@ngx-translate/core';
import { NgxPaginationModule } from 'ngx-pagination';
-import { FileUploadModule } from 'ng2-file-upload';
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
-import { MomentModule } from 'ngx-moment';
import { ConfirmationModalComponent } from './confirmation-modal/confirmation-modal.component';
import {
ExportMetadataSelectorComponent
@@ -30,7 +28,6 @@ import {
import {
ImportBatchSelectorComponent
} from './dso-selector/modal-wrappers/import-batch-selector/import-batch-selector.component';
-import { FileDropzoneNoUploaderComponent } from './file-dropzone-no-uploader/file-dropzone-no-uploader.component';
import { ItemListElementComponent } from './object-list/item-list-element/item-types/item/item-list-element.component';
import { EnumKeysPipe } from './utils/enum-keys-pipe';
import { FileSizePipe } from './utils/file-size-pipe';
@@ -73,9 +70,6 @@ import { TruncatePipe } from './utils/truncate.pipe';
import { TruncatableComponent } from './truncatable/truncatable.component';
import { TruncatableService } from './truncatable/truncatable.service';
import { TruncatablePartComponent } from './truncatable/truncatable-part/truncatable-part.component';
-import { UploaderComponent } from './uploader/uploader.component';
-import { ChipsComponent } from './chips/chips.component';
-import { NumberPickerComponent } from './number-picker/number-picker.component';
import { MockAdminGuard } from './mocks/admin-guard.service.mock';
import { AlertComponent } from './alert/alert.component';
import {
@@ -111,7 +105,6 @@ import { EmphasizePipe } from './utils/emphasize.pipe';
import { InputSuggestionsComponent } from './input-suggestions/input-suggestions.component';
import { CapitalizePipe } from './utils/capitalize.pipe';
import { ObjectKeysPipe } from './utils/object-keys-pipe';
-import { AuthorityConfidenceStateDirective } from './authority-confidence/authority-confidence-state.directive';
import { LangSwitchComponent } from './lang-switch/lang-switch.component';
import {
PlainTextMetadataListElementComponent
@@ -232,7 +225,6 @@ import {
ImportableListItemControlComponent
} from './object-collection/shared/importable-list-item-control/importable-list-item-control.component';
import { ItemVersionsComponent } from './item/item-versions/item-versions.component';
-import { SortablejsModule } from 'ngx-sortablejs';
import { LogInContainerComponent } from './log-in/container/log-in-container.component';
import { LogInShibbolethComponent } from './log-in/methods/shibboleth/log-in-shibboleth.component';
import { LogInPasswordComponent } from './log-in/methods/password/log-in-password.component';
@@ -258,7 +250,6 @@ import { NgForTrackByIdDirective } from './ng-for-track-by-id.directive';
import { FileDownloadLinkComponent } from './file-download-link/file-download-link.component';
import { CollectionDropdownComponent } from './collection-dropdown/collection-dropdown.component';
import { EntityDropdownComponent } from './entity-dropdown/entity-dropdown.component';
-import { VocabularyTreeviewComponent } from './vocabulary-treeview/vocabulary-treeview.component';
import { CurationFormComponent } from '../curation-form/curation-form.component';
import {
PublicationSidebarSearchListElementComponent
@@ -293,9 +284,6 @@ import {
MetadataRepresentationListComponent
} from '../item-page/simple/metadata-representation-list/metadata-representation-list.component';
import { RelatedItemsComponent } from '../item-page/simple/related-items/related-items-component';
-import { LinkMenuItemComponent } from './menu/menu-item/link-menu-item.component';
-import { OnClickMenuItemComponent } from './menu/menu-item/onclick-menu-item.component';
-import { TextMenuItemComponent } from './menu/menu-item/text-menu-item.component';
import { SearchNavbarComponent } from '../search-navbar/search-navbar.component';
import { ThemedSearchNavbarComponent } from '../search-navbar/themed-search-navbar.component';
import {
@@ -312,7 +300,6 @@ import { DsSelectComponent } from './ds-select/ds-select.component';
import { LogInOidcComponent } from './log-in/methods/oidc/log-in-oidc.component';
import { ThemedItemListPreviewComponent } from './object-list/my-dspace-result-list-element/item-list-preview/themed-item-list-preview.component';
import { RSSComponent } from './rss-feed/rss.component';
-import { ExternalLinkMenuItemComponent } from './menu/menu-item/external-link-menu-item.component';
import { DsoPageOrcidButtonComponent } from './dso-page/dso-page-orcid-button/dso-page-orcid-button.component';
import { LogInOrcidComponent } from './log-in/methods/orcid/log-in-orcid.component';
import { BrowserOnlyPipe } from './utils/browser-only.pipe';
@@ -324,11 +311,14 @@ import {
} from '../item-page/simple/field-components/specific-field/title/item-page-title-field.component';
import { MarkdownPipe } from './utils/markdown.pipe';
import { GoogleRecaptchaModule } from '../core/google-recaptcha/google-recaptcha.module';
+import { MenuModule } from './menu/menu.module';
+import {
+ ListableNotificationObjectComponent
+} from './object-list/listable-notification-object/listable-notification-object.component';
+import { ThemedCollectionDropdownComponent } from './collection-dropdown/themed-collection-dropdown.component';
const MODULES = [
CommonModule,
- SortablejsModule,
- FileUploadModule,
FormsModule,
InfiniteScrollModule,
NgbNavModule,
@@ -342,10 +332,10 @@ const MODULES = [
ReactiveFormsModule,
RouterModule,
NouisliderModule,
- MomentModule,
DragDropModule,
CdkTreeModule,
GoogleRecaptchaModule,
+ MenuModule,
];
const ROOT_MODULES = [
@@ -377,7 +367,6 @@ const COMPONENTS = [
AuthNavMenuComponent,
ThemedAuthNavMenuComponent,
UserMenuComponent,
- ChipsComponent,
DsSelectComponent,
ErrorComponent,
FileSectionComponent,
@@ -386,7 +375,6 @@ const COMPONENTS = [
ThemedLoadingComponent,
LogInComponent,
LogOutComponent,
- NumberPickerComponent,
ObjectListComponent,
ThemedObjectListComponent,
ObjectDetailComponent,
@@ -401,8 +389,6 @@ const COMPONENTS = [
SidebarFilterComponent,
SidebarFilterSelectedOptionComponent,
ThumbnailComponent,
- UploaderComponent,
- FileDropzoneNoUploaderComponent,
ItemListPreviewComponent,
ThemedItemListPreviewComponent,
MyDSpaceItemStatusComponent,
@@ -410,10 +396,6 @@ const COMPONENTS = [
ItemDetailPreviewComponent,
ItemDetailPreviewFieldComponent,
ClaimedTaskActionsComponent,
- ClaimedTaskActionsApproveComponent,
- ClaimedTaskActionsRejectComponent,
- ClaimedTaskActionsReturnToPoolComponent,
- ClaimedTaskActionsEditMetadataComponent,
ClaimedTaskActionsLoaderComponent,
ItemActionsComponent,
PoolTaskActionsComponent,
@@ -428,90 +410,33 @@ const COMPONENTS = [
ValidationSuggestionsComponent,
DsoInputSuggestionsComponent,
DSOSelectorComponent,
- CreateCommunityParentSelectorComponent,
- ThemedCreateCommunityParentSelectorComponent,
- CreateCollectionParentSelectorComponent,
- ThemedCreateCollectionParentSelectorComponent,
- CreateItemParentSelectorComponent,
- ThemedCreateItemParentSelectorComponent,
- EditCommunitySelectorComponent,
- ThemedEditCommunitySelectorComponent,
- EditCollectionSelectorComponent,
- ThemedEditCollectionSelectorComponent,
- EditItemSelectorComponent,
- ThemedEditItemSelectorComponent,
- CommunitySearchResultListElementComponent,
- CollectionSearchResultListElementComponent,
- BrowseByComponent,
-
- CollectionSearchResultGridElementComponent,
- CommunitySearchResultGridElementComponent,
SearchExportCsvComponent,
PageSizeSelectorComponent,
ListableObjectComponentLoaderComponent,
- CollectionListElementComponent,
- CommunityListElementComponent,
- CollectionGridElementComponent,
- CommunityGridElementComponent,
- BrowseByComponent,
AbstractTrackableComponent,
ComcolMetadataComponent,
TypeBadgeComponent,
AccessStatusBadgeComponent,
- BrowseByComponent,
- AbstractTrackableComponent,
-
ItemSelectComponent,
CollectionSelectComponent,
MetadataRepresentationLoaderComponent,
SelectableListItemControlComponent,
-
ImportableListItemControlComponent,
-
- LogInShibbolethComponent,
- LogInOidcComponent,
- LogInOrcidComponent,
- LogInPasswordComponent,
LogInContainerComponent,
ItemVersionsComponent,
- ItemSearchResultListElementComponent,
ItemVersionsNoticeComponent,
ModifyItemOverviewComponent,
ImpersonateNavbarComponent,
- FileDownloadLinkComponent,
- BitstreamDownloadPageComponent,
- BitstreamRequestACopyPageComponent,
- CollectionDropdownComponent,
EntityDropdownComponent,
ExportMetadataSelectorComponent,
ImportBatchSelectorComponent,
ExportBatchSelectorComponent,
ConfirmationModalComponent,
- VocabularyTreeviewComponent,
AuthorizedCollectionSelectorComponent,
- CurationFormComponent,
- SearchResultListElementComponent,
- SearchResultGridElementComponent,
- ItemListElementComponent,
- ItemGridElementComponent,
- ItemSearchResultGridElementComponent,
- BrowseEntryListElementComponent,
- SearchResultDetailElementComponent,
- PlainTextMetadataListElementComponent,
- ItemMetadataListElementComponent,
- MetadataRepresentationListElementComponent,
- ItemMetadataRepresentationListElementComponent,
- BundleListElementComponent,
- StartsWithDateComponent,
- StartsWithTextComponent,
- SidebarSearchListElementComponent,
- PublicationSidebarSearchListElementComponent,
- CollectionSidebarSearchListElementComponent,
- CommunitySidebarSearchListElementComponent,
SearchNavbarComponent,
- ScopeSelectorModalComponent,
ItemPageTitleFieldComponent,
ThemedSearchNavbarComponent,
+ ListableNotificationObjectComponent,
];
const ENTRY_COMPONENTS = [
@@ -560,6 +485,7 @@ const ENTRY_COMPONENTS = [
ClaimedTaskActionsReturnToPoolComponent,
ClaimedTaskActionsEditMetadataComponent,
CollectionDropdownComponent,
+ ThemedCollectionDropdownComponent,
FileDownloadLinkComponent,
BitstreamDownloadPageComponent,
BitstreamRequestACopyPageComponent,
@@ -568,16 +494,12 @@ const ENTRY_COMPONENTS = [
ImportBatchSelectorComponent,
ExportBatchSelectorComponent,
ConfirmationModalComponent,
- VocabularyTreeviewComponent,
SidebarSearchListElementComponent,
PublicationSidebarSearchListElementComponent,
CollectionSidebarSearchListElementComponent,
CommunitySidebarSearchListElementComponent,
- LinkMenuItemComponent,
- OnClickMenuItemComponent,
- TextMenuItemComponent,
ScopeSelectorModalComponent,
- ExternalLinkMenuItemComponent
+ ListableNotificationObjectComponent,
];
const SHARED_ITEM_PAGE_COMPONENTS = [
@@ -605,7 +527,6 @@ const DIRECTIVES = [
DragClickDirective,
DebounceDirective,
ClickOutsideDirective,
- AuthorityConfidenceStateDirective,
InListValidator,
AutoFocusDirective,
RoleDirective,
@@ -628,6 +549,7 @@ const DIRECTIVES = [
declarations: [
...PIPES,
...COMPONENTS,
+ ...ENTRY_COMPONENTS,
...DIRECTIVES,
...SHARED_ITEM_PAGE_COMPONENTS,
ItemVersionsSummaryModalComponent,
@@ -640,9 +562,10 @@ const DIRECTIVES = [
...MODULES,
...PIPES,
...COMPONENTS,
+ ...ENTRY_COMPONENTS,
...SHARED_ITEM_PAGE_COMPONENTS,
...DIRECTIVES,
- TranslateModule
+ TranslateModule,
]
})
diff --git a/src/app/shared/sidebar/sidebar-effects.service.ts b/src/app/shared/sidebar/sidebar-effects.service.ts
index ba53e2fec9..f6f99ca0fc 100644
--- a/src/app/shared/sidebar/sidebar-effects.service.ts
+++ b/src/app/shared/sidebar/sidebar-effects.service.ts
@@ -1,7 +1,7 @@
import { map, tap, filter } from 'rxjs/operators';
import { Injectable } from '@angular/core';
import { createEffect, Actions, ofType } from '@ngrx/effects';
-import * as fromRouter from '@ngrx/router-store';
+import { ROUTER_NAVIGATION } from '@ngrx/router-store';
import { SidebarCollapseAction } from './sidebar.actions';
import { URLBaser } from '../../core/url-baser/url-baser';
@@ -14,7 +14,7 @@ export class SidebarEffects {
private previousPath: string;
routeChange$ = createEffect(() => this.actions$
.pipe(
- ofType(fromRouter.ROUTER_NAVIGATION),
+ ofType(ROUTER_NAVIGATION),
filter((action) => this.previousPath !== this.getBaseUrl(action)),
tap((action) => {
this.previousPath = this.getBaseUrl(action);
diff --git a/src/app/shared/sidebar/sidebar.reducer.spec.ts b/src/app/shared/sidebar/sidebar.reducer.spec.ts
index 796c40537c..76962f60c1 100644
--- a/src/app/shared/sidebar/sidebar.reducer.spec.ts
+++ b/src/app/shared/sidebar/sidebar.reducer.spec.ts
@@ -1,3 +1,4 @@
+// eslint-disable-next-line import/no-namespace
import * as deepFreeze from 'deep-freeze';
import { sidebarReducer } from './sidebar.reducer';
diff --git a/src/app/shared/starts-with/date/starts-with-date.component.spec.ts b/src/app/shared/starts-with/date/starts-with-date.component.spec.ts
index 3cd22a625f..2407f21fdf 100644
--- a/src/app/shared/starts-with/date/starts-with-date.component.spec.ts
+++ b/src/app/shared/starts-with/date/starts-with-date.component.spec.ts
@@ -11,11 +11,8 @@ import { StartsWithDateComponent } from './starts-with-date.component';
import { ActivatedRouteStub } from '../../testing/active-router.stub';
import { EnumKeysPipe } from '../../utils/enum-keys-pipe';
import { RouterStub } from '../../testing/router.stub';
-import { PaginationComponentOptions } from '../../pagination/pagination-component-options.model';
-import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model';
import { PaginationService } from '../../../core/pagination/pagination.service';
import { PaginationServiceStub } from '../../testing/pagination-service.stub';
-import { FindListOptions } from '../../../core/data/find-list-options.model';
describe('StartsWithDateComponent', () => {
let comp: StartsWithDateComponent;
diff --git a/src/app/shared/starts-with/text/starts-with-text.component.spec.ts b/src/app/shared/starts-with/text/starts-with-text.component.spec.ts
index c08ef5cfdc..b717c72d76 100644
--- a/src/app/shared/starts-with/text/starts-with-text.component.spec.ts
+++ b/src/app/shared/starts-with/text/starts-with-text.component.spec.ts
@@ -1,5 +1,5 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
-import { ActivatedRoute, NavigationExtras, Router } from '@angular/router';
+import { ActivatedRoute, Router } from '@angular/router';
import { CommonModule } from '@angular/common';
import { RouterTestingModule } from '@angular/router/testing';
import { TranslateModule } from '@ngx-translate/core';
@@ -8,12 +8,8 @@ import { EnumKeysPipe } from '../../utils/enum-keys-pipe';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { By } from '@angular/platform-browser';
import { StartsWithTextComponent } from './starts-with-text.component';
-import { PaginationComponentOptions } from '../../pagination/pagination-component-options.model';
-import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model';
-import { of as observableOf } from 'rxjs';
import { PaginationService } from '../../../core/pagination/pagination.service';
import { PaginationServiceStub } from '../../testing/pagination-service.stub';
-import { FindListOptions } from '../../../core/data/find-list-options.model';
describe('StartsWithTextComponent', () => {
let comp: StartsWithTextComponent;
diff --git a/src/app/shared/testing/css-variable-service.stub.ts b/src/app/shared/testing/css-variable-service.stub.ts
index 6159d89655..2f5c647945 100644
--- a/src/app/shared/testing/css-variable-service.stub.ts
+++ b/src/app/shared/testing/css-variable-service.stub.ts
@@ -1,10 +1,11 @@
import { Observable, of as observableOf } from 'rxjs';
+import { KeyValuePair } from '../key-value-pair.model';
const variables = {
- smMin: '576px,',
- mdMin: '768px,',
- lgMin: '992px',
- xlMin: '1200px',
+ '--bs-sm-min': '576px,',
+ '--bs-md-min': '768px,',
+ '--bs-lg-min': '992px',
+ '--bs-xl-min': '1200px',
} as any;
export class CSSVariableServiceStub {
@@ -19,4 +20,16 @@ export class CSSVariableServiceStub {
addCSSVariable(name: string, value: string): void {
/**/
}
+
+ addCSSVariables(variablesToAdd: KeyValuePair[]): void {
+ /**/
+ }
+
+ clearCSSVariables(): void {
+ /**/
+ }
+
+ getCSSVariablesFromStylesheets(document: Document): void {
+ /**/
+ }
}
diff --git a/src/app/shared/testing/group-mock.ts b/src/app/shared/testing/group-mock.ts
index a6db4c922e..0d6f924c01 100644
--- a/src/app/shared/testing/group-mock.ts
+++ b/src/app/shared/testing/group-mock.ts
@@ -1,6 +1,5 @@
import { Group } from '../../core/eperson/models/group.model';
import { EPersonMock } from './eperson.mock';
-import { of } from 'rxjs';
import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils';
export const GroupMock2: Group = Object.assign(new Group(), {
diff --git a/src/app/shared/theme-support/themed.component.spec.ts b/src/app/shared/theme-support/themed.component.spec.ts
index 404e970a7a..7776e60379 100644
--- a/src/app/shared/theme-support/themed.component.spec.ts
+++ b/src/app/shared/theme-support/themed.component.spec.ts
@@ -71,6 +71,12 @@ describe('ThemedComponent', () => {
expect((component as any).compRef.instance.testInput).toEqual('changed');
});
}));
+
+ it(`should set usedTheme to the name of the matched theme`, waitForAsync(() => {
+ fixture.whenStable().then(() => {
+ expect(component.usedTheme).toEqual('custom');
+ });
+ }));
});
describe('when the current theme doesn\'t match a themed component', () => {
@@ -92,6 +98,12 @@ describe('ThemedComponent', () => {
expect((component as any).compRef.instance.testInput).toEqual('changed');
});
}));
+
+ it(`should set usedTheme to the name of the base theme`, waitForAsync(() => {
+ fixture.whenStable().then(() => {
+ expect(component.usedTheme).toEqual('base');
+ });
+ }));
});
describe('and it extends another theme', () => {
@@ -117,6 +129,12 @@ describe('ThemedComponent', () => {
expect((component as any).compRef.instance.testInput).toEqual('changed');
});
}));
+
+ it(`should set usedTheme to the name of the base theme`, waitForAsync(() => {
+ fixture.whenStable().then(() => {
+ expect(component.usedTheme).toEqual('base');
+ });
+ }));
});
describe('that does match it', () => {
@@ -141,6 +159,12 @@ describe('ThemedComponent', () => {
expect((component as any).compRef.instance.testInput).toEqual('changed');
});
}));
+
+ it(`should set usedTheme to the name of the matched theme`, waitForAsync(() => {
+ fixture.whenStable().then(() => {
+ expect(component.usedTheme).toEqual('custom');
+ });
+ }));
});
describe('that extends another theme that doesn\'t match it either', () => {
@@ -167,6 +191,12 @@ describe('ThemedComponent', () => {
expect((component as any).compRef.instance.testInput).toEqual('changed');
});
}));
+
+ it(`should set usedTheme to the name of the base theme`, waitForAsync(() => {
+ fixture.whenStable().then(() => {
+ expect(component.usedTheme).toEqual('base');
+ });
+ }));
});
describe('that extends another theme that does match it', () => {
@@ -193,6 +223,12 @@ describe('ThemedComponent', () => {
expect((component as any).compRef.instance.testInput).toEqual('changed');
});
}));
+
+ it(`should set usedTheme to the name of the matched theme`, waitForAsync(() => {
+ fixture.whenStable().then(() => {
+ expect(component.usedTheme).toEqual('custom');
+ });
+ }));
});
});
});
diff --git a/src/app/shared/theme-support/themed.component.ts b/src/app/shared/theme-support/themed.component.ts
index 87f182a5ff..995122d284 100644
--- a/src/app/shared/theme-support/themed.component.ts
+++ b/src/app/shared/theme-support/themed.component.ts
@@ -8,13 +8,15 @@ import {
OnDestroy,
ComponentFactoryResolver,
ChangeDetectorRef,
- OnChanges
+ OnChanges,
+ HostBinding
} from '@angular/core';
import { hasValue, isNotEmpty } from '../empty.util';
-import { from as fromPromise, Observable, of as observableOf, Subscription } from 'rxjs';
+import { from as fromPromise, Observable, of as observableOf, Subscription, BehaviorSubject } from 'rxjs';
import { ThemeService } from './theme.service';
-import { catchError, switchMap, map } from 'rxjs/operators';
+import { catchError, switchMap, map, tap } from 'rxjs/operators';
import { GenericConstructor } from '../../core/shared/generic-constructor';
+import { BASE_THEME_NAME } from './theme.constants';
@Component({
selector: 'ds-themed',
@@ -25,11 +27,22 @@ export abstract class ThemedComponent implements OnInit, OnDestroy, OnChanges
@ViewChild('vcr', { read: ViewContainerRef }) vcr: ViewContainerRef;
protected compRef: ComponentRef;
+ /**
+ * A reference to the themed component. Will start as undefined and emit every time the themed
+ * component is rendered
+ */
+ public compRef$: BehaviorSubject> = new BehaviorSubject(undefined);
+
protected lazyLoadSub: Subscription;
protected themeSub: Subscription;
protected inAndOutputNames: (keyof T & keyof this)[] = [];
+ /**
+ * A data attribute on the ThemedComponent to indicate which theme the rendered component came from.
+ */
+ @HostBinding('attr.data-used-theme') usedTheme: string;
+
constructor(
protected resolver: ComponentFactoryResolver,
protected cdr: ChangeDetectorRef,
@@ -80,6 +93,7 @@ export abstract class ThemedComponent implements OnInit, OnDestroy, OnChanges
} else {
// otherwise import and return the default component
return fromPromise(this.importUnthemedComponent()).pipe(
+ tap(() => this.usedTheme = BASE_THEME_NAME),
map((unthemedFile: any) => {
return unthemedFile[this.getComponentName()];
})
@@ -90,6 +104,7 @@ export abstract class ThemedComponent implements OnInit, OnDestroy, OnChanges
const factory = this.resolver.resolveComponentFactory(constructor);
this.compRef = this.vcr.createComponent(factory);
this.connectInputsAndOutputs();
+ this.compRef$.next(this.compRef);
this.cdr.markForCheck();
});
}
@@ -123,6 +138,7 @@ export abstract class ThemedComponent implements OnInit, OnDestroy, OnChanges
private resolveThemedComponent(themeName?: string, checkedThemeNames: string[] = []): Observable {
if (isNotEmpty(themeName)) {
return fromPromise(this.importThemedComponent(themeName)).pipe(
+ tap(() => this.usedTheme = themeName),
catchError(() => {
// Try the next ancestor theme instead
const nextTheme = this.themeService.getThemeConfigFor(themeName)?.extends;
diff --git a/src/app/shared/truncatable/truncatable.reducer.spec.ts b/src/app/shared/truncatable/truncatable.reducer.spec.ts
index 841ec5e367..9866f382f7 100644
--- a/src/app/shared/truncatable/truncatable.reducer.spec.ts
+++ b/src/app/shared/truncatable/truncatable.reducer.spec.ts
@@ -1,3 +1,4 @@
+// eslint-disable-next-line import/no-namespace
import * as deepFreeze from 'deep-freeze';
import { truncatableReducer } from './truncatable.reducer';
diff --git a/src/app/shared/file-dropzone-no-uploader/file-dropzone-no-uploader.component.html b/src/app/shared/upload/file-dropzone-no-uploader/file-dropzone-no-uploader.component.html
similarity index 100%
rename from src/app/shared/file-dropzone-no-uploader/file-dropzone-no-uploader.component.html
rename to src/app/shared/upload/file-dropzone-no-uploader/file-dropzone-no-uploader.component.html
diff --git a/src/app/shared/file-dropzone-no-uploader/file-dropzone-no-uploader.component.ts b/src/app/shared/upload/file-dropzone-no-uploader/file-dropzone-no-uploader.component.ts
similarity index 98%
rename from src/app/shared/file-dropzone-no-uploader/file-dropzone-no-uploader.component.ts
rename to src/app/shared/upload/file-dropzone-no-uploader/file-dropzone-no-uploader.component.ts
index 58960af19e..06636f4256 100644
--- a/src/app/shared/file-dropzone-no-uploader/file-dropzone-no-uploader.component.ts
+++ b/src/app/shared/upload/file-dropzone-no-uploader/file-dropzone-no-uploader.component.ts
@@ -1,5 +1,5 @@
import { Component, EventEmitter, HostListener, Input, OnInit, Output } from '@angular/core';
-import { uniqueId } from 'lodash';
+import uniqueId from 'lodash/uniqueId';
import { FileUploader } from 'ng2-file-upload';
import { Observable, of as observableOf } from 'rxjs';
import { UploaderOptions } from '../uploader/uploader-options.model';
diff --git a/src/app/shared/file-dropzone-no-uploader/file-dropzone-no-uploader.scss b/src/app/shared/upload/file-dropzone-no-uploader/file-dropzone-no-uploader.scss
similarity index 100%
rename from src/app/shared/file-dropzone-no-uploader/file-dropzone-no-uploader.scss
rename to src/app/shared/upload/file-dropzone-no-uploader/file-dropzone-no-uploader.scss
diff --git a/src/app/shared/upload/upload.module.ts b/src/app/shared/upload/upload.module.ts
new file mode 100644
index 0000000000..9f2895d7ac
--- /dev/null
+++ b/src/app/shared/upload/upload.module.ts
@@ -0,0 +1,38 @@
+/**
+ * The contents of this file are subject to the license and copyright
+ * detailed in the LICENSE and NOTICE files at the root of the source
+ * tree and available online at
+ *
+ * http://www.dspace.org/license/
+ */
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { SharedModule } from '../shared.module';
+import { FileUploadModule } from 'ng2-file-upload';
+import { UploaderComponent } from './uploader/uploader.component';
+import { FileDropzoneNoUploaderComponent } from './file-dropzone-no-uploader/file-dropzone-no-uploader.component';
+
+const COMPONENTS = [
+ UploaderComponent,
+ FileDropzoneNoUploaderComponent,
+];
+
+@NgModule({
+ imports: [
+ CommonModule,
+ SharedModule,
+ FileUploadModule,
+ ],
+ declarations: [
+ ...COMPONENTS,
+ ],
+ providers: [
+ ...COMPONENTS,
+ ],
+ exports: [
+ ...COMPONENTS,
+ FileUploadModule,
+ ]
+})
+export class UploadModule {
+}
diff --git a/src/app/shared/uploader/uploader-error.model.ts b/src/app/shared/upload/uploader/uploader-error.model.ts
similarity index 100%
rename from src/app/shared/uploader/uploader-error.model.ts
rename to src/app/shared/upload/uploader/uploader-error.model.ts
diff --git a/src/app/shared/uploader/uploader-options.model.ts b/src/app/shared/upload/uploader/uploader-options.model.ts
similarity index 86%
rename from src/app/shared/uploader/uploader-options.model.ts
rename to src/app/shared/upload/uploader/uploader-options.model.ts
index 959e5c3295..559fb0485b 100644
--- a/src/app/shared/uploader/uploader-options.model.ts
+++ b/src/app/shared/upload/uploader/uploader-options.model.ts
@@ -1,4 +1,4 @@
-import { RestRequestMethod } from '../../core/data/rest-request-method';
+import { RestRequestMethod } from '../../../core/data/rest-request-method';
export class UploaderOptions {
/**
diff --git a/src/app/shared/uploader/uploader-properties.model.ts b/src/app/shared/upload/uploader/uploader-properties.model.ts
similarity index 83%
rename from src/app/shared/uploader/uploader-properties.model.ts
rename to src/app/shared/upload/uploader/uploader-properties.model.ts
index bc0376b809..b84ae30bf8 100644
--- a/src/app/shared/uploader/uploader-properties.model.ts
+++ b/src/app/shared/upload/uploader/uploader-properties.model.ts
@@ -1,4 +1,4 @@
-import { MetadataMap } from '../../core/shared/metadata.models';
+import { MetadataMap } from '../../../core/shared/metadata.models';
/**
* Properties to send to the REST API for uploading a bitstream
diff --git a/src/app/shared/uploader/uploader.component.html b/src/app/shared/upload/uploader/uploader.component.html
similarity index 100%
rename from src/app/shared/uploader/uploader.component.html
rename to src/app/shared/upload/uploader/uploader.component.html
diff --git a/src/app/shared/uploader/uploader.component.scss b/src/app/shared/upload/uploader/uploader.component.scss
similarity index 100%
rename from src/app/shared/uploader/uploader.component.scss
rename to src/app/shared/upload/uploader/uploader.component.scss
diff --git a/src/app/shared/uploader/uploader.component.spec.ts b/src/app/shared/upload/uploader/uploader.component.spec.ts
similarity index 86%
rename from src/app/shared/uploader/uploader.component.spec.ts
rename to src/app/shared/upload/uploader/uploader.component.spec.ts
index 84fee2e147..8ea23c8acb 100644
--- a/src/app/shared/uploader/uploader.component.spec.ts
+++ b/src/app/shared/upload/uploader/uploader.component.spec.ts
@@ -4,16 +4,16 @@ import { ComponentFixture, inject, TestBed, waitForAsync, } from '@angular/core/
import { ScrollToService } from '@nicky-lenaers/ngx-scroll-to';
-import { UploaderService } from './uploader.service';
+import { DragService } from '../../../core/drag.service';
import { UploaderOptions } from './uploader-options.model';
import { UploaderComponent } from './uploader.component';
import { FileUploadModule } from 'ng2-file-upload';
import { TranslateModule } from '@ngx-translate/core';
-import { createTestComponent } from '../testing/utils.test';
+import { createTestComponent } from '../../testing/utils.test';
import { HttpXsrfTokenExtractor } from '@angular/common/http';
-import { CookieService } from '../../core/services/cookie.service';
-import { CookieServiceMock } from '../mocks/cookie.service.mock';
-import { HttpXsrfTokenExtractorMock } from '../mocks/http-xsrf-token-extractor.mock';
+import { CookieService } from '../../../core/services/cookie.service';
+import { CookieServiceMock } from '../../mocks/cookie.service.mock';
+import { HttpXsrfTokenExtractorMock } from '../../mocks/http-xsrf-token-extractor.mock';
describe('Chips component', () => {
@@ -37,7 +37,7 @@ describe('Chips component', () => {
ChangeDetectorRef,
ScrollToService,
UploaderComponent,
- UploaderService,
+ DragService,
{ provide: HttpXsrfTokenExtractor, useValue: new HttpXsrfTokenExtractorMock('mock-token') },
{ provide: CookieService, useValue: new CookieServiceMock() },
],
diff --git a/src/app/shared/uploader/uploader.component.ts b/src/app/shared/upload/uploader/uploader.component.ts
similarity index 92%
rename from src/app/shared/uploader/uploader.component.ts
rename to src/app/shared/upload/uploader/uploader.component.ts
index a0dd0e5bba..14b1ca9b94 100644
--- a/src/app/shared/uploader/uploader.component.ts
+++ b/src/app/shared/upload/uploader/uploader.component.ts
@@ -2,16 +2,16 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Ho
import { of as observableOf } from 'rxjs';
import { FileUploader } from 'ng2-file-upload';
-import { uniqueId } from 'lodash';
+import uniqueId from 'lodash/uniqueId';
import { ScrollToService } from '@nicky-lenaers/ngx-scroll-to';
import { UploaderOptions } from './uploader-options.model';
-import { hasValue, isNotEmpty, isUndefined } from '../empty.util';
-import { UploaderService } from './uploader.service';
+import { hasValue, isNotEmpty, isUndefined } from '../../empty.util';
import { UploaderProperties } from './uploader-properties.model';
import { HttpXsrfTokenExtractor } from '@angular/common/http';
-import { XSRF_COOKIE, XSRF_REQUEST_HEADER, XSRF_RESPONSE_HEADER } from '../../core/xsrf/xsrf.interceptor';
-import { CookieService } from '../../core/services/cookie.service';
+import { XSRF_COOKIE, XSRF_REQUEST_HEADER, XSRF_RESPONSE_HEADER } from '../../../core/xsrf/xsrf.interceptor';
+import { CookieService } from '../../../core/services/cookie.service';
+import { DragService } from '../../../core/drag.service';
@Component({
selector: 'ds-uploader',
@@ -76,7 +76,7 @@ export class UploaderComponent {
@HostListener('window:dragover', ['$event'])
onDragOver(event: any) {
- if (this.enableDragOverDocument && this.uploaderService.isAllowedDragOverPage()) {
+ if (this.enableDragOverDocument && this.dragService.isAllowedDragOverPage()) {
// Show drop area on the page
event.preventDefault();
if ((event.target as any).tagName !== 'HTML') {
@@ -85,9 +85,13 @@ export class UploaderComponent {
}
}
- constructor(private cdr: ChangeDetectorRef, private scrollToService: ScrollToService,
- private uploaderService: UploaderService, private tokenExtractor: HttpXsrfTokenExtractor,
- private cookieService: CookieService) {
+ constructor(
+ private cdr: ChangeDetectorRef,
+ private scrollToService: ScrollToService,
+ private dragService: DragService,
+ private tokenExtractor: HttpXsrfTokenExtractor,
+ private cookieService: CookieService
+ ) {
}
/**
diff --git a/src/app/shared/utils/file-size-pipe.ts b/src/app/shared/utils/file-size-pipe.ts
index 2d219cdaf4..934f3ee67a 100644
--- a/src/app/shared/utils/file-size-pipe.ts
+++ b/src/app/shared/utils/file-size-pipe.ts
@@ -1,4 +1,5 @@
import { Pipe, PipeTransform } from '@angular/core';
+// eslint-disable-next-line import/no-namespace
import * as fileSize from 'filesize';
/*
diff --git a/src/app/shared/utils/markdown.pipe.ts b/src/app/shared/utils/markdown.pipe.ts
index f7e1032cac..e494a82613 100644
--- a/src/app/shared/utils/markdown.pipe.ts
+++ b/src/app/shared/utils/markdown.pipe.ts
@@ -1,9 +1,14 @@
-import { Inject, InjectionToken, Pipe, PipeTransform } from '@angular/core';
-import MarkdownIt from 'markdown-it';
-import * as sanitizeHtml from 'sanitize-html';
+import { Inject, InjectionToken, Pipe, PipeTransform, SecurityContext } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { environment } from '../../../environments/environment';
+const markdownItLoader = async () => (await import('markdown-it')).default;
+type LazyMarkdownIt = ReturnType;
+const MARKDOWN_IT = new InjectionToken(
+ 'Lazily loaded MarkdownIt',
+ { providedIn: 'root', factory: markdownItLoader }
+);
+
const mathjaxLoader = async () => (await import('markdown-it-mathjax3')).default;
type Mathjax = ReturnType;
const MATHJAX = new InjectionToken(
@@ -11,6 +16,13 @@ const MATHJAX = new InjectionToken(
{ providedIn: 'root', factory: mathjaxLoader }
);
+const sanitizeHtmlLoader = async () => (await import('sanitize-html') as any).default;
+type SanitizeHtml = ReturnType;
+const SANITIZE_HTML = new InjectionToken(
+ 'Lazily loaded sanitize-html',
+ { providedIn: 'root', factory: sanitizeHtmlLoader }
+);
+
/**
* Pipe for rendering markdown and mathjax.
* - markdown will only be rendered if {@link MarkdownConfig#enabled} is true
@@ -31,7 +43,9 @@ export class MarkdownPipe implements PipeTransform {
constructor(
protected sanitizer: DomSanitizer,
+ @Inject(MARKDOWN_IT) private markdownIt: LazyMarkdownIt,
@Inject(MATHJAX) private mathjax: Mathjax,
+ @Inject(SANITIZE_HTML) private sanitizeHtml: SanitizeHtml,
) {
}
@@ -39,15 +53,17 @@ export class MarkdownPipe implements PipeTransform {
if (!environment.markdown.enabled) {
return value;
}
+ const MarkdownIt = await this.markdownIt;
const md = new MarkdownIt({
html: true,
linkify: true,
});
+
+ let html: string;
if (environment.markdown.mathjax) {
md.use(await this.mathjax);
- }
- return this.sanitizer.bypassSecurityTrustHtml(
- sanitizeHtml(md.render(value), {
+ const sanitizeHtml = await this.sanitizeHtml;
+ html = sanitizeHtml(md.render(value), {
// sanitize-html doesn't let through SVG by default, so we extend its allowlists to cover MathJax SVG
allowedTags: [
...sanitizeHtml.defaults.allowedTags,
@@ -77,7 +93,11 @@ export class MarkdownPipe implements PipeTransform {
parser: {
lowerCaseAttributeNames: false,
},
- })
- );
+ });
+ } else {
+ html = this.sanitizer.sanitize(SecurityContext.HTML, md.render(value));
+ }
+
+ return this.sanitizer.bypassSecurityTrustHtml(html);
}
}
diff --git a/src/app/statistics-page/statistics-table/statistics-table.component.html b/src/app/statistics-page/statistics-table/statistics-table.component.html
index 3ecd256812..fb042b25c3 100644
--- a/src/app/statistics-page/statistics-table/statistics-table.component.html
+++ b/src/app/statistics-page/statistics-table/statistics-table.component.html
@@ -10,7 +10,7 @@
-
+