1
0

Merge branch 'main' into CST-7694

This commit is contained in:
corrado lombardi
2022-12-23 14:47:14 +01:00
145 changed files with 11801 additions and 11965 deletions

View File

@@ -1,7 +1,7 @@
## References ## References
_Add references/links to any related issues or PRs. These may include:_ _Add references/links to any related issues or PRs. These may include:_
* Fixes #[issue-number] * Fixes #`issue-number` (if this fixes an issue ticket)
* Requires DSpace/DSpace#[pr-number] (if a REST API PR is required to test this) * Requires DSpace/DSpace#`pr-number` (if a REST API PR is required to test this)
## Description ## Description
Short summary of changes (1-2 sentences). 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!_ _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 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 passes [ESLint](https://eslint.org/) validation using `yarn lint`
- [ ] My PR doesn't introduce circular dependencies - [ ] 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 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). - [ ] 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).

View File

@@ -15,17 +15,19 @@ jobs:
env: env:
# The ci step will test the dspace-angular code against DSpace REST. # 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. # 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_PORT: 8080
DSPACE_REST_NAMESPACE: '/server' DSPACE_REST_NAMESPACE: '/server'
DSPACE_REST_SSL: false 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 # When Chrome version is specified, we pin to a specific version of Chrome
# Comment this out to use the latest release # Comment this out to use the latest release
#CHROME_VERSION: "90.0.4430.212-1" #CHROME_VERSION: "90.0.4430.212-1"
strategy: strategy:
# Create a matrix of Node versions to test against (in parallel) # Create a matrix of Node versions to test against (in parallel)
matrix: matrix:
node-version: [14.x, 16.x] node-version: [16.x, 18.x]
# Do NOT exit immediately if one matrix job fails # Do NOT exit immediately if one matrix job fails
fail-fast: false fail-fast: false
# These are the actual CI steps to perform per job # These are the actual CI steps to perform per job
@@ -112,7 +114,7 @@ jobs:
start: yarn run serve:ssr start: yarn run serve:ssr
# Wait for backend & frontend to be available # Wait for backend & frontend to be available
# NOTE: We use the 'sites' REST endpoint to also ensure the database is ready # 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 for 2 mins max for everything to respond
wait-on-timeout: 120 wait-on-timeout: 120
@@ -147,7 +149,7 @@ jobs:
run: | run: |
nohup yarn run serve:ssr & nohup yarn run serve:ssr &
printf 'Waiting for app to start' 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 '.' printf '.'
sleep 2 sleep 2
done done
@@ -158,7 +160,7 @@ jobs:
# This step also prints entire HTML of homepage for easier debugging if grep fails. # This step also prints entire HTML of homepage for easier debugging if grep fails.
- name: Verify SSR (server-side rendering) - name: Verify SSR (server-side rendering)
run: | run: |
result=$(wget -O- -q http://localhost:4000/home) result=$(wget -O- -q http://127.0.0.1:4000/home)
echo "$result" echo "$result"
echo "$result" | grep -oE "<meta name=\"title\" [^>]*>" | grep DSpace echo "$result" | grep -oE "<meta name=\"title\" [^>]*>" | grep DSpace

49
.github/workflows/codescan.yml vendored Normal file
View File

@@ -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

46
CONTRIBUTING.md Normal file
View File

@@ -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.

View File

@@ -1,11 +1,15 @@
# This image will be published as dspace/dspace-angular # This image will be published as dspace/dspace-angular
# See https://github.com/DSpace/dspace-angular/tree/main/docker for usage details # See https://github.com/DSpace/dspace-angular/tree/main/docker for usage details
FROM node:14-alpine FROM node:18-alpine
WORKDIR /app WORKDIR /app
ADD . /app/ ADD . /app/
EXPOSE 4000 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 # 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 # See, for example https://github.com/yarnpkg/yarn/issues/5540
RUN yarn install --network-timeout 300000 RUN yarn install --network-timeout 300000

View File

@@ -35,7 +35,7 @@ https://wiki.lyrasis.org/display/DSDOC7x/Installing+DSpace
Quick start 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 ```bash
# clone the repo # clone the repo
@@ -90,7 +90,7 @@ Requirements
------------ ------------
- [Node.js](https://nodejs.org) and [yarn](https://yarnpkg.com) - [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. 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.
@@ -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) - [Sublime Text](http://www.sublimetext.com/3)
- [Typescript-Sublime-Plugin](https://github.com/Microsoft/Typescript-Sublime-plugin#installation) - [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 File Structure
-------------- --------------

View File

@@ -25,12 +25,10 @@
} }
}, },
"allowedCommonJsDependencies": [ "allowedCommonJsDependencies": [
"angular2-text-mask",
"cerialize", "cerialize",
"core-js", "core-js",
"lodash", "lodash",
"jwt-decode", "jwt-decode",
"url-parse",
"uuid", "uuid",
"webfontloader", "webfontloader",
"zone.js" "zone.js"

View File

@@ -55,6 +55,8 @@ auth:
# Form settings # Form settings
form: form:
# Sets the spellcheck textarea attribute value
spellCheck: true
# NOTE: Map server-side validators to comparative Angular form validators # NOTE: Map server-side validators to comparative Angular form validators
validatorMap: validatorMap:
required: required required: required
@@ -143,6 +145,9 @@ languages:
- code: nl - code: nl
label: Nederlands label: Nederlands
active: true active: true
- code: pl
label: Polski
active: true
- code: pt-PT - code: pt-PT
label: Português label: Português
active: true active: true
@@ -170,6 +175,10 @@ languages:
- code: el - code: el
label: Ελληνικά label: Ελληνικά
active: true active: true
- code: uk
label: раї́нська
active: true
# Browse-By Pages # Browse-By Pages
browseBy: browseBy:
@@ -207,6 +216,11 @@ item:
undoTimeout: 10000 # 10 seconds undoTimeout: 10000 # 10 seconds
# Show the item access status label in items lists # Show the item access status label in items lists
showAccessStatuses: false 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 Page Config
collection: collection:

View File

@@ -5,7 +5,7 @@
"screenshotsFolder": "cypress/screenshots", "screenshotsFolder": "cypress/screenshots",
"pluginsFile": "cypress/plugins/index.ts", "pluginsFile": "cypress/plugins/index.ts",
"fixturesFolder": "cypress/fixtures", "fixturesFolder": "cypress/fixtures",
"baseUrl": "http://localhost:4000", "baseUrl": "http://127.0.0.1:4000",
"retries": { "retries": {
"runMode": 2, "runMode": 2,
"openMode": 0 "openMode": 0

View File

@@ -24,8 +24,8 @@ services:
# __D__ => "-" (e.g. google__D__metadata => google-metadata) # __D__ => "-" (e.g. google__D__metadata => google-metadata)
# dspace.dir, dspace.server.url and dspace.ui.url # dspace.dir, dspace.server.url and dspace.ui.url
dspace__P__dir: /dspace dspace__P__dir: /dspace
dspace__P__server__P__url: http://localhost:8080/server dspace__P__server__P__url: http://127.0.0.1:8080/server
dspace__P__ui__P__url: http://localhost:4000 dspace__P__ui__P__url: http://127.0.0.1:4000
# db.url: Ensure we are using the 'dspacedb' image for our database # db.url: Ensure we are using the 'dspacedb' image for our database
db__P__url: 'jdbc:postgresql://dspacedb:5432/dspace' db__P__url: 'jdbc:postgresql://dspacedb:5432/dspace'
# solr.server: Ensure we are using the 'dspacesolr' image for Solr # solr.server: Ensure we are using the 'dspacesolr' image for Solr

View File

@@ -54,18 +54,18 @@
"ts-node": "10.2.1" "ts-node": "10.2.1"
}, },
"dependencies": { "dependencies": {
"@angular/animations": "~13.2.6", "@angular/animations": "~13.3.12",
"@angular/cdk": "^13.2.6", "@angular/cdk": "^13.2.6",
"@angular/common": "~13.2.6", "@angular/common": "~13.3.12",
"@angular/compiler": "~13.2.6", "@angular/compiler": "~13.3.12",
"@angular/core": "~13.2.6", "@angular/core": "~13.3.12",
"@angular/forms": "~13.2.6", "@angular/forms": "~13.3.12",
"@angular/localize": "13.2.6", "@angular/localize": "13.3.12",
"@angular/platform-browser": "~13.2.6", "@angular/platform-browser": "~13.3.12",
"@angular/platform-browser-dynamic": "~13.2.6", "@angular/platform-browser-dynamic": "~13.3.12",
"@angular/platform-server": "~13.2.6", "@angular/platform-server": "~13.3.12",
"@angular/router": "~13.2.6", "@angular/router": "~13.3.12",
"@babel/runtime": "^7.17.2", "@babel/runtime": "7.17.2",
"@kolkov/ngx-gallery": "^2.0.1", "@kolkov/ngx-gallery": "^2.0.1",
"@material-ui/core": "^4.11.0", "@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1", "@material-ui/icons": "^4.9.1",
@@ -77,15 +77,15 @@
"@ngrx/store": "^13.0.2", "@ngrx/store": "^13.0.2",
"@nguniversal/express-engine": "^13.0.2", "@nguniversal/express-engine": "^13.0.2",
"@ngx-translate/core": "^13.0.0", "@ngx-translate/core": "^13.0.0",
"@nicky-lenaers/ngx-scroll-to": "^9.0.0", "@nicky-lenaers/ngx-scroll-to": "^13.0.0",
"@types/grecaptcha": "^3.0.4", "@types/grecaptcha": "^3.0.4",
"angular-idle-preload": "3.0.0", "angular-idle-preload": "3.0.0",
"angulartics2": "^12.0.0", "angulartics2": "^12.0.0",
"axios": "^0.27.2", "axios": "^0.27.2",
"bootstrap": "4.3.1", "bootstrap": "^4.6.1",
"caniuse-lite": "^1.0.30001165",
"cerialize": "0.1.18", "cerialize": "0.1.18",
"cli-progress": "^3.8.0", "cli-progress": "^3.8.0",
"colors": "^1.4.0",
"compression": "^1.7.4", "compression": "^1.7.4",
"cookie-parser": "1.4.5", "cookie-parser": "1.4.5",
"core-js": "^3.7.0", "core-js": "^3.7.0",
@@ -95,11 +95,8 @@
"express": "^4.17.1", "express": "^4.17.1",
"express-rate-limit": "^5.1.3", "express-rate-limit": "^5.1.3",
"fast-json-patch": "^3.0.0-1", "fast-json-patch": "^3.0.0-1",
"file-saver": "^2.0.5",
"filesize": "^6.1.0", "filesize": "^6.1.0",
"font-awesome": "4.7.0",
"http-proxy-middleware": "^1.0.5", "http-proxy-middleware": "^1.0.5",
"https": "1.0.0",
"js-cookie": "2.2.1", "js-cookie": "2.2.1",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"json5": "^2.1.3", "json5": "^2.1.3",
@@ -119,43 +116,38 @@
"ngx-infinite-scroll": "^10.0.1", "ngx-infinite-scroll": "^10.0.1",
"ngx-pagination": "5.0.0", "ngx-pagination": "5.0.0",
"ngx-sortablejs": "^11.1.0", "ngx-sortablejs": "^11.1.0",
"ngx-ui-switch": "^11.0.1", "ngx-ui-switch": "^13.0.2",
"nouislider": "^14.6.3", "nouislider": "^14.6.3",
"pem": "1.14.4", "pem": "1.14.4",
"postcss-cli": "^9.1.0",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"react-copy-to-clipboard": "^5.0.1", "react-copy-to-clipboard": "^5.0.1",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rxjs": "^7.5.5", "rxjs": "^7.5.5",
"sanitize-html": "^2.7.2", "sanitize-html": "^2.7.2",
"sortablejs": "1.13.0", "sortablejs": "1.13.0",
"tslib": "^2.0.0",
"url-parse": "^1.5.6",
"uuid": "^8.3.2", "uuid": "^8.3.2",
"webfontloader": "1.6.28", "webfontloader": "1.6.28",
"zone.js": "~0.11.5" "zone.js": "~0.11.5"
}, },
"devDependencies": { "devDependencies": {
"@angular-builders/custom-webpack": "~13.1.0", "@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/builder": "13.1.0",
"@angular-eslint/eslint-plugin": "13.1.0", "@angular-eslint/eslint-plugin": "13.1.0",
"@angular-eslint/eslint-plugin-template": "13.1.0", "@angular-eslint/eslint-plugin-template": "13.1.0",
"@angular-eslint/schematics": "13.1.0", "@angular-eslint/schematics": "13.1.0",
"@angular-eslint/template-parser": "13.1.0", "@angular-eslint/template-parser": "13.1.0",
"@angular/cli": "~13.2.6", "@angular/cli": "~13.3.10",
"@angular/compiler-cli": "~13.2.6", "@angular/compiler-cli": "~13.3.12",
"@angular/language-service": "~13.2.6", "@angular/language-service": "~13.3.12",
"@cypress/schematic": "^1.5.0", "@cypress/schematic": "^1.5.0",
"@fortawesome/fontawesome-free": "^5.5.0", "@fortawesome/fontawesome-free": "^6.2.1",
"@ngrx/store-devtools": "^13.0.2", "@ngrx/store-devtools": "^13.0.2",
"@ngtools/webpack": "^13.2.6", "@ngtools/webpack": "^13.2.6",
"@nguniversal/builders": "^13.0.2", "@nguniversal/builders": "^13.1.1",
"@types/deep-freeze": "0.1.2", "@types/deep-freeze": "0.1.2",
"@types/express": "^4.17.9", "@types/express": "^4.17.9",
"@types/file-saver": "^2.0.1",
"@types/jasmine": "~3.6.0", "@types/jasmine": "~3.6.0",
"@types/jasminewd2": "~2.0.8",
"@types/js-cookie": "2.2.6", "@types/js-cookie": "2.2.6",
"@types/lodash": "^4.14.165", "@types/lodash": "^4.14.165",
"@types/node": "^14.14.9", "@types/node": "^14.14.9",
@@ -166,26 +158,18 @@
"compression-webpack-plugin": "^9.2.0", "compression-webpack-plugin": "^9.2.0",
"copy-webpack-plugin": "^6.4.1", "copy-webpack-plugin": "^6.4.1",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"css-loader": "^6.2.0",
"css-minimizer-webpack-plugin": "^3.4.1",
"cssnano": "^5.0.6",
"cypress": "9.7.0", "cypress": "9.7.0",
"cypress-axe": "^0.14.0", "cypress-axe": "^0.14.0",
"debug-loader": "^0.0.1",
"deep-freeze": "0.0.1", "deep-freeze": "0.0.1",
"dotenv": "^8.2.0",
"eslint": "^8.2.0", "eslint": "^8.2.0",
"eslint-plugin-deprecation": "^1.3.2", "eslint-plugin-deprecation": "^1.3.2",
"eslint-plugin-import": "^2.25.4", "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-lodash": "^7.4.0",
"eslint-plugin-unused-imports": "^2.0.0", "eslint-plugin-unused-imports": "^2.0.0",
"express-static-gzip": "^2.1.5", "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-core": "^3.8.0",
"jasmine-marbles": "0.9.2", "jasmine-marbles": "0.9.2",
"jasmine-spec-reporter": "~5.0.0",
"karma": "^6.3.14", "karma": "^6.3.14",
"karma-chrome-launcher": "~3.1.0", "karma-chrome-launcher": "~3.1.0",
"karma-coverage-istanbul-reporter": "~3.0.2", "karma-coverage-istanbul-reporter": "~3.0.2",
@@ -193,26 +177,20 @@
"karma-jasmine-html-reporter": "^1.5.0", "karma-jasmine-html-reporter": "^1.5.0",
"karma-mocha-reporter": "2.2.5", "karma-mocha-reporter": "2.2.5",
"ngx-mask": "^13.1.7", "ngx-mask": "^13.1.7",
"nodemon": "^2.0.15", "nodemon": "^2.0.20",
"postcss": "^8.1", "postcss": "^8.1",
"postcss-apply": "0.12.0", "postcss-apply": "0.12.0",
"postcss-import": "^14.0.0", "postcss-import": "^14.0.0",
"postcss-loader": "^4.0.3", "postcss-loader": "^4.0.3",
"postcss-preset-env": "^7.4.2", "postcss-preset-env": "^7.4.2",
"postcss-responsive-type": "1.0.0", "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": "^16.14.0",
"react-dom": "^16.14.0", "react-dom": "^16.14.0",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"rxjs-spy": "^8.0.2", "rxjs-spy": "^8.0.2",
"sass": "~1.32.6", "sass": "~1.33.0",
"sass-loader": "^12.6.0", "sass-loader": "^12.6.0",
"sass-resources-loader": "^2.1.1", "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", "ts-node": "^8.10.2",
"typescript": "~4.5.5", "typescript": "~4.5.5",
"webpack": "^5.69.1", "webpack": "^5.69.1",

View File

@@ -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 { SubgroupsListComponent } from './group-registry/group-form/subgroup-list/subgroups-list.component';
import { GroupsRegistryComponent } from './group-registry/groups-registry.component'; import { GroupsRegistryComponent } from './group-registry/groups-registry.component';
import { FormModule } from '../shared/form/form.module'; 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({ @NgModule({
imports: [ imports: [
@@ -26,6 +36,12 @@ import { FormModule } from '../shared/form/form.module';
GroupFormComponent, GroupFormComponent,
SubgroupsListComponent, SubgroupsListComponent,
MembersListComponent MembersListComponent
],
providers: [
{
provide: DYNAMIC_ERROR_MESSAGES_MATCHER,
useValue: ValidateEmailErrorStateMatcher
},
] ]
}) })
/** /**

View File

@@ -46,6 +46,7 @@ import { followLink } from '../../../shared/utils/follow-link-config.model';
import { NoContent } from '../../../core/shared/NoContent.model'; import { NoContent } from '../../../core/shared/NoContent.model';
import { Operation } from 'fast-json-patch'; import { Operation } from 'fast-json-patch';
import { ValidateGroupExists } from './validators/group-exists.validator'; import { ValidateGroupExists } from './validators/group-exists.validator';
import { environment } from '../../../../environments/environment';
@Component({ @Component({
selector: 'ds-group-form', selector: 'ds-group-form',
@@ -194,6 +195,7 @@ export class GroupFormComponent implements OnInit, OnDestroy {
label: groupDescription, label: groupDescription,
name: 'groupDescription', name: 'groupDescription',
required: false, required: false,
spellCheck: environment.form.spellCheck,
}); });
this.formModel = [ this.formModel = [
this.groupName, this.groupName,

View File

@@ -15,6 +15,7 @@ import { Router } from '@angular/router';
import { hasValue, isEmpty } from '../../../../shared/empty.util'; import { hasValue, isEmpty } from '../../../../shared/empty.util';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { getBitstreamFormatsModuleRoute } from '../../admin-registries-routing-paths'; 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 * The component responsible for rendering the form to create/edit a bitstream format
@@ -90,6 +91,7 @@ export class FormatFormComponent implements OnInit {
name: 'description', name: 'description',
label: 'admin.registries.bitstream-formats.edit.description.label', label: 'admin.registries.bitstream-formats.edit.description.label',
hint: 'admin.registries.bitstream-formats.edit.description.hint', hint: 'admin.registries.bitstream-formats.edit.description.hint',
spellCheck: environment.form.spellCheck,
}), }),
new DynamicSelectModel({ new DynamicSelectModel({

View File

@@ -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 { 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 { 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 { BatchImportPageComponent } from './admin-import-batch-page/batch-import-page.component';
import { UploadModule } from '../shared/upload/upload.module';
const ENTRY_COMPONENTS = [ const ENTRY_COMPONENTS = [
// put only entry components that use custom decorator // put only entry components that use custom decorator
@@ -25,7 +26,8 @@ const ENTRY_COMPONENTS = [
AccessControlModule, AccessControlModule,
AdminSearchModule.withEntryComponents(), AdminSearchModule.withEntryComponents(),
AdminWorkflowModuleModule.withEntryComponents(), AdminWorkflowModuleModule.withEntryComponents(),
SharedModule SharedModule,
UploadModule,
], ],
declarations: [ declarations: [
AdminCurationTasksComponent, AdminCurationTasksComponent,

View File

@@ -1,14 +1,12 @@
import { APP_BASE_HREF, CommonModule, DOCUMENT } from '@angular/common'; import { APP_BASE_HREF, CommonModule, DOCUMENT } from '@angular/common';
import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http'; import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { AbstractControl } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser'; import { BrowserModule } from '@angular/platform-browser';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { EffectsModule } from '@ngrx/effects'; import { EffectsModule } from '@ngrx/effects';
import { RouterStateSerializer, StoreRouterConnectingModule } from '@ngrx/router-store'; import { RouterStateSerializer, StoreRouterConnectingModule } from '@ngrx/router-store';
import { MetaReducer, StoreModule, USER_PROVIDED_META_REDUCERS } from '@ngrx/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 { TranslateModule } from '@ngx-translate/core';
import { ScrollToModule } from '@nicky-lenaers/ngx-scroll-to'; import { ScrollToModule } from '@nicky-lenaers/ngx-scroll-to';
import { AppRoutingModule } from './app-routing.module'; 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 { LogInterceptor } from './core/log/log.interceptor';
import { EagerThemesModule } from '../themes/eager-themes.module'; import { EagerThemesModule } from '../themes/eager-themes.module';
import { APP_CONFIG, AppConfig } from '../config/app-config.interface'; import { APP_CONFIG, AppConfig } from '../config/app-config.interface';
import { NgxMaskModule } from 'ngx-mask';
import { StoreDevModules } from '../config/store/devtools'; import { StoreDevModules } from '../config/store/devtools';
import { RootModule } from './root.module'; import { RootModule } from './root.module';
@@ -46,14 +43,6 @@ export function getMetaReducers(appConfig: AppConfig): MetaReducer<AppState>[] {
return appConfig.debug ? [...appMetaReducers, ...debugMetaReducers] : appMetaReducers; 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 = [ const IMPORTS = [
CommonModule, CommonModule,
SharedModule, SharedModule,
@@ -64,7 +53,6 @@ const IMPORTS = [
ScrollToModule.forRoot(), ScrollToModule.forRoot(),
NgbModule, NgbModule,
TranslateModule.forRoot(), TranslateModule.forRoot(),
NgxMaskModule.forRoot(),
EffectsModule.forRoot(appEffects), EffectsModule.forRoot(appEffects),
StoreModule.forRoot(appReducers, storeModuleConfig), StoreModule.forRoot(appReducers, storeModuleConfig),
StoreRouterConnectingModule.forRoot(), StoreRouterConnectingModule.forRoot(),
@@ -113,11 +101,6 @@ const PROVIDERS = [
useClass: LogInterceptor, useClass: LogInterceptor,
multi: true multi: true
}, },
{
provide: DYNAMIC_ERROR_MESSAGES_MATCHER,
useValue: ValidateEmailErrorStateMatcher
},
...DYNAMIC_MATCHER_PROVIDERS,
]; ];
const DECLARATIONS = [ const DECLARATIONS = [

View File

@@ -10,7 +10,7 @@
</nav> </nav>
<ng-template #breadcrumb let-text="text" let-url="url"> <ng-template #breadcrumb let-text="text" let-url="url">
<li class="breadcrumb-item"><div class="breadcrumb-item-limiter"><a [routerLink]="url" class="text-truncate">{{text | translate}}</a></div></li> <li class="breadcrumb-item"><div class="breadcrumb-item-limiter"><a [routerLink]="url" class="text-truncate" [ngbTooltip]="text | translate" placement="bottom" >{{text | translate}}</a></div></li>
</ng-template> </ng-template>
<ng-template #activeBreadcrumb let-text="text"> <ng-template #activeBreadcrumb let-text="text">

View File

@@ -1,5 +1,6 @@
import { DynamicFormControlModel, DynamicInputModel, DynamicTextAreaModel } from '@ng-dynamic-forms/core'; import { DynamicFormControlModel, DynamicInputModel, DynamicTextAreaModel } from '@ng-dynamic-forms/core';
import { DynamicSelectModelConfig } from '@ng-dynamic-forms/core/lib/model/select/dynamic-select.model'; import { DynamicSelectModelConfig } from '@ng-dynamic-forms/core/lib/model/select/dynamic-select.model';
import { environment } from '../../../environments/environment';
export const collectionFormEntityTypeSelectionConfig: DynamicSelectModelConfig<string> = { export const collectionFormEntityTypeSelectionConfig: DynamicSelectModelConfig<string> = {
id: 'entityType', id: 'entityType',
@@ -26,21 +27,26 @@ export const collectionFormModels: DynamicFormControlModel[] = [
new DynamicTextAreaModel({ new DynamicTextAreaModel({
id: 'description', id: 'description',
name: 'dc.description', name: 'dc.description',
spellCheck: environment.form.spellCheck,
}), }),
new DynamicTextAreaModel({ new DynamicTextAreaModel({
id: 'abstract', id: 'abstract',
name: 'dc.description.abstract', name: 'dc.description.abstract',
spellCheck: environment.form.spellCheck,
}), }),
new DynamicTextAreaModel({ new DynamicTextAreaModel({
id: 'rights', id: 'rights',
name: 'dc.rights', name: 'dc.rights',
spellCheck: environment.form.spellCheck,
}), }),
new DynamicTextAreaModel({ new DynamicTextAreaModel({
id: 'tableofcontents', id: 'tableofcontents',
name: 'dc.description.tableofcontents', name: 'dc.description.tableofcontents',
spellCheck: environment.form.spellCheck,
}), }),
new DynamicTextAreaModel({ new DynamicTextAreaModel({
id: 'license', id: 'license',
name: 'dc.rights.license', name: 'dc.rights.license',
spellCheck: environment.form.spellCheck,
}) })
]; ];

View File

@@ -72,6 +72,7 @@ import { MenuItemType } from '../shared/menu/menu-item-type.model';
id: 'statistics_collection_:id', id: 'statistics_collection_:id',
active: true, active: true,
visible: true, visible: true,
index: 2,
model: { model: {
type: MenuItemType.LINK, type: MenuItemType.LINK,
text: 'menu.section.statistics', text: 'menu.section.statistics',

View File

@@ -25,7 +25,7 @@ import { ComcolModule } from '../shared/comcol/comcol.module';
StatisticsModule.forRoot(), StatisticsModule.forRoot(),
EditItemPageModule, EditItemPageModule,
CollectionFormModule, CollectionFormModule,
ComcolModule ComcolModule,
], ],
declarations: [ declarations: [
CollectionPageComponent, CollectionPageComponent,
@@ -38,7 +38,7 @@ import { ComcolModule } from '../shared/comcol/comcol.module';
], ],
providers: [ providers: [
SearchService, SearchService,
] ],
}) })
export class CollectionPageModule { export class CollectionPageModule {

View File

@@ -25,7 +25,7 @@ import { ComcolModule } from '../../shared/comcol/comcol.module';
CollectionFormModule, CollectionFormModule,
ResourcePoliciesModule, ResourcePoliciesModule,
FormModule, FormModule,
ComcolModule ComcolModule,
], ],
declarations: [ declarations: [
EditCollectionPageComponent, EditCollectionPageComponent,

View File

@@ -13,6 +13,7 @@ import { CommunityDataService } from '../../core/data/community-data.service';
import { AuthService } from '../../core/auth/auth.service'; import { AuthService } from '../../core/auth/auth.service';
import { RequestService } from '../../core/data/request.service'; import { RequestService } from '../../core/data/request.service';
import { ObjectCacheService } from '../../core/cache/object-cache.service'; import { ObjectCacheService } from '../../core/cache/object-cache.service';
import { environment } from '../../../environments/environment';
/** /**
* Form used for creating and editing communities * Form used for creating and editing communities
@@ -52,18 +53,22 @@ export class CommunityFormComponent extends ComColFormComponent<Community> {
new DynamicTextAreaModel({ new DynamicTextAreaModel({
id: 'description', id: 'description',
name: 'dc.description', name: 'dc.description',
spellCheck: environment.form.spellCheck,
}), }),
new DynamicTextAreaModel({ new DynamicTextAreaModel({
id: 'abstract', id: 'abstract',
name: 'dc.description.abstract', name: 'dc.description.abstract',
spellCheck: environment.form.spellCheck,
}), }),
new DynamicTextAreaModel({ new DynamicTextAreaModel({
id: 'rights', id: 'rights',
name: 'dc.rights', name: 'dc.rights',
spellCheck: environment.form.spellCheck,
}), }),
new DynamicTextAreaModel({ new DynamicTextAreaModel({
id: 'tableofcontents', id: 'tableofcontents',
name: 'dc.description.tableofcontents', name: 'dc.description.tableofcontents',
spellCheck: environment.form.spellCheck,
}), }),
]; ];

View File

@@ -55,6 +55,7 @@ import { MenuItemType } from '../shared/menu/menu-item-type.model';
id: 'statistics_community_:id', id: 'statistics_community_:id',
active: true, active: true,
visible: true, visible: true,
index: 2,
model: { model: {
type: MenuItemType.LINK, type: MenuItemType.LINK,
text: 'menu.section.statistics', text: 'menu.section.statistics',

View File

@@ -36,7 +36,7 @@ const DECLARATIONS = [CommunityPageComponent,
CommunityPageRoutingModule, CommunityPageRoutingModule,
StatisticsModule.forRoot(), StatisticsModule.forRoot(),
CommunityFormModule, CommunityFormModule,
ComcolModule ComcolModule,
], ],
declarations: [ declarations: [
...DECLARATIONS ...DECLARATIONS

View File

@@ -21,7 +21,7 @@ import { ComcolModule } from '../../shared/comcol/comcol.module';
EditCommunityPageRoutingModule, EditCommunityPageRoutingModule,
CommunityFormModule, CommunityFormModule,
ComcolModule, ComcolModule,
ResourcePoliciesModule ResourcePoliciesModule,
], ],
declarations: [ declarations: [
EditCommunityPageComponent, EditCommunityPageComponent,

View File

@@ -2,15 +2,12 @@ import { CommonModule } from '@angular/common';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core'; import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core';
import { DynamicFormLayoutService, DynamicFormService, DynamicFormValidationService } from '@ng-dynamic-forms/core';
import { EffectsModule } from '@ngrx/effects'; import { EffectsModule } from '@ngrx/effects';
import { Action, StoreConfig, StoreModule } from '@ngrx/store'; import { Action, StoreConfig, StoreModule } from '@ngrx/store';
import { MyDSpaceGuard } from '../my-dspace-page/my-dspace.guard'; import { MyDSpaceGuard } from '../my-dspace-page/my-dspace.guard';
import { isNotEmpty } from '../shared/empty.util'; 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 { HostWindowService } from '../shared/host-window.service';
import { MenuService } from '../shared/menu/menu.service'; import { MenuService } from '../shared/menu/menu.service';
import { EndpointMockingRestService } from '../shared/mocks/dspace-rest/endpoint-mocking-rest.service'; import { EndpointMockingRestService } from '../shared/mocks/dspace-rest/endpoint-mocking-rest.service';
@@ -24,8 +21,6 @@ import { SelectableListService } from '../shared/object-list/selectable-list/sel
import { ObjectSelectService } from '../shared/object-select/object-select.service'; import { ObjectSelectService } from '../shared/object-select/object-select.service';
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model'; import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
import { SidebarService } from '../shared/sidebar/sidebar.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 { AuthenticatedGuard } from './auth/authenticated.guard';
import { AuthStatus } from './auth/models/auth-status.model'; import { AuthStatus } from './auth/models/auth-status.model';
import { BrowseService } from './browse/browse.service'; import { BrowseService } from './browse/browse.service';
@@ -137,9 +132,6 @@ import {
import { Registration } from './shared/registration.model'; import { Registration } from './shared/registration.model';
import { MetadataSchemaDataService } from './data/metadata-schema-data.service'; import { MetadataSchemaDataService } from './data/metadata-schema-data.service';
import { MetadataFieldDataService } from './data/metadata-field-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 { TokenResponseParsingService } from './auth/token-response-parsing.service';
import { SubmissionCcLicenseDataService } from './submission/submission-cc-license-data.service'; import { SubmissionCcLicenseDataService } from './submission/submission-cc-license-data.service';
import { SubmissionCcLicence } from './submission/models/submission-cc-license.model'; import { SubmissionCcLicence } from './submission/models/submission-cc-license.model';
@@ -149,7 +141,6 @@ import { VocabularyEntry } from './submission/vocabularies/models/vocabulary-ent
import { Vocabulary } from './submission/vocabularies/models/vocabulary.model'; import { Vocabulary } from './submission/vocabularies/models/vocabulary.model';
import { VocabularyEntryDetail } from './submission/vocabularies/models/vocabulary-entry-detail.model'; import { VocabularyEntryDetail } from './submission/vocabularies/models/vocabulary-entry-detail.model';
import { VocabularyService } from './submission/vocabularies/vocabulary.service'; import { VocabularyService } from './submission/vocabularies/vocabulary.service';
import { VocabularyTreeviewService } from '../shared/vocabulary-treeview/vocabulary-treeview.service';
import { ConfigurationDataService } from './data/configuration-data.service'; import { ConfigurationDataService } from './data/configuration-data.service';
import { ConfigurationProperty } from './shared/configuration-property.model'; import { ConfigurationProperty } from './shared/configuration-property.model';
import { ReloadGuard } from './reload/reload.guard'; import { ReloadGuard } from './reload/reload.guard';
@@ -210,12 +201,6 @@ const PROVIDERS = [
DSOResponseParsingService, DSOResponseParsingService,
{ provide: MOCK_RESPONSE_MAP, useValue: mockResponseMap }, { provide: MOCK_RESPONSE_MAP, useValue: mockResponseMap },
{ provide: DspaceRestService, useFactory: restServiceFactory, deps: [MOCK_RESPONSE_MAP, HttpClient] }, { provide: DspaceRestService, useFactory: restServiceFactory, deps: [MOCK_RESPONSE_MAP, HttpClient] },
DynamicFormLayoutService,
DynamicFormService,
DynamicFormValidationService,
FormBuilderService,
SectionFormOperationsService,
FormService,
EPersonDataService, EPersonDataService,
LinkHeadService, LinkHeadService,
HALEndpointService, HALEndpointService,
@@ -244,12 +229,10 @@ const PROVIDERS = [
SubmissionResponseParsingService, SubmissionResponseParsingService,
SubmissionJsonPatchOperationsService, SubmissionJsonPatchOperationsService,
JsonPatchOperationsBuilder, JsonPatchOperationsBuilder,
UploaderService,
UUIDService, UUIDService,
NotificationsService, NotificationsService,
WorkspaceitemDataService, WorkspaceitemDataService,
WorkflowItemDataService, WorkflowItemDataService,
UploaderService,
DSpaceObjectDataService, DSpaceObjectDataService,
ConfigurationDataService, ConfigurationDataService,
DSOChangeAnalyzer, DSOChangeAnalyzer,
@@ -266,7 +249,6 @@ const PROVIDERS = [
ClaimedTaskDataService, ClaimedTaskDataService,
PoolTaskDataService, PoolTaskDataService,
BitstreamDataService, BitstreamDataService,
DsDynamicTypeBindRelationService,
EntityTypeDataService, EntityTypeDataService,
ContentSourceResponseParsingService, ContentSourceResponseParsingService,
ItemTemplateDataService, ItemTemplateDataService,
@@ -302,7 +284,6 @@ const PROVIDERS = [
VocabularyService, VocabularyService,
VocabularyDataService, VocabularyDataService,
VocabularyEntryDetailsDataService, VocabularyEntryDetailsDataService,
VocabularyTreeviewService,
SequenceService, SequenceService,
GroupDataService, GroupDataService,
FeedbackDataService, FeedbackDataService,

View File

@@ -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'; import { Injectable } from '@angular/core';
@Injectable() @Injectable({
export class UploaderService { providedIn: 'root'
})
export class DragService {
private _overrideDragOverPage = false; private _overrideDragOverPage = false;
public overrideDragOverPage() { public overrideDragOverPage() {

View File

@@ -40,7 +40,7 @@ export class LocaleService {
protected translate: TranslateService, protected translate: TranslateService,
protected authService: AuthService, protected authService: AuthService,
protected routeService: RouteService, protected routeService: RouteService,
@Inject(DOCUMENT) private document: any @Inject(DOCUMENT) protected document: any
) { ) {
} }

View File

@@ -1,12 +1,31 @@
import { LANG_ORIGIN, LocaleService } from './locale.service'; 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 { combineLatest, Observable, of as observableOf } from 'rxjs';
import { map, mergeMap, take } from 'rxjs/operators'; 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() @Injectable()
export class ServerLocaleService extends LocaleService { 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 * Get the languages list of the user in Accept-Language format
* *
@@ -50,6 +69,10 @@ export class ServerLocaleService extends LocaleService {
if (isNotEmpty(epersonLang)) { if (isNotEmpty(epersonLang)) {
languages.push(...epersonLang); languages.push(...epersonLang);
} }
if (hasValue(this.req.headers['accept-language'])) {
languages.push(...this.req.headers['accept-language'].split(',')
);
}
return languages; return languages;
}) })
); );

View File

@@ -4,7 +4,7 @@ import { RemoteData } from '../../../core/data/remote-data';
import { Item } from '../../../core/shared/item.model'; import { Item } from '../../../core/shared/item.model';
import { map, take, switchMap } from 'rxjs/operators'; import { map, take, switchMap } from 'rxjs/operators';
import { ActivatedRoute, Router } from '@angular/router'; 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 { hasValue, isEmpty, isNotEmpty } from '../../../shared/empty.util';
import { ItemDataService } from '../../../core/data/item-data.service'; import { ItemDataService } from '../../../core/data/item-data.service';
import { AuthService } from '../../../core/auth/auth.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 { Bundle } from '../../../core/shared/bundle.model';
import { BundleDataService } from '../../../core/data/bundle-data.service'; import { BundleDataService } from '../../../core/data/bundle-data.service';
import { getFirstSucceededRemoteDataPayload, getFirstCompletedRemoteData } from '../../../core/shared/operators'; 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 { RequestService } from '../../../core/data/request.service';
import { getBitstreamModuleRoute } from '../../../app-routing-paths'; import { getBitstreamModuleRoute } from '../../../app-routing-paths';
import { getEntityEditRoute } from '../../item-page-routing-paths'; import { getEntityEditRoute } from '../../item-page-routing-paths';

View File

@@ -18,6 +18,8 @@ import { NotificationsService } from '../../../../shared/notifications/notificat
import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service.stub'; import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service.stub';
import { PaginationService } from '../../../../core/pagination/pagination.service'; import { PaginationService } from '../../../../core/pagination/pagination.service';
import { PaginationServiceStub } from '../../../../shared/testing/pagination-service.stub'; import { PaginationServiceStub } from '../../../../shared/testing/pagination-service.stub';
import { APP_CONFIG } from 'src/config/app-config.interface';
import { environment } from 'src/environments/environment';
describe('FullFileSectionComponent', () => { describe('FullFileSectionComponent', () => {
let comp: FullFileSectionComponent; let comp: FullFileSectionComponent;
@@ -69,7 +71,8 @@ describe('FullFileSectionComponent', () => {
providers: [ providers: [
{ provide: BitstreamDataService, useValue: bitstreamDataService }, { provide: BitstreamDataService, useValue: bitstreamDataService },
{ provide: NotificationsService, useValue: new NotificationsServiceStub() }, { provide: NotificationsService, useValue: new NotificationsServiceStub() },
{ provide: PaginationService, useValue: paginationService } { provide: PaginationService, useValue: paginationService },
{ provide: APP_CONFIG, useValue: environment },
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]

View File

@@ -1,4 +1,4 @@
import { Component, Input, OnInit } from '@angular/core'; import { Component, Inject, Input, OnInit } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { BitstreamDataService } from '../../../../core/data/bitstream-data.service'; 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 { TranslateService } from '@ngx-translate/core';
import { hasValue, isEmpty } from '../../../../shared/empty.util'; import { hasValue, isEmpty } from '../../../../shared/empty.util';
import { PaginationService } from '../../../../core/pagination/pagination.service'; 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 * This component renders the file section of the item
@@ -34,26 +35,26 @@ export class FullFileSectionComponent extends FileSectionComponent implements On
originals$: Observable<RemoteData<PaginatedList<Bitstream>>>; originals$: Observable<RemoteData<PaginatedList<Bitstream>>>;
licenses$: Observable<RemoteData<PaginatedList<Bitstream>>>; licenses$: Observable<RemoteData<PaginatedList<Bitstream>>>;
pageSize = 5;
originalOptions = Object.assign(new PaginationComponentOptions(), { originalOptions = Object.assign(new PaginationComponentOptions(), {
id: 'obo', id: 'obo',
currentPage: 1, currentPage: 1,
pageSize: this.pageSize pageSize: this.appConfig.item.bitstream.pageSize
}); });
licenseOptions = Object.assign(new PaginationComponentOptions(), { licenseOptions = Object.assign(new PaginationComponentOptions(), {
id: 'lbo', id: 'lbo',
currentPage: 1, currentPage: 1,
pageSize: this.pageSize pageSize: this.appConfig.item.bitstream.pageSize
}); });
constructor( constructor(
bitstreamDataService: BitstreamDataService, bitstreamDataService: BitstreamDataService,
protected notificationsService: NotificationsService, protected notificationsService: NotificationsService,
protected translateService: TranslateService, 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 { ngOnInit(): void {

View File

@@ -67,6 +67,7 @@ import { OrcidPageGuard } from './orcid-page/orcid-page.guard';
id: 'statistics_item_:id', id: 'statistics_item_:id',
active: true, active: true,
visible: true, visible: true,
index: 2,
model: { model: {
type: MenuItemType.LINK, type: MenuItemType.LINK,
text: 'menu.section.statistics', text: 'menu.section.statistics',

View File

@@ -46,6 +46,7 @@ import { OrcidPageComponent } from './orcid-page/orcid-page.component';
import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap'; import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap';
import { OrcidSyncSettingsComponent } from './orcid-page/orcid-sync-settings/orcid-sync-settings.component'; import { OrcidSyncSettingsComponent } from './orcid-page/orcid-sync-settings/orcid-sync-settings.component';
import { OrcidQueueComponent } from './orcid-page/orcid-queue/orcid-queue.component'; import { OrcidQueueComponent } from './orcid-page/orcid-queue/orcid-queue.component';
import { UploadModule } from '../shared/upload/upload.module';
const ENTRY_COMPONENTS = [ const ENTRY_COMPONENTS = [
@@ -94,7 +95,8 @@ const DECLARATIONS = [
JournalEntitiesModule.withEntryComponents(), JournalEntitiesModule.withEntryComponents(),
ResearchEntitiesModule.withEntryComponents(), ResearchEntitiesModule.withEntryComponents(),
NgxGalleryModule, NgxGalleryModule,
NgbAccordionModule NgbAccordionModule,
UploadModule,
], ],
declarations: [ declarations: [
...DECLARATIONS, ...DECLARATIONS,

View File

@@ -17,6 +17,8 @@ import { MetadataFieldWrapperComponent } from '../../../field-components/metadat
import { createPaginatedList } from '../../../../shared/testing/utils.test'; import { createPaginatedList } from '../../../../shared/testing/utils.test';
import { NotificationsService } from '../../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../../shared/notifications/notifications.service';
import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service.stub'; 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', () => { describe('FileSectionComponent', () => {
let comp: FileSectionComponent; let comp: FileSectionComponent;
@@ -65,7 +67,8 @@ describe('FileSectionComponent', () => {
declarations: [FileSectionComponent, VarDirective, FileSizePipe, MetadataFieldWrapperComponent], declarations: [FileSectionComponent, VarDirective, FileSizePipe, MetadataFieldWrapperComponent],
providers: [ providers: [
{ provide: BitstreamDataService, useValue: bitstreamDataService }, { provide: BitstreamDataService, useValue: bitstreamDataService },
{ provide: NotificationsService, useValue: new NotificationsServiceStub() } { provide: NotificationsService, useValue: new NotificationsServiceStub() },
{ provide: APP_CONFIG, useValue: environment }
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]

View File

@@ -1,4 +1,4 @@
import { Component, Input, OnInit } from '@angular/core'; import { Component, Inject, Input, OnInit } from '@angular/core';
import { BehaviorSubject } from 'rxjs'; import { BehaviorSubject } from 'rxjs';
import { BitstreamDataService } from '../../../../core/data/bitstream-data.service'; 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 { NotificationsService } from '../../../../shared/notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { getFirstCompletedRemoteData } from '../../../../core/shared/operators'; 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 * This component renders the file section of the item
@@ -35,13 +36,15 @@ export class FileSectionComponent implements OnInit {
isLastPage: boolean; isLastPage: boolean;
pageSize = 5; pageSize: number;
constructor( constructor(
protected bitstreamDataService: BitstreamDataService, protected bitstreamDataService: BitstreamDataService,
protected notificationsService: NotificationsService, protected notificationsService: NotificationsService,
protected translateService: TranslateService protected translateService: TranslateService,
@Inject(APP_CONFIG) protected appConfig: AppConfig
) { ) {
this.pageSize = this.appConfig.item.bitstream.pageSize;
} }
ngOnInit(): void { ngOnInit(): void {

View File

@@ -5,7 +5,7 @@
</button> </button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<ds-collection-dropdown (selectionChange)="selectObject($event)"> <ds-themed-collection-dropdown (selectionChange)="selectObject($event)">
</ds-collection-dropdown> </ds-themed-collection-dropdown>
</div> </div>
</div> </div>

View File

@@ -128,10 +128,13 @@ describe('CollectionSelectorComponent', () => {
beforeEach(() => { beforeEach(() => {
scheduler = getTestScheduler(); scheduler = getTestScheduler();
fixture = TestBed.createComponent(CollectionSelectorComponent); fixture = TestBed.overrideComponent(CollectionSelectorComponent, {
set: {
template: '<ds-collection-dropdown (selectionChange)="selectObject($event)"></ds-collection-dropdown>'
}
}).createComponent(CollectionSelectorComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should create', () => { it('should create', () => {

View File

@@ -16,10 +16,10 @@ import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
import { getMockScrollToService } from '../../shared/mocks/scroll-to-service.mock'; 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 { HostWindowService } from '../../shared/host-window.service';
import { HostWindowServiceStub } from '../../shared/testing/host-window-service.stub'; 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 { HttpXsrfTokenExtractor } from '@angular/common/http';
import { CookieService } from '../../core/services/cookie.service'; import { CookieService } from '../../core/services/cookie.service';
import { CookieServiceMock } from '../../shared/mocks/cookie.service.mock'; import { CookieServiceMock } from '../../shared/mocks/cookie.service.mock';
@@ -59,7 +59,7 @@ describe('MyDSpaceNewSubmissionComponent test', () => {
NgbModal, NgbModal,
ChangeDetectorRef, ChangeDetectorRef,
MyDSpaceNewSubmissionComponent, MyDSpaceNewSubmissionComponent,
UploaderService, DragService,
{ provide: HttpXsrfTokenExtractor, useValue: new HttpXsrfTokenExtractorMock('mock-token') }, { provide: HttpXsrfTokenExtractor, useValue: new HttpXsrfTokenExtractorMock('mock-token') },
{ provide: CookieService, useValue: new CookieServiceMock() }, { provide: CookieService, useValue: new CookieServiceMock() },
{ provide: HostWindowService, useValue: new HostWindowServiceStub(800) }, { provide: HostWindowService, useValue: new HostWindowServiceStub(800) },

View File

@@ -8,13 +8,13 @@ import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { AuthService } from '../../core/auth/auth.service'; import { AuthService } from '../../core/auth/auth.service';
import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { NotificationsService } from '../../shared/notifications/notifications.service'; 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 { HALEndpointService } from '../../core/shared/hal-endpoint.service';
import { hasValue } from '../../shared/empty.util'; import { hasValue } from '../../shared/empty.util';
import { SearchResult } from '../../shared/search/models/search-result.model'; import { SearchResult } from '../../shared/search/models/search-result.model';
import { CollectionSelectorComponent } from '../collection-selector/collection-selector.component'; import { CollectionSelectorComponent } from '../collection-selector/collection-selector.component';
import { UploaderComponent } from '../../shared/uploader/uploader.component'; import { UploaderComponent } from '../../shared/upload/uploader/uploader.component';
import { UploaderError } from '../../shared/uploader/uploader-error.model'; import { UploaderError } from '../../shared/upload/uploader/uploader-error.model';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
/** /**

View File

@@ -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 { 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 { ThemedMyDSpacePageComponent } from './themed-my-dspace-page.component';
import { SearchModule } from '../shared/search/search.module'; import { SearchModule } from '../shared/search/search.module';
import { UploadModule } from '../shared/upload/upload.module';
const DECLARATIONS = [ const DECLARATIONS = [
MyDSpacePageComponent, MyDSpacePageComponent,
@@ -30,7 +31,8 @@ const DECLARATIONS = [
SharedModule, SharedModule,
SearchModule, SearchModule,
MyDspacePageRoutingModule, MyDspacePageRoutingModule,
MyDspaceSearchModule.withEntryComponents() MyDspaceSearchModule.withEntryComponents(),
UploadModule,
], ],
declarations: DECLARATIONS, declarations: DECLARATIONS,
providers: [ providers: [

View File

@@ -21,6 +21,7 @@ const effects = [
const ENTRY_COMPONENTS = [ const ENTRY_COMPONENTS = [
// put only entry components that use custom decorator // put only entry components that use custom decorator
NavbarSectionComponent, NavbarSectionComponent,
ExpandableNavbarSectionComponent,
ThemedExpandableNavbarSectionComponent, ThemedExpandableNavbarSectionComponent,
]; ];
@@ -34,11 +35,9 @@ const ENTRY_COMPONENTS = [
CoreModule.forRoot() CoreModule.forRoot()
], ],
declarations: [ declarations: [
...ENTRY_COMPONENTS,
NavbarComponent, NavbarComponent,
ThemedNavbarComponent, ThemedNavbarComponent,
NavbarSectionComponent,
ExpandableNavbarSectionComponent,
ThemedExpandableNavbarSectionComponent,
], ],
providers: [], providers: [],
exports: [ exports: [

View File

@@ -95,6 +95,10 @@ describe('RegisterEmailComponent', () => {
comp.form.patchValue({email: 'valid@email.org'}); comp.form.patchValue({email: 'valid@email.org'});
expect(comp.form.invalid).toBeFalse(); 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', () => { describe('register', () => {
it('should send a registration to the service and on success display a message and return to home', () => { it('should send a registration to the service and on success display a message and return to home', () => {

View File

@@ -79,7 +79,9 @@ export class RegisterEmailFormComponent implements OnInit {
this.form = this.formBuilder.group({ this.form = this.formBuilder.group({
email: new FormControl('', { email: new FormControl('', {
validators: [Validators.required, 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])?)*$')
], ],
}) })
}); });

View File

@@ -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<CollectionDropdownComponent> {
@Input() entityType: string;
@Output() searchComplete = new EventEmitter<any>();
@Output() theOnlySelectable = new EventEmitter<CollectionListEntry>();
@Output() selectionChange = new EventEmitter<CollectionListEntry>();
protected inAndOutputNames: (keyof CollectionDropdownComponent & keyof this)[] = ['entityType', 'searchComplete', 'theOnlySelectable', 'selectionChange'];
protected getComponentName(): string {
return 'CollectionDropdownComponent';
}
protected importThemedComponent(themeName: string): Promise<any> {
return import(`../../../themes/${themeName}/app/shared/collection-dropdown/collection-dropdown.component`);
}
protected importUnthemedComponent(): Promise<any> {
return import(`./collection-dropdown.component`);
}
}

View File

@@ -17,8 +17,8 @@ import { MetadataMap, MetadataValue } from '../../../../core/shared/metadata.mod
import { ResourceType } from '../../../../core/shared/resource-type'; import { ResourceType } from '../../../../core/shared/resource-type';
import { hasValue, isNotEmpty } from '../../../empty.util'; import { hasValue, isNotEmpty } from '../../../empty.util';
import { NotificationsService } from '../../../notifications/notifications.service'; import { NotificationsService } from '../../../notifications/notifications.service';
import { UploaderOptions } from '../../../uploader/uploader-options.model'; import { UploaderOptions } from '../../../upload/uploader/uploader-options.model';
import { UploaderComponent } from '../../../uploader/uploader.component'; import { UploaderComponent } from '../../../upload/uploader/uploader.component';
import { Operation } from 'fast-json-patch'; import { Operation } from 'fast-json-patch';
import { NoContent } from '../../../../core/shared/NoContent.model'; import { NoContent } from '../../../../core/shared/NoContent.model';
import { getFirstCompletedRemoteData } from '../../../../core/shared/operators'; import { getFirstCompletedRemoteData } from '../../../../core/shared/operators';

View File

@@ -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 { ComcolRoleComponent } from './comcol-forms/edit-comcol-page/comcol-role/comcol-role.component';
import { SharedModule } from '../shared.module'; import { SharedModule } from '../shared.module';
import { FormModule } from '../form/form.module'; import { FormModule } from '../form/form.module';
import { UploadModule } from '../upload/upload.module';
const COMPONENTS = [ const COMPONENTS = [
ComcolPageContentComponent, ComcolPageContentComponent,
@@ -28,9 +29,7 @@ const COMPONENTS = [
ComcolPageBrowseByComponent, ComcolPageBrowseByComponent,
ThemedComcolPageBrowseByComponent, ThemedComcolPageBrowseByComponent,
ComcolRoleComponent, ComcolRoleComponent,
ThemedComcolPageHandleComponent ThemedComcolPageHandleComponent
]; ];
@NgModule({ @NgModule({
@@ -40,10 +39,12 @@ const COMPONENTS = [
imports: [ imports: [
CommonModule, CommonModule,
FormModule, FormModule,
SharedModule SharedModule,
UploadModule,
], ],
exports: [ exports: [
...COMPONENTS ...COMPONENTS,
UploadModule,
] ]
}) })
export class ComcolModule { } export class ComcolModule { }

View File

@@ -1,5 +1,4 @@
import { Injectable } from '@angular/core'; import { Inject, Injectable, InjectionToken } from '@angular/core';
import { setup, show } from 'klaro/dist/klaro-no-translations';
import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs'; import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs';
import { AuthService } from '../../core/auth/auth.service'; import { AuthService } from '../../core/auth/auth.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
@@ -43,6 +42,17 @@ const cookiePurposeMessagePrefix = 'cookies.consent.purpose.';
*/ */
const updateDebounce = 300; 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<Promise<any>>(
'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 * Browser implementation for the KlaroService, representing a service for handling Klaro consent preferences and UI
*/ */
@@ -65,7 +75,9 @@ export class BrowserKlaroService extends KlaroService {
private authService: AuthService, private authService: AuthService,
private ePersonService: EPersonDataService, private ePersonService: EPersonDataService,
private configService: ConfigurationDataService, private configService: ConfigurationDataService,
private cookieService: CookieService) { private cookieService: CookieService,
@Inject(LAZY_KLARO) private lazyKlaro: Promise<any>,
) {
super(); super();
} }
@@ -135,8 +147,7 @@ export class BrowserKlaroService extends KlaroService {
this.translateConfiguration(); this.translateConfiguration();
this.klaroConfig.services = this.filterConfigServices(servicesToHide); this.klaroConfig.services = this.filterConfigServices(servicesToHide);
this.lazyKlaro.then(({ setup }) => setup(this.klaroConfig));
setup(this.klaroConfig);
}); });
} }
@@ -220,7 +231,7 @@ export class BrowserKlaroService extends KlaroService {
* Show the cookie consent form * Show the cookie consent form
*/ */
showSettings() { showSettings() {
show(this.klaroConfig); this.lazyKlaro.then(({show}) => show(this.klaroConfig));
} }
/** /**

View File

@@ -53,8 +53,9 @@ export class AuthorizedCollectionSelectorComponent extends DSOSelectorComponent
* Perform a search for authorized collections with the current query and page * Perform a search for authorized collections with the current query and page
* @param query Query to search objects for * @param query Query to search objects for
* @param page Page to retrieve * @param page Page to retrieve
* @param useCache Whether or not to use the cache
*/ */
search(query: string, page: number): Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> { search(query: string, page: number, useCache: boolean = true): Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> {
let searchListService$: Observable<RemoteData<PaginatedList<Collection>>> = null; let searchListService$: Observable<RemoteData<PaginatedList<Collection>>> = null;
const findOptions: FindListOptions = { const findOptions: FindListOptions = {
currentPage: page, currentPage: page,
@@ -69,7 +70,7 @@ export class AuthorizedCollectionSelectorComponent extends DSOSelectorComponent
findOptions); findOptions);
} else { } else {
searchListService$ = this.collectionDataService searchListService$ = this.collectionDataService
.getAuthorizedCollection(query, findOptions, true, false, followLink('parentCommunity')); .getAuthorizedCollection(query, findOptions, useCache, false, followLink('parentCommunity'));
} }
return searchListService$.pipe( return searchListService$.pipe(
getFirstCompletedRemoteData(), getFirstCompletedRemoteData(),

View File

@@ -21,12 +21,12 @@
</button> </button>
<button *ngFor="let listEntry of (listEntries$ | async)" <button *ngFor="let listEntry of (listEntries$ | async)"
class="list-group-item list-group-item-action border-0 list-entry" class="list-group-item list-group-item-action border-0 list-entry"
[ngClass]="{'bg-primary': listEntry.indexableObject.id === currentDSOId}" [ngClass]="{'bg-primary': listEntry['id'] === currentDSOId}"
title="{{ getName(listEntry) }}" title="{{ getName(listEntry) }}"
dsHoverClass="ds-hover" dsHoverClass="ds-hover"
(click)="onSelect.emit(listEntry.indexableObject)" #listEntryElement> (click)="onClick(listEntry)" #listEntryElement>
<ds-listable-object-component-loader [object]="listEntry" [viewMode]="viewMode" <ds-listable-object-component-loader [object]="listEntry" [viewMode]="viewMode"
[linkType]=linkTypes.None [context]="getContext(listEntry.indexableObject.id)"></ds-listable-object-component-loader> [linkType]=linkTypes.None [context]="getContext(listEntry['id'])"></ds-listable-object-component-loader>
</button> </button>
</ng-container> </ng-container>
<button *ngIf="loading" <button *ngIf="loading"

View File

@@ -35,6 +35,14 @@ import { RemoteData } from '../../../core/data/remote-data';
import { NotificationsService } from '../../notifications/notifications.service'; import { NotificationsService } from '../../notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
import {
ListableNotificationObject
} from '../../object-list/listable-notification-object/listable-notification-object.model';
import { ListableObject } from '../../object-collection/shared/listable-object.model';
import { NotificationType } from '../../notifications/models/notification-type';
import {
LISTABLE_NOTIFICATION_OBJECT
} from '../../object-list/listable-notification-object/listable-notification-object.resource-type';
@Component({ @Component({
selector: 'ds-dso-selector', selector: 'ds-dso-selector',
@@ -82,7 +90,7 @@ export class DSOSelectorComponent implements OnInit, OnDestroy {
/** /**
* List with search results of DSpace objects for the current query * List with search results of DSpace objects for the current query
*/ */
listEntries$: BehaviorSubject<SearchResult<DSpaceObject>[]> = new BehaviorSubject(null); listEntries$: BehaviorSubject<ListableObject[]> = new BehaviorSubject(null);
/** /**
* The current page to load * The current page to load
@@ -116,11 +124,6 @@ export class DSOSelectorComponent implements OnInit, OnDestroy {
*/ */
linkTypes = CollectionElementLinkType; linkTypes = CollectionElementLinkType;
/**
* Track whether the element has the mouse over it
*/
isMouseOver = false;
/** /**
* Array to track all subscriptions and unsubscribe them onDestroy * Array to track all subscriptions and unsubscribe them onDestroy
* @type {Array} * @type {Array}
@@ -182,24 +185,28 @@ export class DSOSelectorComponent implements OnInit, OnDestroy {
}) })
); );
}) })
).subscribe((rd) => { ).subscribe((rd: RemoteData<PaginatedList<SearchResult<DSpaceObject>>>) => {
this.loading = false; this.updateList(rd);
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;
}
})); }));
} }
updateList(rd: RemoteData<PaginatedList<SearchResult<DSpaceObject>>>) {
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 * 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 * Perform a search for the current query and page
* @param query Query to search objects for * @param query Query to search objects for
* @param page Page to retrieve * @param page Page to retrieve
* @param useCache Whether or not to use the cache
*/ */
search(query: string, page: number): Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> { search(query: string, page: number, useCache: boolean = true): Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> {
return this.searchService.search( return this.searchService.search(
new PaginatedSearchOptions({ new PaginatedSearchOptions({
query: query, query: query,
@@ -220,7 +228,9 @@ export class DSOSelectorComponent implements OnInit, OnDestroy {
pagination: Object.assign({}, this.defaultPagination, { pagination: Object.assign({}, this.defaultPagination, {
currentPage: page currentPage: page
}) })
}) }),
null,
useCache,
).pipe( ).pipe(
getFirstCompletedRemoteData() getFirstCompletedRemoteData()
); );
@@ -262,7 +272,28 @@ export class DSOSelectorComponent implements OnInit, OnDestroy {
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe()); this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
} }
getName(searchResult: SearchResult<DSpaceObject>): 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<DSpaceObject>).indexableObject)) {
this.onSelect.emit((listableObject as SearchResult<DSpaceObject>).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<PaginatedList<SearchResult<DSpaceObject>>>) => {
this.updateList(rd);
});
}
}
getName(listableObject: ListableObject): string {
return hasValue((listableObject as SearchResult<DSpaceObject>).indexableObject) ?
this.dsoNameService.getName((listableObject as SearchResult<DSpaceObject>).indexableObject) : null;
} }
} }

View File

@@ -9,7 +9,7 @@
<h3 class="position-relative py-1 my-3 font-weight-normal"> <h3 class="position-relative py-1 my-3 font-weight-normal">
<hr> <hr>
<div id="create-community-or-separator" class="text-center position-absolute w-100"> <div id="create-community-or-separator" class="text-center position-absolute w-100">
<span class="px-4 bg-white">or</span> <span class="px-4 bg-white">{{'dso-selector.create.community.or-divider' | translate}}</span>
</div> </div>
</h3> </h3>

View File

@@ -79,6 +79,8 @@ import { FormService } from '../../form.service';
import { SubmissionService } from '../../../../submission/submission.service'; import { SubmissionService } from '../../../../submission/submission.service';
import { FormBuilderService } from '../form-builder.service'; import { FormBuilderService } from '../form-builder.service';
import { NgxMaskModule } from 'ngx-mask'; import { NgxMaskModule } from 'ngx-mask';
import { APP_CONFIG } from '../../../../../config/app-config.interface';
import { environment } from '../../../../../environments/environment';
function getMockDsDynamicTypeBindRelationService(): DsDynamicTypeBindRelationService { function getMockDsDynamicTypeBindRelationService(): DsDynamicTypeBindRelationService {
return jasmine.createSpyObj('DsDynamicTypeBindRelationService', { return jasmine.createSpyObj('DsDynamicTypeBindRelationService', {
@@ -230,7 +232,8 @@ describe('DsDynamicFormControlContainerComponent test suite', () => {
findById: () => observableOf(createSuccessfulRemoteDataObject(testWSI)) findById: () => observableOf(createSuccessfulRemoteDataObject(testWSI))
} }
}, },
{ provide: NgZone, useValue: new NgZone({}) } { provide: NgZone, useValue: new NgZone({}) },
{ provide: APP_CONFIG, useValue: environment }
], ],
schemas: [CUSTOM_ELEMENTS_SCHEMA] schemas: [CUSTOM_ELEMENTS_SCHEMA]
}).compileComponents().then(() => { }).compileComponents().then(() => {

View File

@@ -4,7 +4,7 @@ import {
Component, Component,
ComponentFactoryResolver, ComponentFactoryResolver,
ContentChildren, ContentChildren,
EventEmitter, EventEmitter, Inject,
Input, Input,
NgZone, NgZone,
OnChanges, OnChanges,
@@ -118,6 +118,8 @@ import { RelationshipOptions } from '../models/relationship-options.model';
import { FormBuilderService } from '../form-builder.service'; import { FormBuilderService } from '../form-builder.service';
import { DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP } from './ds-dynamic-form-constants'; import { DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP } from './ds-dynamic-form-constants';
import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model'; 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<DynamicFormControl> | null { export function dsDynamicFormControlMapFn(model: DynamicFormControlModel): Type<DynamicFormControl> | null {
switch (model.type) { switch (model.type) {
@@ -231,6 +233,11 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
private showErrorMessagesPreviousStage: boolean; private showErrorMessagesPreviousStage: boolean;
/**
* Determines whether to request embedded thumbnail.
*/
fetchThumbnail: boolean;
get componentType(): Type<DynamicFormControl> | null { get componentType(): Type<DynamicFormControl> | null {
return dsDynamicFormControlMapFn(this.model); return dsDynamicFormControlMapFn(this.model);
} }
@@ -253,9 +260,11 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
private ref: ChangeDetectorRef, private ref: ChangeDetectorRef,
private formService: FormService, private formService: FormService,
private formBuilderService: FormBuilderService, private formBuilderService: FormBuilderService,
private submissionService: SubmissionService private submissionService: SubmissionService,
@Inject(APP_CONFIG) protected appConfig: AppConfig,
) { ) {
super(ref, componentFactoryResolver, layoutService, validationService, dynamicFormComponentService, relationService); 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('rightItem'),
followLink('relationshipType') followLink('relationshipType')
); );
relationshipsRD$.pipe( relationshipsRD$.pipe(
getFirstSucceededRemoteDataPayload(), getFirstSucceededRemoteDataPayload(),
getPaginatedListPayload() getPaginatedListPayload()
@@ -317,8 +325,10 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
} }
if (hasValue(this.value) && this.value.isVirtual) { if (hasValue(this.value) && this.value.isVirtual) {
const relationship$ = this.relationshipService.findById(this.value.virtualValue, true, true, followLink('leftItem'), followLink('rightItem'), followLink('relationshipType')) const relationship$ = this.relationshipService.findById(this.value.virtualValue,
.pipe( true,
true,
... itemLinksToFollow(this.fetchThumbnail)).pipe(
getAllSucceededRemoteData(), getAllSucceededRemoteData(),
getRemoteDataPayload()); getRemoteDataPayload());
this.relationshipValue$ = observableCombineLatest([this.item$.pipe(take(1)), relationship$]).pipe( this.relationshipValue$ = observableCombineLatest([this.item$.pipe(take(1)), relationship$]).pipe(

View File

@@ -1,3 +1 @@
span.text-contents{
padding: var(--bs-btn-padding-y) 0;
}

View File

@@ -1,12 +1,12 @@
<div class="d-flex"> <div class="d-flex">
<span class="mr-auto text-contents"> <div class="flex-grow-1 mr-auto">
<ng-container *ngIf="!(relatedItem$ | async)"> <ng-container *ngIf="!(relatedItem$ | async)">
<ds-themed-loading [showMessage]="false"></ds-themed-loading> <ds-themed-loading [showMessage]="false"></ds-themed-loading>
</ng-container> </ng-container>
<ng-container *ngIf="(relatedItem$ | async)"> <ng-container *ngIf="(relatedItem$ | async)">
<ds-listable-object-component-loader [showLabel]="false" [viewMode]="viewType" [object]="(relatedItem$ | async)"></ds-listable-object-component-loader> <ds-listable-object-component-loader [showLabel]="false" [viewMode]="viewType" [object]="(relatedItem$ | async)"></ds-listable-object-component-loader>
</ng-container> </ng-container>
</span> </div>
<button type="button" class="btn btn-secondary" <button type="button" class="btn btn-secondary"
(click)="removeSelection()"> (click)="removeSelection()">
<i class="fas fa-trash" aria-hidden="true"></i> <i class="fas fa-trash" aria-hidden="true"></i>

View File

@@ -69,7 +69,7 @@ describe('ExistingRelationListElementComponent', () => {
providers: [ providers: [
{ provide: SelectableListService, useValue: selectionService }, { provide: SelectableListService, useValue: selectionService },
{ provide: Store, useValue: store }, { provide: Store, useValue: store },
{ provide: SubmissionService, useClass: SubmissionServiceStub }, { provide: SubmissionService, useClass: SubmissionServiceStub }
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}) })

View File

@@ -20,7 +20,7 @@ import { FormFieldMetadataValueObject } from '../../../models/form-field-metadat
import { VocabularyEntry } from '../../../../../../core/submission/vocabularies/models/vocabulary-entry.model'; import { VocabularyEntry } from '../../../../../../core/submission/vocabularies/models/vocabulary-entry.model';
import { createTestComponent } from '../../../../../testing/utils.test'; import { createTestComponent } from '../../../../../testing/utils.test';
import { DynamicLookupNameModel } from './dynamic-lookup-name.model'; 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 { ObjNgFor } from '../../../../../utils/object-ngfor.pipe';
import { import {
mockDynamicFormLayoutService, mockDynamicFormLayoutService,

View File

@@ -21,11 +21,11 @@ import { DsDynamicOneboxComponent } from './dynamic-onebox.component';
import { DynamicOneboxModel } from './dynamic-onebox.model'; import { DynamicOneboxModel } from './dynamic-onebox.model';
import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model'; import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model';
import { createTestComponent } from '../../../../../testing/utils.test'; 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 { ObjNgFor } from '../../../../../utils/object-ngfor.pipe';
import { VocabularyEntry } from '../../../../../../core/submission/vocabularies/models/vocabulary-entry.model'; import { VocabularyEntry } from '../../../../../../core/submission/vocabularies/models/vocabulary-entry.model';
import { createSuccessfulRemoteDataObject$ } from '../../../../../remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../../../../../remote-data.utils';
import { VocabularyTreeviewComponent } from '../../../../../vocabulary-treeview/vocabulary-treeview.component'; import { VocabularyTreeviewComponent } from '../../../../vocabulary-treeview/vocabulary-treeview.component';
import { import {
mockDynamicFormLayoutService, mockDynamicFormLayoutService,
mockDynamicFormValidationService mockDynamicFormValidationService

View File

@@ -30,7 +30,7 @@ import { VocabularyEntry } from '../../../../../../core/submission/vocabularies/
import { PageInfo } from '../../../../../../core/shared/page-info.model'; import { PageInfo } from '../../../../../../core/shared/page-info.model';
import { DsDynamicVocabularyComponent } from '../dynamic-vocabulary.component'; import { DsDynamicVocabularyComponent } from '../dynamic-vocabulary.component';
import { Vocabulary } from '../../../../../../core/submission/vocabularies/models/vocabulary.model'; 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'; import { VocabularyEntryDetail } from '../../../../../../core/submission/vocabularies/models/vocabulary-entry-detail.model';
/** /**

View File

@@ -16,7 +16,7 @@ import { FormFieldModel } from '../../../models/form-field.model';
import { FormBuilderService } from '../../../form-builder.service'; import { FormBuilderService } from '../../../form-builder.service';
import { FormService } from '../../../../form.service'; import { FormService } from '../../../../form.service';
import { FormComponent } from '../../../../form.component'; 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 { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model';
import { DsDynamicInputModel } from '../ds-dynamic-input.model'; import { DsDynamicInputModel } from '../ds-dynamic-input.model';
import { createTestComponent } from '../../../../../testing/utils.test'; import { createTestComponent } from '../../../../../testing/utils.test';

View File

@@ -19,10 +19,10 @@ import { FormBuilderService } from '../../../form-builder.service';
import { SubmissionFormsModel } from '../../../../../../core/config/models/config-submission-forms.model'; import { SubmissionFormsModel } from '../../../../../../core/config/models/config-submission-forms.model';
import { FormService } from '../../../../form.service'; import { FormService } from '../../../../form.service';
import { FormComponent } from '../../../../form.component'; 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 { hasValue, isEmpty, isNotEmpty, isNotNull } from '../../../../../empty.util';
import { shrinkInOut } from '../../../../../animations/shrink'; 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 { hasOnlyEmptyProperties } from '../../../../../object.util';
import { VocabularyService } from '../../../../../../core/submission/vocabularies/vocabulary.service'; import { VocabularyService } from '../../../../../../core/submission/vocabularies/vocabulary.service';
import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model'; import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model';

View File

@@ -13,7 +13,7 @@ import { VocabularyService } from '../../../../../../core/submission/vocabularie
import { VocabularyServiceStub } from '../../../../../testing/vocabulary-service.stub'; import { VocabularyServiceStub } from '../../../../../testing/vocabulary-service.stub';
import { DsDynamicTagComponent } from './dynamic-tag.component'; import { DsDynamicTagComponent } from './dynamic-tag.component';
import { DynamicTagModel } from './dynamic-tag.model'; 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 { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model';
import { VocabularyEntry } from '../../../../../../core/submission/vocabularies/models/vocabulary-entry.model'; import { VocabularyEntry } from '../../../../../../core/submission/vocabularies/models/vocabulary-entry.model';
import { createTestComponent } from '../../../../../testing/utils.test'; import { createTestComponent } from '../../../../../testing/utils.test';

View File

@@ -9,7 +9,7 @@ import isEqual from 'lodash/isEqual';
import { VocabularyService } from '../../../../../../core/submission/vocabularies/vocabulary.service'; import { VocabularyService } from '../../../../../../core/submission/vocabularies/vocabulary.service';
import { DynamicTagModel } from './dynamic-tag.model'; 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 { hasValue, isNotEmpty } from '../../../../../empty.util';
import { environment } from '../../../../../../../environments/environment'; import { environment } from '../../../../../../../environments/environment';
import { getFirstSucceededRemoteDataPayload } from '../../../../../../core/shared/operators'; import { getFirstSucceededRemoteDataPayload } from '../../../../../../core/shared/operators';

View File

@@ -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 { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { VarDirective } from '../../../../../utils/var.directive'; import { VarDirective } from '../../../../../utils/var.directive';
import { TranslateModule } from '@ngx-translate/core'; 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 { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core';
import { PaginatedSearchOptions } from '../../../../../search/models/paginated-search-options.model'; import { PaginatedSearchOptions } from '../../../../../search/models/paginated-search-options.model';
import { SearchConfigurationService } from '../../../../../../core/shared/search/search-configuration.service'; import { SearchConfigurationService } from '../../../../../../core/shared/search/search-configuration.service';
import { of as observableOf } from 'rxjs'; import { of as observableOf, EMPTY } from 'rxjs';
import { import {
createFailedRemoteDataObject$, createFailedRemoteDataObject$,
createPendingRemoteDataObject$, createPendingRemoteDataObject$,
@@ -22,11 +24,13 @@ import { SelectableListService } from '../../../../../object-list/selectable-lis
import { Item } from '../../../../../../core/shared/item.model'; import { Item } from '../../../../../../core/shared/item.model';
import { Collection } from '../../../../../../core/shared/collection.model'; import { Collection } from '../../../../../../core/shared/collection.model';
import { RelationshipOptions } from '../../../models/relationship-options.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 { createPaginatedList } from '../../../../../testing/utils.test';
import { PaginationService } from '../../../../../../core/pagination/pagination.service'; import { PaginationService } from '../../../../../../core/pagination/pagination.service';
import { PaginationServiceStub } from '../../../../../testing/pagination-service.stub'; import { PaginationServiceStub } from '../../../../../testing/pagination-service.stub';
import { ItemType } from '../../../../../../core/shared/item-relationships/item-type.model'; 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', () => { describe('DsDynamicLookupRelationExternalSourceTabComponent', () => {
let component: DsDynamicLookupRelationExternalSourceTabComponent; let component: DsDynamicLookupRelationExternalSourceTabComponent;
@@ -187,12 +191,13 @@ describe('DsDynamicLookupRelationExternalSourceTabComponent', () => {
describe('import', () => { describe('import', () => {
beforeEach(() => { beforeEach(() => {
spyOn(modalService, 'open').and.returnValue(Object.assign({ componentInstance: Object.assign({ importedObject: new EventEmitter<any>() }) })); spyOn(modalService, 'open').and.returnValue(Object.assign({ componentInstance: Object.assign({ importedObject: new EventEmitter<any>(), compRef$: EMPTY }) }));
component.modalRef = modalService.open(ThemedExternalSourceEntryImportModalComponent, { size: 'lg', container: 'ds-dynamic-lookup-relation-modal' });
component.import(externalEntries[0]); component.import(externalEntries[0]);
}); });
it('should open a new ExternalSourceEntryImportModalComponent', () => { it('should open a new ExternalSourceEntryImportModalComponent', () => {
expect(modalService.open).toHaveBeenCalledWith(ExternalSourceEntryImportModalComponent, jasmine.any(Object)); expect(modalService.open).toHaveBeenCalledWith(ThemedExternalSourceEntryImportModalComponent, jasmine.any(Object));
}); });
}); });
}); });

View File

@@ -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 { SEARCH_CONFIG_SERVICE } from '../../../../../../my-dspace-page/my-dspace-page.component';
import { SearchConfigurationService } from '../../../../../../core/shared/search/search-configuration.service'; import { SearchConfigurationService } from '../../../../../../core/shared/search/search-configuration.service';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
@@ -16,7 +16,8 @@ import { PaginationComponentOptions } from '../../../../../pagination/pagination
import { RelationshipOptions } from '../../../models/relationship-options.model'; import { RelationshipOptions } from '../../../models/relationship-options.model';
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
import { ExternalSourceEntryImportModalComponent } from './external-source-entry-import-modal/external-source-entry-import-modal.component'; 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 { SelectableListService } from '../../../../../object-list/selectable-list/selectable-list.service';
import { Item } from '../../../../../../core/shared/item.model'; import { Item } from '../../../../../../core/shared/item.model';
import { Collection } from '../../../../../../core/shared/collection.model'; import { Collection } from '../../../../../../core/shared/collection.model';
@@ -114,9 +115,9 @@ export class DsDynamicLookupRelationExternalSourceTabComponent implements OnInit
modalRef: NgbModalRef; 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 * The entity types compatible with the given external source
@@ -161,30 +162,40 @@ export class DsDynamicLookupRelationExternalSourceTabComponent implements OnInit
* @param entry The entry to import * @param entry The entry to import
*/ */
import(entry) { import(entry) {
this.modalRef = this.modalService.open(ExternalSourceEntryImportModalComponent, { this.modalRef = this.modalService.open(ThemedExternalSourceEntryImportModalComponent, {
size: 'lg', size: 'lg',
container: 'ds-dynamic-lookup-relation-modal' container: 'ds-dynamic-lookup-relation-modal'
}); });
const modalComp = this.modalRef.componentInstance;
modalComp.externalSourceEntry = entry; const modalComp$ = this.modalRef.componentInstance.compRef$.pipe(
modalComp.item = this.item; hasValueOperator(),
modalComp.collection = this.collection; map((compRef: ComponentRef<ExternalSourceEntryImportModalComponent>) => compRef.instance)
modalComp.relationship = this.relationship; );
modalComp.label = this.label;
modalComp.relatedEntityType = this.relatedEntityType; this.subs.push(modalComp$.subscribe((modalComp: ExternalSourceEntryImportModalComponent) => {
this.importObjectSub = modalComp.importedObject.subscribe((object) => { 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.selectableListService.selectSingle(this.listId, object);
this.importedObject.emit(object); this.importedObject.emit(object);
}); }));
} }
/** /**
* Unsubscribe from open subscriptions * Unsubscribe from open subscriptions
*/ */
ngOnDestroy(): void { ngOnDestroy(): void {
if (hasValue(this.importObjectSub)) { this.subs
this.importObjectSub.unsubscribe(); .filter((sub) => hasValue(sub))
} .forEach((sub) => sub.unsubscribe());
} }
/** /**

View File

@@ -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<ExternalSourceEntryImportModalComponent> {
protected getComponentName(): string {
return 'ExternalSourceEntryImportModalComponent';
}
protected importThemedComponent(themeName: string): Promise<any> {
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<any> {
return import(`./external-source-entry-import-modal.component`);
}
}

View File

@@ -1,7 +1,12 @@
import {Inject, InjectionToken} from '@angular/core'; import { Inject, InjectionToken } from '@angular/core';
import uniqueId from 'lodash/uniqueId'; import uniqueId from 'lodash/uniqueId';
import {DynamicFormControlLayout, DynamicFormControlRelation, MATCH_VISIBLE, OR_OPERATOR} from '@ng-dynamic-forms/core'; import {
DynamicFormControlLayout,
DynamicFormControlRelation,
MATCH_VISIBLE,
OR_OPERATOR
} from '@ng-dynamic-forms/core';
import { hasValue, isNotEmpty, isNotNull, isNotUndefined } from '../../../empty.util'; import { hasValue, isNotEmpty, isNotNull, isNotUndefined } from '../../../empty.util';
import { FormFieldModel } from '../models/form-field.model'; import { FormFieldModel } from '../models/form-field.model';
@@ -22,6 +27,12 @@ export const SUBMISSION_ID: InjectionToken<string> = new InjectionToken<string>(
export const CONFIG_DATA: InjectionToken<FormFieldModel> = new InjectionToken<FormFieldModel>('configData'); export const CONFIG_DATA: InjectionToken<FormFieldModel> = new InjectionToken<FormFieldModel>('configData');
export const INIT_FORM_VALUES: InjectionToken<any> = new InjectionToken<any>('initFormValues'); export const INIT_FORM_VALUES: InjectionToken<any> = new InjectionToken<any>('initFormValues');
export const PARSER_OPTIONS: InjectionToken<ParserOptions> = new InjectionToken<ParserOptions>('parserOptions'); export const PARSER_OPTIONS: InjectionToken<ParserOptions> = new InjectionToken<ParserOptions>('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 { export abstract class FieldParser {
@@ -43,7 +54,7 @@ export abstract class FieldParser {
public abstract modelFactory(fieldValue?: FormFieldMetadataValueObject, label?: boolean): any; public abstract modelFactory(fieldValue?: FormFieldMetadataValueObject, label?: boolean): any;
public parse() { 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.List)
&& (this.configData.input.type !== ParserType.Tag) && (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' * fields in type bind, made up of a 'match' outcome (make this field visible), an 'operator'
* (OR) and a 'when' condition (the bindValues array). * (OR) and a 'when' condition (the bindValues array).
* @param configuredTypeBindValues array of types from the submission definition (CONFIG_DATA) * @param configuredTypeBindValues array of types from the submission definition (CONFIG_DATA)
* @param typeField
* @private * @private
* @return DynamicFormControlRelation[] array with one relation in it, for type bind matching to show a field * @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); 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) { 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.validators = Object.assign({}, controlModel.validators, { pattern: regex });
controlModel.errorMessages = Object.assign( controlModel.errorMessages = Object.assign(
{}, {},

View File

@@ -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 { DynamicOneboxModel } from '../ds-dynamic-form-ui/models/onebox/dynamic-onebox.model';
import { DsDynamicInputModel } from '../ds-dynamic-form-ui/models/ds-dynamic-input.model'; import { DsDynamicInputModel } from '../ds-dynamic-form-ui/models/ds-dynamic-input.model';
import { ParserOptions } from './parser-options'; import { ParserOptions } from './parser-options';
import { FieldParser } from './field-parser';
describe('OneboxFieldParser test suite', () => { describe('OneboxFieldParser test suite', () => {
let field1: FormFieldModel; let field1: FormFieldModel;
@@ -101,4 +102,51 @@ describe('OneboxFieldParser test suite', () => {
expect(fieldModel instanceof DynamicOneboxModel).toBe(true); 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);
});
});
}); });

View File

@@ -5,6 +5,7 @@ import {
DsDynamicTextAreaModel, DsDynamicTextAreaModel,
DsDynamicTextAreaModelConfig DsDynamicTextAreaModelConfig
} from '../ds-dynamic-form-ui/models/ds-dynamic-textarea.model'; } from '../ds-dynamic-form-ui/models/ds-dynamic-textarea.model';
import { environment } from '../../../../../environments/environment';
export class TextareaFieldParser extends FieldParser { export class TextareaFieldParser extends FieldParser {
@@ -20,6 +21,7 @@ export class TextareaFieldParser extends FieldParser {
}; };
textAreaModelConfig.rows = 10; textAreaModelConfig.rows = 10;
textAreaModelConfig.spellCheck = environment.form.spellCheck;
this.setValues(textAreaModelConfig, fieldValue); this.setValues(textAreaModelConfig, fieldValue);
const textAreaModel = new DsDynamicTextAreaModel(textAreaModelConfig, layout); const textAreaModel = new DsDynamicTextAreaModel(textAreaModelConfig, layout);

View File

@@ -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 { ComponentFixture, fakeAsync, inject, TestBed, tick, waitForAsync, } from '@angular/core/testing';
import { Chips } from './models/chips.model'; import { Chips } from './models/chips.model';
import { UploaderService } from '../uploader/uploader.service';
import { ChipsComponent } from './chips.component'; import { ChipsComponent } from './chips.component';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { FormFieldMetadataValueObject } from '../form/builder/models/form-field-metadata-value.model'; import { FormFieldMetadataValueObject } from '../builder/models/form-field-metadata-value.model';
import { createTestComponent } from '../testing/utils.test'; import { createTestComponent } from '../../testing/utils.test';
import { AuthorityConfidenceStateDirective } from '../authority-confidence/authority-confidence-state.directive'; import { AuthorityConfidenceStateDirective } from '../directives/authority-confidence-state.directive';
import { TranslateModule } from '@ngx-translate/core'; 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 { SortablejsModule } from 'ngx-sortablejs';
import { environment } from '../../../environments/environment'; import { environment } from '../../../../environments/environment';
describe('ChipsComponent test suite', () => { describe('ChipsComponent test suite', () => {
@@ -41,7 +40,6 @@ describe('ChipsComponent test suite', () => {
providers: [ providers: [
ChangeDetectorRef, ChangeDetectorRef,
ChipsComponent, ChipsComponent,
UploaderService
], ],
schemas: [CUSTOM_ELEMENTS_SCHEMA] schemas: [CUSTOM_ELEMENTS_SCHEMA]
}); });

View File

@@ -5,7 +5,7 @@ import isObject from 'lodash/isObject';
import { Chips } from './models/chips.model'; import { Chips } from './models/chips.model';
import { ChipsItem } from './models/chips-item.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 { TranslateService } from '@ngx-translate/core';
import { Options } from 'sortablejs'; import { Options } from 'sortablejs';
import { BehaviorSubject } from 'rxjs'; import { BehaviorSubject } from 'rxjs';
@@ -33,7 +33,7 @@ export class ChipsComponent implements OnChanges {
constructor( constructor(
private cdr: ChangeDetectorRef, private cdr: ChangeDetectorRef,
private uploaderService: UploaderService, private dragService: DragService,
private translate: TranslateService) { private translate: TranslateService) {
this.options = { this.options = {
@@ -76,12 +76,12 @@ export class ChipsComponent implements OnChanges {
onDragStart(index) { onDragStart(index) {
this.isDragging.next(true); this.isDragging.next(true);
this.uploaderService.overrideDragOverPage(); this.dragService.overrideDragOverPage();
this.dragged = index; this.dragged = index;
} }
onDragEnd(event) { onDragEnd(event) {
this.uploaderService.allowDragOverPage(); this.dragService.allowDragOverPage();
this.dragged = -1; this.dragged = -1;
this.chips.updateOrder(); this.chips.updateOrder();
this.isDragging.next(false); this.isDragging.next(false);

View File

@@ -1,5 +1,5 @@
import { ChipsItem, ChipsItemIcon } from './chips-item.model'; 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', () => { describe('ChipsItem model test suite', () => {
let item: ChipsItem; let item: ChipsItem;

View File

@@ -1,9 +1,9 @@
import isObject from 'lodash/isObject'; import isObject from 'lodash/isObject';
import uniqueId from 'lodash/uniqueId'; import uniqueId from 'lodash/uniqueId';
import { hasValue, isNotEmpty } from '../../empty.util'; import { hasValue, isNotEmpty } from '../../../empty.util';
import { FormFieldMetadataValueObject } from '../../form/builder/models/form-field-metadata-value.model'; import { FormFieldMetadataValueObject } from '../../builder/models/form-field-metadata-value.model';
import { ConfidenceType } from '../../../core/shared/confidence-type'; import { ConfidenceType } from '../../../../core/shared/confidence-type';
import { PLACEHOLDER_PARENT_METADATA } from '../../form/builder/ds-dynamic-form-ui/ds-dynamic-form-constants'; import { PLACEHOLDER_PARENT_METADATA } from '../../builder/ds-dynamic-form-ui/ds-dynamic-form-constants';
export interface ChipsItemIcon { export interface ChipsItemIcon {
metadata: string; metadata: string;

View File

@@ -1,6 +1,6 @@
import { Chips } from './chips.model'; import { Chips } from './chips.model';
import { ChipsItem } from './chips-item.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', () => { describe('Chips model test suite', () => {
let items: any[]; let items: any[];

View File

@@ -3,11 +3,11 @@ import isEqual from 'lodash/isEqual';
import isObject from 'lodash/isObject'; import isObject from 'lodash/isObject';
import { BehaviorSubject } from 'rxjs'; import { BehaviorSubject } from 'rxjs';
import { ChipsItem, ChipsItemIcon } from './chips-item.model'; import { ChipsItem, ChipsItemIcon } from './chips-item.model';
import { hasValue, isNotEmpty } from '../../empty.util'; import { hasValue, isNotEmpty } from '../../../empty.util';
import { MetadataIconConfig } from '../../../../config/submission-config.interface'; import { MetadataIconConfig } from '../../../../../config/submission-config.interface';
import { FormFieldMetadataValueObject } from '../../form/builder/models/form-field-metadata-value.model'; import { FormFieldMetadataValueObject } from '../../builder/models/form-field-metadata-value.model';
import { VocabularyEntry } from '../../../core/submission/vocabularies/models/vocabulary-entry.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 { PLACEHOLDER_PARENT_METADATA } from '../../builder/ds-dynamic-form-ui/ds-dynamic-form-constants';
export class Chips { export class Chips {
chipsItems: BehaviorSubject<ChipsItem[]>; chipsItems: BehaviorSubject<ChipsItem[]>;

View File

@@ -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 { import {
AfterViewInit, AfterViewInit,
Directive, Directive,
@@ -13,13 +21,13 @@ import {
import findIndex from 'lodash/findIndex'; import findIndex from 'lodash/findIndex';
import { VocabularyEntry } from '../../core/submission/vocabularies/models/vocabulary-entry.model'; import { VocabularyEntry } from '../../../core/submission/vocabularies/models/vocabulary-entry.model';
import { FormFieldMetadataValueObject } from '../form/builder/models/form-field-metadata-value.model'; import { FormFieldMetadataValueObject } from '../builder/models/form-field-metadata-value.model';
import { ConfidenceType } from '../../core/shared/confidence-type'; import { ConfidenceType } from '../../../core/shared/confidence-type';
import { isNotEmpty, isNull } from '../empty.util'; import { isNotEmpty, isNull } from '../../empty.util';
import { ConfidenceIconConfig } from '../../../config/submission-config.interface'; import { ConfidenceIconConfig } from '../../../../config/submission-config.interface';
import { environment } from '../../../environments/environment'; import { environment } from '../../../../environments/environment';
import { VocabularyEntryDetail } from '../../core/submission/vocabularies/models/vocabulary-entry-detail.model'; 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 * Directive to add to the element a bootstrap utility class based on metadata confidence value

View File

@@ -2,10 +2,7 @@ import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { FormComponent } from './form.component'; import { FormComponent } from './form.component';
import { DsDynamicFormComponent } from './builder/ds-dynamic-form-ui/ds-dynamic-form.component'; import { DsDynamicFormComponent } from './builder/ds-dynamic-form-ui/ds-dynamic-form.component';
import { import { DsDynamicFormControlContainerComponent, dsDynamicFormControlMapFn } from './builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component';
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 { 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 { 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'; 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 { SharedModule } from '../shared.module';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { SearchModule } from '../search/search.module'; 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 { 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 { 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 { 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 { CustomSwitchComponent } from './builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component';
import { DynamicFormsNGBootstrapUIModule } from '@ng-dynamic-forms/ui-ng-bootstrap'; 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 = [ const COMPONENTS = [
CustomSwitchComponent, CustomSwitchComponent,
@@ -53,12 +61,21 @@ const COMPONENTS = [
ExistingMetadataListElementComponent, ExistingMetadataListElementComponent,
ExistingRelationListElementComponent, ExistingRelationListElementComponent,
ExternalSourceEntryImportModalComponent, ExternalSourceEntryImportModalComponent,
FormComponent FormComponent,
ChipsComponent,
NumberPickerComponent,
VocabularyTreeviewComponent,
ThemedExternalSourceEntryImportModalComponent
];
const DIRECTIVES = [
AuthorityConfidenceStateDirective,
]; ];
@NgModule({ @NgModule({
declarations: [ declarations: [
...COMPONENTS ...COMPONENTS,
...DIRECTIVES,
], ],
imports: [ imports: [
CommonModule, CommonModule,
@@ -66,16 +83,27 @@ const COMPONENTS = [
DynamicFormsNGBootstrapUIModule, DynamicFormsNGBootstrapUIModule,
SearchModule, SearchModule,
SharedModule, SharedModule,
TranslateModule TranslateModule,
SortablejsModule,
NgxMaskModule.forRoot(),
], ],
exports: [ exports: [
...COMPONENTS ...COMPONENTS,
...DIRECTIVES,
], ],
providers: [ providers: [
{ {
provide: DYNAMIC_FORM_CONTROL_MAP_FN, provide: DYNAMIC_FORM_CONTROL_MAP_FN,
useValue: dsDynamicFormControlMapFn useValue: dsDynamicFormControlMapFn
} },
...DYNAMIC_MATCHER_PROVIDERS,
VocabularyTreeviewService,
DynamicFormLayoutService,
DynamicFormService,
DynamicFormValidationService,
FormBuilderService,
DsDynamicTypeBindRelationService,
FormService,
] ]
}) })
export class FormModule { export class FormModule {

View File

@@ -2,12 +2,11 @@
import { ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { ComponentFixture, inject, TestBed, waitForAsync, } from '@angular/core/testing'; import { ComponentFixture, inject, TestBed, waitForAsync, } from '@angular/core/testing';
import { UploaderService } from '../uploader/uploader.service';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { NumberPickerComponent } from './number-picker.component'; import { NumberPickerComponent } from './number-picker.component';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { createTestComponent } from '../testing/utils.test'; import { createTestComponent } from '../../testing/utils.test';
describe('NumberPickerComponent test suite', () => { describe('NumberPickerComponent test suite', () => {
@@ -33,7 +32,6 @@ describe('NumberPickerComponent test suite', () => {
providers: [ providers: [
ChangeDetectorRef, ChangeDetectorRef,
NumberPickerComponent, NumberPickerComponent,
UploaderService
], ],
schemas: [CUSTOM_ELEMENTS_SCHEMA] schemas: [CUSTOM_ELEMENTS_SCHEMA]
}); });

View File

@@ -1,6 +1,6 @@
import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output, SimpleChanges, } from '@angular/core'; import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output, SimpleChanges, } from '@angular/core';
import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR } from '@angular/forms';
import { isEmpty } from '../empty.util'; import { isEmpty } from '../../empty.util';
@Component({ @Component({
selector: 'ds-number-picker', selector: 'ds-number-picker',

View File

@@ -1,7 +1,7 @@
/* eslint-disable max-classes-per-file */ /* eslint-disable max-classes-per-file */
import { BehaviorSubject } from 'rxjs'; import { BehaviorSubject } from 'rxjs';
import { VocabularyEntryDetail } from '../../core/submission/vocabularies/models/vocabulary-entry-detail.model'; import { VocabularyEntryDetail } from '../../../core/submission/vocabularies/models/vocabulary-entry-detail.model';
import { PageInfo } from '../../core/shared/page-info.model'; import { PageInfo } from '../../../core/shared/page-info.model';
export const LOAD_MORE = 'LOAD_MORE'; export const LOAD_MORE = 'LOAD_MORE';
export const LOAD_MORE_ROOT = 'LOAD_MORE_ROOT'; export const LOAD_MORE_ROOT = 'LOAD_MORE_ROOT';

View File

@@ -8,18 +8,18 @@ import { TranslateModule } from '@ngx-translate/core';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { provideMockStore } from '@ngrx/store/testing'; 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 { VocabularyTreeviewComponent } from './vocabulary-treeview.component';
import { VocabularyTreeviewService } from './vocabulary-treeview.service'; 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 { TreeviewFlatNode } from './vocabulary-treeview-node.model';
import { FormFieldMetadataValueObject } from '../form/builder/models/form-field-metadata-value.model'; import { FormFieldMetadataValueObject } from '../builder/models/form-field-metadata-value.model';
import { VocabularyOptions } from '../../core/submission/vocabularies/models/vocabulary-options.model'; import { VocabularyOptions } from '../../../core/submission/vocabularies/models/vocabulary-options.model';
import { PageInfo } from '../../core/shared/page-info.model'; import { PageInfo } from '../../../core/shared/page-info.model';
import { VocabularyEntry } from '../../core/submission/vocabularies/models/vocabulary-entry.model'; import { VocabularyEntry } from '../../../core/submission/vocabularies/models/vocabulary-entry.model';
import { AuthTokenInfo } from '../../core/auth/models/auth-token-info.model'; import { AuthTokenInfo } from '../../../core/auth/models/auth-token-info.model';
import { authReducer } from '../../core/auth/auth.reducer'; import { authReducer } from '../../../core/auth/auth.reducer';
import { storeModuleConfig } from '../../app.reducer'; import { storeModuleConfig } from '../../../app.reducer';
describe('VocabularyTreeviewComponent test suite', () => { describe('VocabularyTreeviewComponent test suite', () => {

View File

@@ -7,17 +7,17 @@ import { Observable, Subscription } from 'rxjs';
import { select, Store } from '@ngrx/store'; import { select, Store } from '@ngrx/store';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { VocabularyEntryDetail } from '../../core/submission/vocabularies/models/vocabulary-entry-detail.model'; import { VocabularyEntryDetail } from '../../../core/submission/vocabularies/models/vocabulary-entry-detail.model';
import { hasValue, isEmpty, isNotEmpty } from '../empty.util'; import { hasValue, isEmpty, isNotEmpty } from '../../empty.util';
import { isAuthenticated } from '../../core/auth/selectors'; import { isAuthenticated } from '../../../core/auth/selectors';
import { VocabularyTreeviewService } from './vocabulary-treeview.service'; import { VocabularyTreeviewService } from './vocabulary-treeview.service';
import { LOAD_MORE, LOAD_MORE_ROOT, TreeviewFlatNode, TreeviewNode } from './vocabulary-treeview-node.model'; import { LOAD_MORE, LOAD_MORE_ROOT, TreeviewFlatNode, TreeviewNode } from './vocabulary-treeview-node.model';
import { VocabularyOptions } from '../../core/submission/vocabularies/models/vocabulary-options.model'; import { VocabularyOptions } from '../../../core/submission/vocabularies/models/vocabulary-options.model';
import { PageInfo } from '../../core/shared/page-info.model'; import { PageInfo } from '../../../core/shared/page-info.model';
import { VocabularyEntry } from '../../core/submission/vocabularies/models/vocabulary-entry.model'; import { VocabularyEntry } from '../../../core/submission/vocabularies/models/vocabulary-entry.model';
import { VocabularyTreeFlattener } from './vocabulary-tree-flattener'; import { VocabularyTreeFlattener } from './vocabulary-tree-flattener';
import { VocabularyTreeFlatDataSource } from './vocabulary-tree-flat-data-source'; 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 * Component that show a hierarchical vocabulary in a tree view

View File

@@ -5,15 +5,15 @@ import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-transla
import { cold, getTestScheduler, hot } from 'jasmine-marbles'; import { cold, getTestScheduler, hot } from 'jasmine-marbles';
import { VocabularyTreeviewService } from './vocabulary-treeview.service'; import { VocabularyTreeviewService } from './vocabulary-treeview.service';
import { VocabularyService } from '../../core/submission/vocabularies/vocabulary.service'; import { VocabularyService } from '../../../core/submission/vocabularies/vocabulary.service';
import { TranslateLoaderMock } from '../mocks/translate-loader.mock'; import { TranslateLoaderMock } from '../../mocks/translate-loader.mock';
import { VocabularyOptions } from '../../core/submission/vocabularies/models/vocabulary-options.model'; 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 { LOAD_MORE_NODE, LOAD_MORE_ROOT_NODE, TreeviewFlatNode, TreeviewNode } from './vocabulary-treeview-node.model';
import { PageInfo } from '../../core/shared/page-info.model'; import { PageInfo } from '../../../core/shared/page-info.model';
import { VocabularyEntryDetail } from '../../core/submission/vocabularies/models/vocabulary-entry-detail.model'; import { VocabularyEntryDetail } from '../../../core/submission/vocabularies/models/vocabulary-entry-detail.model';
import { buildPaginatedList } from '../../core/data/paginated-list.model'; import { buildPaginatedList } from '../../../core/data/paginated-list.model';
import { createSuccessfulRemoteDataObject } from '../remote-data.utils'; import { createSuccessfulRemoteDataObject } from '../../remote-data.utils';
import { VocabularyEntry } from '../../core/submission/vocabularies/models/vocabulary-entry.model'; import { VocabularyEntry } from '../../../core/submission/vocabularies/models/vocabulary-entry.model';
import { expand, map, switchMap } from 'rxjs/operators'; import { expand, map, switchMap } from 'rxjs/operators';
import { from as observableFrom } from 'rxjs'; import { from as observableFrom } from 'rxjs';

View File

@@ -10,17 +10,17 @@ import {
TreeviewFlatNode, TreeviewFlatNode,
TreeviewNode TreeviewNode
} from './vocabulary-treeview-node.model'; } from './vocabulary-treeview-node.model';
import { VocabularyEntry } from '../../core/submission/vocabularies/models/vocabulary-entry.model'; import { VocabularyEntry } from '../../../core/submission/vocabularies/models/vocabulary-entry.model';
import { VocabularyService } from '../../core/submission/vocabularies/vocabulary.service'; import { VocabularyService } from '../../../core/submission/vocabularies/vocabulary.service';
import { PageInfo } from '../../core/shared/page-info.model'; import { PageInfo } from '../../../core/shared/page-info.model';
import { isEmpty, isNotEmpty } from '../empty.util'; import { isEmpty, isNotEmpty } from '../../empty.util';
import { VocabularyOptions } from '../../core/submission/vocabularies/models/vocabulary-options.model'; import { VocabularyOptions } from '../../../core/submission/vocabularies/models/vocabulary-options.model';
import { import {
getFirstSucceededRemoteDataPayload, getFirstSucceededRemoteDataPayload,
getFirstSucceededRemoteListPayload getFirstSucceededRemoteListPayload
} from '../../core/shared/operators'; } from '../../../core/shared/operators';
import { PaginatedList } from '../../core/data/paginated-list.model'; import { PaginatedList } from '../../../core/data/paginated-list.model';
import { VocabularyEntryDetail } from '../../core/submission/vocabularies/models/vocabulary-entry-detail.model'; import { VocabularyEntryDetail } from '../../../core/submission/vocabularies/models/vocabulary-entry-detail.model';
/** /**
* A service that provides methods to deal with vocabulary tree * A service that provides methods to deal with vocabulary tree

View File

@@ -12,8 +12,11 @@ import { ExternalLinkMenuItemComponent } from './menu-item/external-link-menu-it
const COMPONENTS = [ const COMPONENTS = [
MenuSectionComponent, MenuSectionComponent,
MenuComponent, MenuComponent,
LinkMenuItemComponent, ];
const ENTRY_COMPONENTS = [
TextMenuItemComponent, TextMenuItemComponent,
LinkMenuItemComponent,
OnClickMenuItemComponent, OnClickMenuItemComponent,
ExternalLinkMenuItemComponent, ExternalLinkMenuItemComponent,
]; ];
@@ -32,10 +35,12 @@ const PROVIDERS = [
...MODULES ...MODULES
], ],
declarations: [ declarations: [
...COMPONENTS ...COMPONENTS,
...ENTRY_COMPONENTS,
], ],
providers: [ providers: [
...PROVIDERS ...PROVIDERS,
...ENTRY_COMPONENTS,
], ],
exports: [ exports: [
...COMPONENTS ...COMPONENTS

View File

@@ -0,0 +1 @@
<div class="alert d-block {{ object?.notificationType }} m-0">{{ object?.message | translate }}</div>

View File

@@ -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<ListableNotificationObjectComponent>;
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();
});
});

View File

@@ -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<ListableNotificationObject> {
}

View File

@@ -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<ListableObject>)[] {
return [...this.renderTypes, this.constructor as GenericConstructor<ListableObject>];
}
}

View File

@@ -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');

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