diff --git a/.browserslistrc b/.browserslistrc
index f8a421c330..427441dc93 100644
--- a/.browserslistrc
+++ b/.browserslistrc
@@ -2,10 +2,16 @@
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
+# For the full list of supported browsers by the Angular framework, please see:
+# https://angular.io/guide/browser-support
+
# You can see what browsers were selected by your queries by running:
# npx browserslist
-> 0.5%
-last 2 versions
+last 1 Chrome version
+last 1 Firefox version
+last 2 Edge major versions
+last 2 Safari major versions
+last 2 iOS major versions
Firefox ESR
-not IE 9-11 # For IE 9-11 support, remove 'not'.
+not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line.
diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644
index 0000000000..b95b54b979
--- /dev/null
+++ b/.eslintrc.json
@@ -0,0 +1,229 @@
+{
+ "root": true,
+ "plugins": [
+ "@typescript-eslint",
+ "@angular-eslint/eslint-plugin",
+ "eslint-plugin-import",
+ "eslint-plugin-jsdoc",
+ "eslint-plugin-deprecation",
+ "unused-imports",
+ "eslint-plugin-lodash"
+ ],
+ "overrides": [
+ {
+ "files": [
+ "*.ts"
+ ],
+ "parserOptions": {
+ "project": [
+ "./tsconfig.json",
+ "./cypress/tsconfig.json"
+ ],
+ "createDefaultProgram": true
+ },
+ "extends": [
+ "eslint:recommended",
+ "plugin:@typescript-eslint/recommended",
+ "plugin:@typescript-eslint/recommended-requiring-type-checking",
+ "plugin:@angular-eslint/recommended",
+ "plugin:@angular-eslint/template/process-inline-templates"
+ ],
+ "rules": {
+ "max-classes-per-file": [
+ "error",
+ 1
+ ],
+ "comma-dangle": [
+ "off",
+ "always-multiline"
+ ],
+ "eol-last": [
+ "error",
+ "always"
+ ],
+ "no-console": [
+ "error",
+ {
+ "allow": [
+ "log",
+ "warn",
+ "dir",
+ "timeLog",
+ "assert",
+ "clear",
+ "count",
+ "countReset",
+ "group",
+ "groupEnd",
+ "table",
+ "debug",
+ "info",
+ "dirxml",
+ "error",
+ "groupCollapsed",
+ "Console",
+ "profile",
+ "profileEnd",
+ "timeStamp",
+ "context"
+ ]
+ }
+ ],
+ "curly": "error",
+ "brace-style": [
+ "error",
+ "1tbs",
+ {
+ "allowSingleLine": true
+ }
+ ],
+ "eqeqeq": [
+ "error",
+ "always",
+ {
+ "null": "ignore"
+ }
+ ],
+ "radix": "error",
+ "guard-for-in": "error",
+ "no-bitwise": "error",
+ "no-restricted-imports": "error",
+ "no-caller": "error",
+ "no-debugger": "error",
+ "no-redeclare": "error",
+ "no-eval": "error",
+ "no-fallthrough": "error",
+ "no-trailing-spaces": "error",
+ "space-infix-ops": "error",
+ "keyword-spacing": "error",
+ "no-var": "error",
+ "no-unused-expressions": [
+ "error",
+ {
+ "allowTernary": true
+ }
+ ],
+ "prefer-const": "off", // todo: re-enable & fix errors (more strict than it used to be in TSLint)
+ "prefer-spread": "off",
+ "no-underscore-dangle": "off",
+
+ // todo: disabled rules from eslint:recommended, consider re-enabling & fixing
+ "no-prototype-builtins": "off",
+ "no-useless-escape": "off",
+ "no-case-declarations": "off",
+ "no-extra-boolean-cast": "off",
+
+ "@angular-eslint/directive-selector": [
+ "error",
+ {
+ "type": "attribute",
+ "prefix": "ds",
+ "style": "camelCase"
+ }
+ ],
+ "@angular-eslint/component-selector": [
+ "error",
+ {
+ "type": "element",
+ "prefix": "ds",
+ "style": "kebab-case"
+ }
+ ],
+ "@angular-eslint/pipe-prefix": [
+ "error",
+ {
+ "prefixes": [
+ "ds"
+ ]
+ }
+ ],
+ "@angular-eslint/no-attribute-decorator": "error",
+ "@angular-eslint/no-forward-ref": "error",
+ "@angular-eslint/no-output-native": "warn",
+ "@angular-eslint/no-output-on-prefix": "warn",
+ "@angular-eslint/no-conflicting-lifecycle": "warn",
+
+ "@typescript-eslint/no-inferrable-types":[
+ "error",
+ {
+ "ignoreParameters": true
+ }
+ ],
+ "@typescript-eslint/quotes": [
+ "error",
+ "single",
+ {
+ "avoidEscape": true,
+ "allowTemplateLiterals": true
+ }
+ ],
+ "@typescript-eslint/semi": "error",
+ "@typescript-eslint/no-shadow": "error",
+ "@typescript-eslint/dot-notation": "error",
+ "@typescript-eslint/consistent-type-definitions": "error",
+ "@typescript-eslint/prefer-function-type": "error",
+ "@typescript-eslint/naming-convention": [
+ "error",
+ {
+ "selector": "property",
+ "format": null
+ }
+ ],
+ "@typescript-eslint/member-ordering": [
+ "error",
+ {
+ "default": [
+ "static-field",
+ "instance-field",
+ "static-method",
+ "instance-method"
+ ]
+ }
+ ],
+ "@typescript-eslint/type-annotation-spacing": "error",
+ "@typescript-eslint/unified-signatures": "error",
+ "@typescript-eslint/ban-types": "warn", // todo: deal with {} type issues & re-enable
+ "@typescript-eslint/no-floating-promises": "warn",
+ "@typescript-eslint/no-misused-promises": "warn",
+ "@typescript-eslint/restrict-plus-operands": "warn",
+ "@typescript-eslint/unbound-method": "off",
+ "@typescript-eslint/ban-ts-comment": "off",
+ "@typescript-eslint/no-var-requires": "off",
+ "@typescript-eslint/no-unused-vars": "off",
+ "@typescript-eslint/no-unnecessary-type-assertion": "off",
+ "@typescript-eslint/no-explicit-any": "off",
+ "@typescript-eslint/no-unsafe-assignment": "off",
+ "@typescript-eslint/no-unsafe-member-access": "off",
+ "@typescript-eslint/no-unsafe-call": "off",
+ "@typescript-eslint/no-unsafe-argument": "off",
+ "@typescript-eslint/no-unsafe-return": "off",
+ "@typescript-eslint/restrict-template-expressions": "off",
+ "@typescript-eslint/require-await": "off",
+
+ "deprecation/deprecation": "warn",
+
+ "import/order": "off",
+ "import/no-deprecated": "warn",
+ "import/no-namespace": "error",
+ "unused-imports/no-unused-imports": "error",
+ "lodash/import-scope": [
+ "error",
+ "method"
+ ]
+ }
+ },
+ {
+ "files": [
+ "*.html"
+ ],
+ "extends": [
+ "plugin:@angular-eslint/template/recommended"
+ ],
+ "rules": {
+ // todo: re-enable & fix errors
+ "@angular-eslint/template/no-negated-async": "off",
+ "@angular-eslint/template/eqeqeq": "off"
+ }
+ }
+ ]
+}
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000000..406640bfcc
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,16 @@
+# By default, auto detect text files and perform LF normalization
+# This ensures code is always checked in with LF line endings
+* text=auto
+
+# JS and TS files must always use LF for Angular tools to work
+# Some Angular tools expect LF line endings, even on Windows.
+# This ensures Windows always checks out these files with LF line endings
+# We've copied many of these rules from https://github.com/angular/angular-cli/
+*.js eol=lf
+*.ts eol=lf
+*.json eol=lf
+*.json5 eol=lf
+*.css eol=lf
+*.scss eol=lf
+*.html eol=lf
+*.svg eol=lf
\ No newline at end of file
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index be15b0a507..e50105b879 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -1,7 +1,7 @@
## References
_Add references/links to any related issues or PRs. These may include:_
-* Fixes #[issue-number]
-* Requires DSpace/DSpace#[pr-number] (if a REST API PR is required to test this)
+* Fixes #`issue-number` (if this fixes an issue ticket)
+* Requires DSpace/DSpace#`pr-number` (if a REST API PR is required to test this)
## Description
Short summary of changes (1-2 sentences).
@@ -19,8 +19,10 @@ List of changes in this PR:
_This checklist provides a reminder of what we are going to look for when reviewing your PR. You need not complete this checklist prior to creating your PR (draft PRs are always welcome). If you are unsure about an item in the checklist, don't hesitate to ask. We're here to help!_
- [ ] My PR is small in size (e.g. less than 1,000 lines of code, not including comments & specs/tests), or I have provided reasons as to why that's not possible.
-- [ ] My PR passes [TSLint](https://palantir.github.io/tslint/) validation using `yarn run lint`
-- [ ] My PR doesn't introduce circular dependencies
+- [ ] My PR passes [ESLint](https://eslint.org/) validation using `yarn lint`
+- [ ] My PR doesn't introduce circular dependencies (verified via `yarn check-circ-deps`)
- [ ] My PR includes [TypeDoc](https://typedoc.org/) comments for _all new (or modified) public methods and classes_. It also includes TypeDoc for large or complex private methods.
- [ ] My PR passes all specs/tests and includes new/updated specs or tests based on the [Code Testing Guide](https://wiki.lyrasis.org/display/DSPACE/Code+Testing+Guide).
-- [ ] If my PR includes new, third-party dependencies (in `package.json`), I've made sure their licenses align with the [DSpace BSD License](https://github.com/DSpace/DSpace/blob/main/LICENSE) based on the [Licensing of Contributions](https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines#CodeContributionGuidelines-LicensingofContributions) documentation.
+- [ ] If my PR includes new libraries/dependencies (in `package.json`), I've made sure their licenses align with the [DSpace BSD License](https://github.com/DSpace/DSpace/blob/main/LICENSE) based on the [Licensing of Contributions](https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines#CodeContributionGuidelines-LicensingofContributions) documentation.
+- [ ] If my PR includes new features or configurations, I've provided basic technical documentation in the PR itself.
+- [ ] If my PR fixes an issue ticket, I've [linked them together](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue).
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 539fd740ee..f3b7aff689 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -6,34 +6,39 @@ name: Build
# Run this Build for all pushes / PRs to current branch
on: [push, pull_request]
+permissions:
+ contents: read # to fetch code (actions/checkout)
+
jobs:
tests:
runs-on: ubuntu-latest
env:
# The ci step will test the dspace-angular code against DSpace REST.
# Direct that step to utilize a DSpace REST service that has been started in docker.
- DSPACE_REST_HOST: localhost
+ DSPACE_REST_HOST: 127.0.0.1
DSPACE_REST_PORT: 8080
DSPACE_REST_NAMESPACE: '/server'
DSPACE_REST_SSL: false
+ # Spin up UI on 127.0.0.1 to avoid host resolution issues in e2e tests with Node 18+
+ DSPACE_UI_HOST: 127.0.0.1
# When Chrome version is specified, we pin to a specific version of Chrome
# Comment this out to use the latest release
#CHROME_VERSION: "90.0.4430.212-1"
strategy:
# Create a matrix of Node versions to test against (in parallel)
matrix:
- node-version: [12.x, 14.x]
+ node-version: [16.x, 18.x]
# Do NOT exit immediately if one matrix job fails
fail-fast: false
# These are the actual CI steps to perform per job
steps:
# https://github.com/actions/checkout
- name: Checkout codebase
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
# https://github.com/actions/setup-node
- name: Install Node.js ${{ matrix.node-version }}
- uses: actions/setup-node@v2
+ uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
@@ -58,7 +63,7 @@ jobs:
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Cache Yarn dependencies
- uses: actions/cache@v2
+ uses: actions/cache@v3
with:
# Cache entire Yarn cache directory (see previous step)
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
@@ -70,7 +75,10 @@ jobs:
run: yarn install --frozen-lockfile
- name: Run lint
- run: yarn run lint
+ run: yarn run lint --quiet
+
+ - name: Check for circular dependencies
+ run: yarn run check-circ-deps
- name: Run build
run: yarn run build:prod
@@ -79,11 +87,11 @@ jobs:
run: yarn run test:headless
# NOTE: Angular CLI only supports code coverage for specs. See https://github.com/angular/angular-cli/issues/6286
- # Upload coverage reports to Codecov (for Node v12 only)
+ # Upload coverage reports to Codecov (for one version of Node only)
# https://github.com/codecov/codecov-action
- name: Upload coverage to Codecov.io
- uses: codecov/codecov-action@v2
- if: matrix.node-version == '12.x'
+ uses: codecov/codecov-action@v3
+ if: matrix.node-version == '16.x'
# Using docker-compose start backend using CI configuration
# and load assetstore from a cached copy
@@ -97,7 +105,7 @@ jobs:
# https://github.com/cypress-io/github-action
# (NOTE: to run these e2e tests locally, just use 'ng e2e')
- name: Run e2e tests (integration tests)
- uses: cypress-io/github-action@v2
+ uses: cypress-io/github-action@v4
with:
# Run tests in Chrome, headless mode
browser: chrome
@@ -106,14 +114,14 @@ jobs:
start: yarn run serve:ssr
# Wait for backend & frontend to be available
# NOTE: We use the 'sites' REST endpoint to also ensure the database is ready
- wait-on: http://localhost:8080/server/api/core/sites, http://localhost:4000
+ wait-on: http://127.0.0.1:8080/server/api/core/sites, http://127.0.0.1:4000
# Wait for 2 mins max for everything to respond
wait-on-timeout: 120
# Cypress always creates a video of all e2e tests (whether they succeeded or failed)
# Save those in an Artifact
- name: Upload e2e test videos to Artifacts
- uses: actions/upload-artifact@v2
+ uses: actions/upload-artifact@v3
if: always()
with:
name: e2e-test-videos
@@ -122,18 +130,26 @@ jobs:
# If e2e tests fail, Cypress creates a screenshot of what happened
# Save those in an Artifact
- name: Upload e2e test failure screenshots to Artifacts
- uses: actions/upload-artifact@v2
+ uses: actions/upload-artifact@v3
if: failure()
with:
name: e2e-test-screenshots
path: cypress/screenshots
+ - name: Stop app (in case it stays up after e2e tests)
+ run: |
+ app_pid=$(lsof -t -i:4000)
+ if [[ ! -z $app_pid ]]; then
+ echo "App was still up! (PID: $app_pid)"
+ kill -9 $app_pid
+ fi
+
# Start up the app with SSR enabled (run in background)
- name: Start app in SSR (server-side rendering) mode
run: |
nohup yarn run serve:ssr &
printf 'Waiting for app to start'
- until curl --output /dev/null --silent --head --fail http://localhost:4000/home; do
+ until curl --output /dev/null --silent --head --fail http://127.0.0.1:4000/home; do
printf '.'
sleep 2
done
@@ -144,7 +160,7 @@ jobs:
# This step also prints entire HTML of homepage for easier debugging if grep fails.
- name: Verify SSR (server-side rendering)
run: |
- result=$(wget -O- -q http://localhost:4000/home)
+ result=$(wget -O- -q http://127.0.0.1:4000/home)
echo "$result"
echo "$result" | grep -oE "]*>" | grep DSpace
diff --git a/.github/workflows/codescan.yml b/.github/workflows/codescan.yml
new file mode 100644
index 0000000000..35a2e2d24a
--- /dev/null
+++ b/.github/workflows/codescan.yml
@@ -0,0 +1,49 @@
+# DSpace CodeQL code scanning configuration for GitHub
+# https://docs.github.com/en/code-security/code-scanning
+#
+# NOTE: Code scanning must be run separate from our default build.yml
+# because CodeQL requires a fresh build with all tests *disabled*.
+name: "Code Scanning"
+
+# Run this code scan for all pushes / PRs to main branch. Also run once a week.
+on:
+ push:
+ branches: [ main ]
+ pull_request:
+ branches: [ main ]
+ # Don't run if PR is only updating static documentation
+ paths-ignore:
+ - '**/*.md'
+ - '**/*.txt'
+ schedule:
+ - cron: "37 0 * * 1"
+
+jobs:
+ analyze:
+ name: Analyze Code
+ runs-on: ubuntu-latest
+ # Limit permissions of this GitHub action. Can only write to security-events
+ permissions:
+ actions: read
+ contents: read
+ security-events: write
+
+ steps:
+ # https://github.com/actions/checkout
+ - name: Checkout repository
+ uses: actions/checkout@v3
+
+ # Initializes the CodeQL tools for scanning.
+ # https://github.com/github/codeql-action
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v2
+ with:
+ languages: javascript
+
+ # Autobuild attempts to build any compiled languages
+ - name: Autobuild
+ uses: github/codeql-action/autobuild@v2
+
+ # Perform GitHub Code Scanning.
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v2
\ No newline at end of file
diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
index 00ec2fa8f7..908c5c34fd 100644
--- a/.github/workflows/docker.yml
+++ b/.github/workflows/docker.yml
@@ -12,6 +12,9 @@ on:
- 'dspace-**'
pull_request:
+permissions:
+ contents: read # to fetch code (actions/checkout)
+
jobs:
docker:
# Ensure this job never runs on forked repos. It's only executed for 'dspace/dspace-angular'
@@ -31,21 +34,29 @@ jobs:
# We turn off 'latest' tag by default.
TAGS_FLAVOR: |
latest=false
+ # Architectures / Platforms for which we will build Docker images
+ # If this is a PR, we ONLY build for AMD64. For PRs we only do a sanity check test to ensure Docker builds work.
+ # If this is NOT a PR (e.g. a tag or merge commit), also build for ARM64.
+ PLATFORMS: linux/amd64${{ github.event_name != 'pull_request' && ', linux/arm64' || '' }}
steps:
# https://github.com/actions/checkout
- name: Checkout codebase
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
# https://github.com/docker/setup-buildx-action
- name: Setup Docker Buildx
- uses: docker/setup-buildx-action@v1
+ uses: docker/setup-buildx-action@v2
+
+ # https://github.com/docker/setup-qemu-action
+ - name: Set up QEMU emulation to build for multiple architectures
+ uses: docker/setup-qemu-action@v2
# https://github.com/docker/login-action
- name: Login to DockerHub
# Only login if not a PR, as PRs only trigger a Docker build and not a push
if: github.event_name != 'pull_request'
- uses: docker/login-action@v1
+ uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
@@ -57,7 +68,7 @@ jobs:
# Get Metadata for docker_build step below
- name: Sync metadata (tags, labels) from GitHub to Docker for 'dspace-angular' image
id: meta_build
- uses: docker/metadata-action@v3
+ uses: docker/metadata-action@v4
with:
images: dspace/dspace-angular
tags: ${{ env.IMAGE_TAGS }}
@@ -66,10 +77,11 @@ jobs:
# https://github.com/docker/build-push-action
- name: Build and push 'dspace-angular' image
id: docker_build
- uses: docker/build-push-action@v2
+ uses: docker/build-push-action@v3
with:
context: .
file: ./Dockerfile
+ platforms: ${{ env.PLATFORMS }}
# For pull requests, we run the Docker build (to ensure no PR changes break the build),
# but we ONLY do an image push to DockerHub if it's NOT a PR
push: ${{ github.event_name != 'pull_request' }}
diff --git a/.github/workflows/issue_opened.yml b/.github/workflows/issue_opened.yml
index 6b9a273ab6..5d7c1c30f7 100644
--- a/.github/workflows/issue_opened.yml
+++ b/.github/workflows/issue_opened.yml
@@ -5,25 +5,22 @@ on:
issues:
types: [opened]
+permissions: {}
jobs:
automation:
runs-on: ubuntu-latest
steps:
# Add the new issue to a project board, if it needs triage
- # See https://github.com/marketplace/actions/create-project-card-action
- - name: Add issue to project board
+ # See https://github.com/actions/add-to-project
+ - name: Add issue to triage board
# Only add to project board if issue is flagged as "needs triage" or has no labels
# NOTE: By default we flag new issues as "needs triage" in our issue template
if: (contains(github.event.issue.labels.*.name, 'needs triage') || join(github.event.issue.labels.*.name) == '')
- uses: technote-space/create-project-card-action@v1
+ uses: actions/add-to-project@v0.3.0
# Note, the authentication token below is an ORG level Secret.
- # It must be created/recreated manually via a personal access token with "public_repo" and "admin:org" permissions
+ # It must be created/recreated manually via a personal access token with admin:org, project, public_repo permissions
# See: https://docs.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token#permissions-for-the-github_token
# This is necessary because the "DSpace Backlog" project is an org level project (i.e. not repo specific)
with:
- GITHUB_TOKEN: ${{ secrets.ORG_PROJECT_TOKEN }}
- PROJECT: DSpace Backlog
- COLUMN: Triage
- CHECK_ORG_PROJECT: true
- # Ignore errors
- continue-on-error: true
+ github-token: ${{ secrets.TRIAGE_PROJECT_TOKEN }}
+ project-url: https://github.com/orgs/DSpace/projects/24
diff --git a/.github/workflows/label_merge_conflicts.yml b/.github/workflows/label_merge_conflicts.yml
index dcbab18f1b..a840a4fd17 100644
--- a/.github/workflows/label_merge_conflicts.yml
+++ b/.github/workflows/label_merge_conflicts.yml
@@ -5,21 +5,32 @@ name: Check for merge conflicts
# NOTE: This means merge conflicts are only checked for when a PR is merged to main.
on:
push:
- branches:
- - main
+ branches: [ main ]
+ # So that the `conflict_label_name` is removed if conflicts are resolved,
+ # we allow this to run for `pull_request_target` so that github secrets are available.
+ pull_request_target:
+ types: [ synchronize ]
+
+permissions: {}
jobs:
triage:
+ # Ensure this job never runs on forked repos. It's only executed for 'dspace/dspace-angular'
+ if: github.repository == 'dspace/dspace-angular'
runs-on: ubuntu-latest
+ permissions:
+ pull-requests: write
steps:
- # See: https://github.com/mschilde/auto-label-merge-conflicts/
+ # See: https://github.com/prince-chrismc/label-merge-conflicts-action
- name: Auto-label PRs with merge conflicts
- uses: mschilde/auto-label-merge-conflicts@v2.0
+ uses: prince-chrismc/label-merge-conflicts-action@v2
# Add "merge conflict" label if a merge conflict is detected. Remove it when resolved.
# Note, the authentication token is created automatically
# See: https://docs.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token
with:
- CONFLICT_LABEL_NAME: 'merge conflict'
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- # Ignore errors
- continue-on-error: true
+ conflict_label_name: 'merge conflict'
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ conflict_comment: |
+ Hi @${author},
+ Conflicts have been detected against the base branch.
+ Please [resolve these conflicts](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/addressing-merge-conflicts/about-merge-conflicts) as soon as you can. Thanks!
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 026110f222..bdd0d4e589 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
+/.angular/cache
/__build__
/__server_build__
/node_modules
@@ -36,3 +37,5 @@ package-lock.json
.env
/nbproject/
+
+junit.xml
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000000..4e732302f4
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,46 @@
+# How to Contribute
+
+DSpace is a community built and supported project. We do not have a centralized development or support team, but have a dedicated group of volunteers who help us improve the software, documentation, resources, etc.
+
+* [Contribute new code via a Pull Request](#contribute-new-code-via-a-pull-request)
+* [Contribute documentation](#contribute-documentation)
+* [Help others on mailing lists or Slack](#help-others-on-mailing-lists-or-slack)
+* [Join a working or interest group](#join-a-working-or-interest-group)
+
+## Contribute new code via a Pull Request
+
+We accept [GitHub Pull Requests (PRs)](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork) at any time from anyone.
+Contributors to each release are recognized in our [Release Notes](https://wiki.lyrasis.org/display/DSDOC7x/Release+Notes).
+
+Code Contribution Checklist
+- [ ] PRs _should_ be smaller in size (ideally less than 1,000 lines of code, not including comments & tests)
+- [ ] PRs **must** pass [ESLint](https://eslint.org/) validation using `yarn lint`
+- [ ] PRs **must** not introduce circular dependencies (verified via `yarn check-circ-deps`)
+- [ ] PRs **must** include [TypeDoc](https://typedoc.org/) comments for _all new (or modified) public methods and classes_. Large or complex private methods should also have TypeDoc.
+- [ ] PRs **must** pass all automated pecs/tests and includes new/updated specs or tests based on the [Code Testing Guide](https://wiki.lyrasis.org/display/DSPACE/Code+Testing+Guide).
+- [ ] If a PR includes new libraries/dependencies (in `package.json`), then their software licenses **must** align with the [DSpace BSD License](https://github.com/DSpace/dspace-angular/blob/main/LICENSE) based on the [Licensing of Contributions](https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines#CodeContributionGuidelines-LicensingofContributions) documentation.
+- [ ] Basic technical documentation _should_ be provided for any new features or configuration, either in the PR itself or in the DSpace Wiki documentation.
+- [ ] If a PR fixes an issue ticket, please [link them together](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue).
+
+Additional details on the code contribution process can be found in our [Code Contribution Guidelines](https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines)
+
+## Contribute documentation
+
+DSpace Documentation is a collaborative effort in a shared Wiki. The latest documentation is at https://wiki.lyrasis.org/display/DSDOC7x
+
+If you find areas of the DSpace Documentation which you wish to improve, please request a Wiki account by emailing wikihelp@lyrasis.org.
+Once you have an account setup, contact @tdonohue (via [Slack](https://wiki.lyrasis.org/display/DSPACE/Slack) or email) for access to edit our Documentation.
+
+## Help others on mailing lists or Slack
+
+DSpace has our own [Slack](https://wiki.lyrasis.org/display/DSPACE/Slack) community and [Mailing Lists](https://wiki.lyrasis.org/display/DSPACE/Mailing+Lists) where discussions take place and questions are answered.
+Anyone is welcome to join and help others. We just ask you to follow our [Code of Conduct](https://www.lyrasis.org/about/Pages/Code-of-Conduct.aspx) (adopted via LYRASIS).
+
+## Join a working or interest group
+
+Most of the work in building/improving DSpace comes via [Working Groups](https://wiki.lyrasis.org/display/DSPACE/DSpace+Working+Groups) or [Interest Groups](https://wiki.lyrasis.org/display/DSPACE/DSpace+Interest+Groups).
+
+All working/interest groups are open to anyone to join and participate. A few key groups to be aware of include:
+
+* [DSpace 7 Working Group](https://wiki.lyrasis.org/display/DSPACE/DSpace+7+Working+Group) - This is the main (mostly volunteer) development team. We meet weekly to review our current development [project board](https://github.com/orgs/DSpace/projects), assigning tickets and/or PRs.
+* [DSpace Community Advisory Team (DCAT)](https://wiki.lyrasis.org/display/cmtygp/DSpace+Community+Advisory+Team) - This is an interest group for repository managers/administrators. We meet monthly to discuss DSpace, share tips & provide feedback back to developers.
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index 2d98971112..61d960e7d3 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,12 +1,21 @@
# This image will be published as dspace/dspace-angular
# See https://github.com/DSpace/dspace-angular/tree/main/docker for usage details
-FROM node:14-alpine
+FROM node:18-alpine
WORKDIR /app
ADD . /app/
EXPOSE 4000
+# Ensure Python and other build tools are available
+# These are needed to install some node modules, especially on linux/arm64
+RUN apk add --update python3 make g++ && rm -rf /var/cache/apk/*
+
# We run yarn install with an increased network timeout (5min) to avoid "ESOCKETTIMEDOUT" errors from hub.docker.com
# See, for example https://github.com/yarnpkg/yarn/issues/5540
RUN yarn install --network-timeout 300000
-CMD yarn run start:dev
+
+# On startup, run in DEVELOPMENT mode (this defaults to live reloading enabled, etc).
+# Listen / accept connections from all IP addresses.
+# NOTE: At this time it is only possible to run Docker container in Production mode
+# if you have a public IP. See https://github.com/DSpace/dspace-angular/issues/1485
+CMD yarn serve --host 0.0.0.0
diff --git a/README.md b/README.md
index 74010f3c5c..c90dc1d08f 100644
--- a/README.md
+++ b/README.md
@@ -35,7 +35,7 @@ https://wiki.lyrasis.org/display/DSDOC7x/Installing+DSpace
Quick start
-----------
-**Ensure you're running [Node](https://nodejs.org) `v12.x`, `v14.x` or `v16.x`, [npm](https://www.npmjs.com/) >= `v5.x` and [yarn](https://yarnpkg.com) == `v1.x`**
+**Ensure you're running [Node](https://nodejs.org) `v16.x` or `v18.x`, [npm](https://www.npmjs.com/) >= `v5.x` and [yarn](https://yarnpkg.com) == `v1.x`**
```bash
# clone the repo
@@ -90,7 +90,7 @@ Requirements
------------
- [Node.js](https://nodejs.org) and [yarn](https://yarnpkg.com)
-- Ensure you're running node `v12.x`, `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.
@@ -101,7 +101,7 @@ Installing
### Configuring
-Default configuration file is located in `config/` folder.
+Default runtime configuration file is located in `config/` folder. These configurations can be changed without rebuilding the distribution.
To override the default configuration values, create local files that override the parameters you need to change. You can use `config.example.yml` as a starting point.
@@ -167,6 +167,22 @@ These configuration sources are collected **at run time**, and written to `dist/
The configuration file can be externalized by using environment variable `DSPACE_APP_CONFIG_PATH`.
+#### Buildtime Configuring
+
+Buildtime configuration must defined before build in order to include in transpiled JavaScript. This is primarily for the server. These settings can be found under `src/environment/` folder.
+
+To override the default configuration values for development, create local file that override the build time parameters you need to change.
+
+- Create a new `environment.(dev or development).ts` file in `src/environment/` for a `development` environment;
+
+If needing to update default configurations values for production, update local file that override the build time parameters you need to change.
+
+- Update `environment.production.ts` file in `src/environment/` for a `production` environment;
+
+The environment object is provided for use as import in code and is extended with the runtime configuration on bootstrap of the application.
+
+> Take caution moving runtime configs into the buildtime configuration. They will be overwritten by what is defined in the runtime config on bootstrap.
+
#### Using environment variables in code
To use environment variables in a UI component, use:
@@ -183,7 +199,6 @@ or
import { environment } from '../environment.ts';
```
-
Running the app
---------------
@@ -193,7 +208,7 @@ After you have installed all dependencies you can now run the app. Run `yarn run
When building for production we're using Ahead of Time (AoT) compilation. With AoT, the browser downloads a pre-compiled version of the application, so it can render the application immediately, without waiting to compile the app first. The compiler is roughly half the size of Angular itself, so omitting it dramatically reduces the application payload.
-To build the app for production and start the server run:
+To build the app for production and start the server (in one command) run:
```bash
yarn start
@@ -207,6 +222,10 @@ yarn run build:prod
```
This will build the application and put the result in the `dist` folder. You can copy this folder to wherever you need it for your application server. If you will be using the built-in Express server, you'll also need a copy of the `node_modules` folder tucked inside your copy of `dist`.
+After building the app for production, it can be started by running:
+```bash
+yarn run serve:ssr
+```
### Running the application with Docker
NOTE: At this time, we do not have production-ready Docker images for DSpace.
@@ -268,11 +287,29 @@ E2E tests (aka integration tests) use [Cypress.io](https://www.cypress.io/). Con
The test files can be found in the `./cypress/integration/` folder.
-Before you can run e2e tests, two things are required:
-1. You MUST have a running backend (i.e. REST API). By default, the e2e tests look for this at http://localhost:8080/server/ or whatever `rest` backend is defined in your `config.prod.yml` or `config.yml`. You may override this using env variables, see [Configuring](#configuring).
-2. Your backend MUST include our Entities Test Data set. Some tests run against a (currently hardcoded) Community/Collection/Item UUID. These UUIDs are all valid for our Entities Test Data set. The Entities Test Data set may be installed easily via Docker, see https://github.com/DSpace/DSpace/tree/main/dspace/src/main/docker-compose#ingest-option-2-ingest-entities-test-data
+Before you can run e2e tests, two things are REQUIRED:
+1. You MUST be running the DSpace backend (i.e. REST API) locally. The e2e tests will *NOT* succeed if run against our demo REST API (https://api7.dspace.org/server/), as that server is uncontrolled and may have content added/removed at any time.
+ * After starting up your backend on localhost, make sure either your `config.prod.yml` or `config.dev.yml` has its `rest` settings defined to use that localhost backend.
+ * If you'd prefer, you may instead use environment variables as described at [Configuring](#configuring). For example:
+ ```
+ DSPACE_REST_SSL = false
+ DSPACE_REST_HOST = localhost
+ DSPACE_REST_PORT = 8080
+ ```
+2. Your backend MUST include our [Entities Test Data set](https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data). Some tests run against a specific Community/Collection/Item UUID. These UUIDs are all valid for our Entities Test Data set.
+ * (Recommended) The Entities Test Data set may be installed easily via Docker, see https://github.com/DSpace/DSpace/tree/main/dspace/src/main/docker-compose#ingest-option-2-ingest-entities-test-data
+ * Alternatively, the Entities Test Data set may be installed via a simple SQL import (e. g. `psql -U dspace < dspace7-entities-data.sql`). See instructions in link above.
-Run `ng e2e` to kick off the tests. This will start Cypress and allow you to select the browser you wish to use, as well as whether you wish to run all tests or an individual test file. Once you click run on test(s), this opens the [Cypress Test Runner](https://docs.cypress.io/guides/core-concepts/test-runner) to run your test(s) and show you the results.
+After performing the above setup, you can run the e2e tests using
+```
+ng e2e
+````
+NOTE: By default these tests will run against the REST API backend configured via environment variables or in `config.prod.yml`. If you'd rather it use `config.dev.yml`, just set the NODE_ENV environment variable like this:
+```
+NODE_ENV=development ng e2e
+```
+
+The `ng e2e` command will start Cypress and allow you to select the browser you wish to use, as well as whether you wish to run all tests or an individual test file. Once you click run on test(s), this opens the [Cypress Test Runner](https://docs.cypress.io/guides/core-concepts/test-runner) to run your test(s) and show you the results.
#### Writing E2E Tests
@@ -293,8 +330,11 @@ All E2E tests must be created under the `./cypress/integration/` folder, and mus
* In the [Cypress Test Runner](https://docs.cypress.io/guides/core-concepts/test-runner), you'll Cypress automatically visit the page. This first test will succeed, as all you are doing is making sure the _page exists_.
* From here, you can use the [Selector Playground](https://docs.cypress.io/guides/core-concepts/test-runner#Selector-Playground) in the Cypress Test Runner window to determine how to tell Cypress to interact with a specific HTML element on that page.
* Most commands start by telling Cypress to [get()](https://docs.cypress.io/api/commands/get) a specific element, using a CSS or jQuery style selector
+ * It's generally best not to rely on attributes like `class` and `id` in tests, as those are likely to change later on. Instead, you can add a `data-test` attribute to makes it clear that it's required for a test.
* Cypress can then do actions like [click()](https://docs.cypress.io/api/commands/click) an element, or [type()](https://docs.cypress.io/api/commands/type) text in an input field, etc.
- * Cypress can also validate that something occurs, using [should()](https://docs.cypress.io/api/commands/should) assertions.
+ * When running with server-side rendering enabled, the client first receives HTML without the JS; only once the page is rendered client-side do some elements (e.g. a button that toggles a Bootstrap dropdown) become fully interactive. This can trip up Cypress in some cases as it may try to `click` or `type` in an element that's not fully loaded yet, causing tests to fail.
+ * To work around this issue, define the attributes you use for Cypress selectors as `[attr.data-test]="'button' | ngBrowserOnly"`. This will only show the attribute in CSR HTML, forcing Cypress to wait until CSR is complete before interacting with the element.
+ * Cypress can also validate that something occurs, using [should()](https://docs.cypress.io/api/commands/should) assertions.
* Any time you save your test file, the Cypress Test Runner will reload & rerun it. This allows you can see your results quickly as you write the tests & correct any broken tests rapidly.
* Cypress also has a great guide on [writing your first test](https://on.cypress.io/writing-first-test) with much more info. Keep in mind, while the examples in the Cypress docs often involve Javascript files (.js), the same examples will work in our Typescript (.ts) e2e tests.
@@ -311,7 +351,7 @@ Documentation
Official DSpace documentation is available in the DSpace wiki at https://wiki.lyrasis.org/display/DSDOC7x/
-Some UI specific configuration documentation is also found in the [`./docs`](docs) folder of htis codebase.
+Some UI specific configuration documentation is also found in the [`./docs`](docs) folder of this codebase.
### Building code documentation
@@ -339,10 +379,10 @@ To get the most out of TypeScript, you'll need a TypeScript-aware editor. We've
- [Sublime Text](http://www.sublimetext.com/3)
- [Typescript-Sublime-Plugin](https://github.com/Microsoft/Typescript-Sublime-plugin#installation)
-Collaborating
+Contributing
-------------
-See [the guide on the wiki](https://wiki.lyrasis.org/display/DSPACE/DSpace+7+-+Angular+UI+Development#DSpace7-AngularUIDevelopment-Howtocontribute)
+See [Contributing documentation](CONTRIBUTING.md)
File Structure
--------------
diff --git a/angular.json b/angular.json
index a0a4cd8ea1..b32670ad77 100644
--- a/angular.json
+++ b/angular.json
@@ -17,7 +17,6 @@
"build": {
"builder": "@angular-builders/custom-webpack:browser",
"options": {
- "extractCss": true,
"preserveSymlinks": true,
"customWebpackConfig": {
"path": "./webpack/webpack.browser.ts",
@@ -26,12 +25,10 @@
}
},
"allowedCommonJsDependencies": [
- "angular2-text-mask",
"cerialize",
"core-js",
"lodash",
"jwt-decode",
- "url-parse",
"uuid",
"webfontloader",
"zone.js"
@@ -64,19 +61,31 @@
"bundleName": "dspace-theme"
}
],
- "scripts": []
+ "scripts": [],
+ "baseHref": "/"
},
"configurations": {
+ "development": {
+ "buildOptimizer": false,
+ "optimization": false,
+ "vendorChunk": true,
+ "extractLicenses": false,
+ "sourceMap": true,
+ "namedChunks": true
+ },
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.production.ts"
+ },
+ {
+ "replace": "src/config/store/devtools.ts",
+ "with": "src/config/store/devtools.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
- "extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
@@ -104,6 +113,9 @@
"port": 4000
},
"configurations": {
+ "development": {
+ "browserTarget": "dspace-angular:build:development"
+ },
"production": {
"browserTarget": "dspace-angular:build:production"
}
@@ -157,19 +169,6 @@
}
}
},
- "lint": {
- "builder": "@angular-devkit/build-angular:tslint",
- "options": {
- "tsConfig": [
- "tsconfig.app.json",
- "tsconfig.spec.json",
- "cypress/tsconfig.json"
- ],
- "exclude": [
- "**/node_modules/**"
- ]
- }
- },
"e2e": {
"builder": "@cypress/schematic:cypress",
"options": {
@@ -197,6 +196,10 @@
"tsConfig": "tsconfig.server.json"
},
"configurations": {
+ "development": {
+ "sourceMap": true,
+ "optimization": false
+ },
"production": {
"sourceMap": false,
"optimization": true,
@@ -204,6 +207,10 @@
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.production.ts"
+ },
+ {
+ "replace": "src/config/store/devtools.ts",
+ "with": "src/config/store/devtools.prod.ts"
}
]
}
@@ -253,12 +260,22 @@
"watch": true,
"headless": false
}
+ },
+ "lint": {
+ "builder": "@angular-eslint/builder:lint",
+ "options": {
+ "lintFilePatterns": [
+ "src/**/*.ts",
+ "src/**/*.html"
+ ]
+ }
}
}
}
},
"defaultProject": "dspace-angular",
"cli": {
- "analytics": false
+ "analytics": false,
+ "defaultCollection": "@angular-eslint/schematics"
}
-}
\ No newline at end of file
+}
diff --git a/config/config.example.yml b/config/config.example.yml
index ecb2a3cfb9..9abf167b90 100644
--- a/config/config.example.yml
+++ b/config/config.example.yml
@@ -2,7 +2,8 @@
debug: false
# Angular Universal server settings
-# NOTE: these must be 'synced' with the 'dspace.ui.url' setting in your backend's local.cfg.
+# NOTE: these settings define where Node.js will start your UI application. Therefore, these
+# "ui" settings usually specify a localhost port/URL which is later proxied to a public URL (using Apache or similar)
ui:
ssl: false
host: localhost
@@ -13,9 +14,12 @@ ui:
rateLimiter:
windowMs: 60000 # 1 minute
max: 500 # limit each IP to 500 requests per windowMs
+ # Trust X-FORWARDED-* headers from proxies (default = true)
+ useProxies: true
# The REST API server settings
-# NOTE: these must be 'synced' with the 'dspace.server.url' setting in your backend's local.cfg.
+# NOTE: these settings define which (publicly available) REST API to use. They are usually
+# 'synced' with the 'dspace.server.url' setting in your backend's local.cfg.
rest:
ssl: true
host: api7.dspace.org
@@ -51,6 +55,8 @@ auth:
# Form settings
form:
+ # Sets the spellcheck textarea attribute value
+ spellCheck: true
# NOTE: Map server-side validators to comparative Angular form validators
validatorMap:
required: required
@@ -139,6 +145,9 @@ languages:
- code: nl
label: Nederlands
active: true
+ - code: pl
+ label: Polski
+ active: true
- code: pt-PT
label: Português
active: true
@@ -148,6 +157,28 @@ languages:
- code: fi
label: Suomi
active: true
+ - code: sv
+ label: Svenska
+ active: true
+ - code: tr
+ label: Türkçe
+ active: true
+ - code: kk
+ label: Қазақ
+ active: true
+ - code: bn
+ label: বাংলা
+ active: true
+ - code: hi
+ label: हिंदी
+ active: true
+ - code: el
+ label: Ελληνικά
+ active: true
+ - code: uk
+ label: Yкраї́нська
+ active: true
+
# Browse-By Pages
browseBy:
@@ -157,11 +188,39 @@ browseBy:
fiveYearLimit: 30
# The absolute lowest year to display in the dropdown (only used when no lowest date can be found for all items)
defaultLowerLimit: 1900
+ # If true, thumbnail images for items will be added to BOTH search and browse result lists.
+ showThumbnails: true
+ # The number of entries in a paginated browse results list.
+ # Rounded to the nearest size in the list of selectable sizes on the
+ # settings menu.
+ pageSize: 20
-# Item Page Config
+communityList:
+ # No. of communities to list per expansion (show more)
+ pageSize: 20
+
+homePage:
+ recentSubmissions:
+ # The number of item showing in recent submission components
+ pageSize: 5
+ # Sort record of recent submission
+ sortField: 'dc.date.accessioned'
+ topLevelCommunityList:
+ # No. of communities to list per page on the home page
+ # This will always round to the nearest number from the list of page sizes. e.g. if you set it to 7 it'll use 10
+ pageSize: 5
+
+# Item Config
item:
edit:
undoTimeout: 10000 # 10 seconds
+ # Show the item access status label in items lists
+ showAccessStatuses: false
+ bitstream:
+ # Number of entries in the bitstream list in the item view page.
+ # Rounded to the nearest size in the list of selectable sizes on the
+ # settings menu. See pageSizeOptions in 'pagination-component-options.model.ts'.
+ pageSize: 5
# Collection Page Config
collection:
@@ -228,9 +287,26 @@ themes:
rel: manifest
href: assets/dspace/images/favicons/manifest.webmanifest
+# The default bundles that should always be displayed as suggestions when you upload a new bundle
+bundle:
+ standardBundles: [ ORIGINAL, THUMBNAIL, LICENSE ]
+
# Whether to enable media viewer for image and/or video Bitstreams (i.e. Bitstreams whose MIME type starts with 'image' or 'video').
# For images, this enables a gallery viewer where you can zoom or page through images.
# For videos, this enables embedded video streaming
mediaViewer:
image: false
video: false
+
+# Whether the end user agreement is required before users use the repository.
+# If enabled, the user will be required to accept the agreement before they can use the repository.
+# And whether the privacy statement should exist or not.
+info:
+ enableEndUserAgreement: true
+ enablePrivacyStatement: true
+
+# Whether to enable Markdown (https://commonmark.org/) and MathJax (https://www.mathjax.org/)
+# display in supported metadata fields. By default, only dc.description.abstract is supported.
+markdown:
+ enabled: false
+ mathjax: false
diff --git a/cypress.json b/cypress.json
index e06de8e4c5..3adf7839c2 100644
--- a/cypress.json
+++ b/cypress.json
@@ -5,6 +5,21 @@
"screenshotsFolder": "cypress/screenshots",
"pluginsFile": "cypress/plugins/index.ts",
"fixturesFolder": "cypress/fixtures",
- "baseUrl": "http://localhost:4000",
- "retries": 2
-}
\ No newline at end of file
+ "baseUrl": "http://127.0.0.1:4000",
+ "retries": {
+ "runMode": 2,
+ "openMode": 0
+ },
+ "env": {
+ "DSPACE_TEST_ADMIN_USER": "dspacedemo+admin@gmail.com",
+ "DSPACE_TEST_ADMIN_PASSWORD": "dspace",
+ "DSPACE_TEST_COMMUNITY": "0958c910-2037-42a9-81c7-dca80e3892b4",
+ "DSPACE_TEST_COLLECTION": "282164f5-d325-4740-8dd1-fa4d6d3e7200",
+ "DSPACE_TEST_ENTITY_PUBLICATION": "e98b0f27-5c19-49a0-960d-eb6ad5287067",
+ "DSPACE_TEST_SEARCH_TERM": "test",
+ "DSPACE_TEST_SUBMIT_COLLECTION_NAME": "Sample Collection",
+ "DSPACE_TEST_SUBMIT_COLLECTION_UUID": "9d8334e9-25d3-4a67-9cea-3dffdef80144",
+ "DSPACE_TEST_SUBMIT_USER": "dspacedemo+submit@gmail.com",
+ "DSPACE_TEST_SUBMIT_USER_PASSWORD": "dspace"
+ }
+}
diff --git a/cypress/.gitignore b/cypress/.gitignore
index 99bd2a6312..645beff45f 100644
--- a/cypress/.gitignore
+++ b/cypress/.gitignore
@@ -1,2 +1,3 @@
screenshots/
videos/
+downloads/
diff --git a/cypress/integration/homepage.spec.ts b/cypress/integration/homepage.spec.ts
index ddde260bc7..8fdf61dbf7 100644
--- a/cypress/integration/homepage.spec.ts
+++ b/cypress/integration/homepage.spec.ts
@@ -16,8 +16,8 @@ describe('Homepage', () => {
it('should have a working search box', () => {
const queryString = 'test';
- cy.get('ds-search-form input[name="query"]').type(queryString);
- cy.get('ds-search-form button.search-button').click();
+ cy.get('[data-test="search-box"]').type(queryString);
+ cy.get('[data-test="search-button"]').click();
cy.url().should('include', '/search');
cy.url().should('include', 'query=' + encodeURI(queryString));
});
diff --git a/cypress/integration/login-modal.spec.ts b/cypress/integration/login-modal.spec.ts
new file mode 100644
index 0000000000..fece28b425
--- /dev/null
+++ b/cypress/integration/login-modal.spec.ts
@@ -0,0 +1,126 @@
+import { TEST_ADMIN_PASSWORD, TEST_ADMIN_USER, TEST_ENTITY_PUBLICATION } from 'cypress/support';
+
+const page = {
+ openLoginMenu() {
+ // Click the "Log In" dropdown menu in header
+ cy.get('ds-themed-navbar [data-test="login-menu"]').click();
+ },
+ openUserMenu() {
+ // Once logged in, click the User menu in header
+ cy.get('ds-themed-navbar [data-test="user-menu"]').click();
+ },
+ submitLoginAndPasswordByPressingButton(email, password) {
+ // Enter email
+ cy.get('ds-themed-navbar [data-test="email"]').type(email);
+ // Enter password
+ cy.get('ds-themed-navbar [data-test="password"]').type(password);
+ // Click login button
+ cy.get('ds-themed-navbar [data-test="login-button"]').click();
+ },
+ submitLoginAndPasswordByPressingEnter(email, password) {
+ // In opened Login modal, fill out email & password, then click Enter
+ cy.get('ds-themed-navbar [data-test="email"]').type(email);
+ cy.get('ds-themed-navbar [data-test="password"]').type(password);
+ cy.get('ds-themed-navbar [data-test="password"]').type('{enter}');
+ },
+ submitLogoutByPressingButton() {
+ // This is the POST command that will actually log us out
+ cy.intercept('POST', '/server/api/authn/logout').as('logout');
+ // Click logout button
+ cy.get('ds-themed-navbar [data-test="logout-button"]').click();
+ // Wait until above POST command responds before continuing
+ // (This ensures next action waits until logout completes)
+ cy.wait('@logout');
+ }
+};
+
+describe('Login Modal', () => {
+ it('should login when clicking button & stay on same page', () => {
+ const ENTITYPAGE = '/entities/publication/' + TEST_ENTITY_PUBLICATION;
+ cy.visit(ENTITYPAGE);
+
+ // Login menu should exist
+ cy.get('ds-log-in').should('exist');
+
+ // Login, and the tag should no longer exist
+ page.openLoginMenu();
+ cy.get('.form-login').should('be.visible');
+
+ page.submitLoginAndPasswordByPressingButton(TEST_ADMIN_USER, TEST_ADMIN_PASSWORD);
+ cy.get('ds-log-in').should('not.exist');
+
+ // Verify we are still on the same page
+ cy.url().should('include', ENTITYPAGE);
+
+ // Open user menu, verify user menu & logout button now available
+ page.openUserMenu();
+ cy.get('ds-user-menu').should('be.visible');
+ cy.get('ds-log-out').should('be.visible');
+ });
+
+ it('should login when clicking enter key & stay on same page', () => {
+ cy.visit('/home');
+
+ // Open login menu in header & verify tag is visible
+ page.openLoginMenu();
+ cy.get('.form-login').should('be.visible');
+
+ // Login, and the tag should no longer exist
+ page.submitLoginAndPasswordByPressingEnter(TEST_ADMIN_USER, TEST_ADMIN_PASSWORD);
+ cy.get('.form-login').should('not.exist');
+
+ // Verify we are still on homepage
+ cy.url().should('include', '/home');
+
+ // Open user menu, verify user menu & logout button now available
+ page.openUserMenu();
+ cy.get('ds-user-menu').should('be.visible');
+ cy.get('ds-log-out').should('be.visible');
+ });
+
+ it('should support logout', () => {
+ // First authenticate & access homepage
+ cy.login(TEST_ADMIN_USER, TEST_ADMIN_PASSWORD);
+ cy.visit('/');
+
+ // Verify ds-log-in tag doesn't exist, but ds-log-out tag does exist
+ cy.get('ds-log-in').should('not.exist');
+ cy.get('ds-log-out').should('exist');
+
+ // Click logout button
+ page.openUserMenu();
+ page.submitLogoutByPressingButton();
+
+ // Verify ds-log-in tag now exists
+ cy.get('ds-log-in').should('exist');
+ cy.get('ds-log-out').should('not.exist');
+ });
+
+ it('should allow new user registration', () => {
+ cy.visit('/');
+
+ page.openLoginMenu();
+
+ // Registration link should be visible
+ cy.get('ds-themed-navbar [data-test="register"]').should('be.visible');
+
+ // Click registration link & you should go to registration page
+ cy.get('ds-themed-navbar [data-test="register"]').click();
+ cy.location('pathname').should('eq', '/register');
+ cy.get('ds-register-email').should('exist');
+ });
+
+ it('should allow forgot password', () => {
+ cy.visit('/');
+
+ page.openLoginMenu();
+
+ // Forgot password link should be visible
+ cy.get('ds-themed-navbar [data-test="forgot"]').should('be.visible');
+
+ // Click link & you should go to Forgot Password page
+ cy.get('ds-themed-navbar [data-test="forgot"]').click();
+ cy.location('pathname').should('eq', '/forgot');
+ cy.get('ds-forgot-email').should('exist');
+ });
+});
diff --git a/cypress/integration/my-dspace.spec.ts b/cypress/integration/my-dspace.spec.ts
new file mode 100644
index 0000000000..48f44eecb9
--- /dev/null
+++ b/cypress/integration/my-dspace.spec.ts
@@ -0,0 +1,155 @@
+import { Options } from 'cypress-axe';
+import { TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD, TEST_SUBMIT_COLLECTION_NAME } from 'cypress/support';
+import { testA11y } from 'cypress/support/utils';
+
+describe('My DSpace page', () => {
+ it('should display recent submissions and pass accessibility tests', () => {
+ cy.visit('/mydspace');
+
+ // This page is restricted, so we will be shown the login form. Fill it out & submit.
+ cy.loginViaForm(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
+
+ cy.get('ds-my-dspace-page').should('exist');
+
+ // At least one recent submission should be displayed
+ cy.get('[data-test="list-object"]').should('be.visible');
+
+ // Click each filter toggle to open *every* filter
+ // (As we want to scan filter section for accessibility issues as well)
+ cy.get('.filter-toggle').click({ multiple: true });
+
+ // Analyze for accessibility issues
+ testA11y(
+ {
+ include: ['ds-my-dspace-page'],
+ exclude: [
+ ['nouislider'] // Date filter slider is missing ARIA labels. Will be fixed by #1175
+ ],
+ },
+ {
+ rules: {
+ // Search filters fail these two "moderate" impact rules
+ 'heading-order': { enabled: false },
+ 'landmark-unique': { enabled: false }
+ }
+ } as Options
+ );
+ });
+
+ it('should have a working detailed view that passes accessibility tests', () => {
+ cy.visit('/mydspace');
+
+ // This page is restricted, so we will be shown the login form. Fill it out & submit.
+ cy.loginViaForm(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
+
+ cy.get('ds-my-dspace-page').should('exist');
+
+ // Click button in sidebar to display detailed view
+ cy.get('ds-search-sidebar [data-test="detail-view"]').click();
+
+ cy.get('ds-object-detail').should('exist');
+
+ // Analyze for accessibility issues
+ testA11y('ds-my-dspace-page',
+ {
+ rules: {
+ // Search filters fail these two "moderate" impact rules
+ 'heading-order': { enabled: false },
+ 'landmark-unique': { enabled: false }
+ }
+ } as Options
+ );
+ });
+
+ // NOTE: Deleting existing submissions is exercised by submission.spec.ts
+ it('should let you start a new submission & edit in-progress submissions', () => {
+ cy.visit('/mydspace');
+
+ // This page is restricted, so we will be shown the login form. Fill it out & submit.
+ cy.loginViaForm(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
+
+ // Open the New Submission dropdown
+ cy.get('button[data-test="submission-dropdown"]').click();
+ // Click on the "Item" type in that dropdown
+ cy.get('#entityControlsDropdownMenu button[title="none"]').click();
+
+ // This should display the (popup window)
+ cy.get('ds-create-item-parent-selector').should('be.visible');
+
+ // Type in a known Collection name in the search box
+ cy.get('ds-authorized-collection-selector input[type="search"]').type(TEST_SUBMIT_COLLECTION_NAME);
+
+ // Click on the button matching that known Collection name
+ cy.get('ds-authorized-collection-selector button[title="' + TEST_SUBMIT_COLLECTION_NAME + '"]').click();
+
+ // New URL should include /workspaceitems, as we've started a new submission
+ cy.url().should('include', '/workspaceitems');
+
+ // The Submission edit form tag should be visible
+ cy.get('ds-submission-edit').should('be.visible');
+
+ // A Collection menu button should exist & its value should be the selected collection
+ cy.get('#collectionControlsMenuButton span').should('have.text', TEST_SUBMIT_COLLECTION_NAME);
+
+ // Now that we've created a submission, we'll test that we can go back and Edit it.
+ // Get our Submission URL, to parse out the ID of this new submission
+ cy.location().then(fullUrl => {
+ // This will be the full path (/workspaceitems/[id]/edit)
+ const path = fullUrl.pathname;
+ // Split on the slashes
+ const subpaths = path.split('/');
+ // Part 2 will be the [id] of the submission
+ const id = subpaths[2];
+
+ // Click the "Save for Later" button to save this submission
+ cy.get('ds-submission-form-footer [data-test="save-for-later"]').click();
+
+ // "Save for Later" should send us to MyDSpace
+ cy.url().should('include', '/mydspace');
+
+ // Close any open notifications, to make sure they don't get in the way of next steps
+ cy.get('[data-dismiss="alert"]').click({multiple: true});
+
+ // This is the GET command that will actually run the search
+ cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results');
+ // On MyDSpace, find the submission we just created via its ID
+ cy.get('[data-test="search-box"]').type(id);
+ cy.get('[data-test="search-button"]').click();
+
+ // Wait for search results to come back from the above GET command
+ cy.wait('@search-results');
+
+ // Click the Edit button for this in-progress submission
+ cy.get('#edit_' + id).click();
+
+ // Should send us back to the submission form
+ cy.url().should('include', '/workspaceitems/' + id + '/edit');
+
+ // Discard our new submission by clicking Discard in Submission form & confirming
+ cy.get('ds-submission-form-footer [data-test="discard"]').click();
+ cy.get('button#discard_submit').click();
+
+ // Discarding should send us back to MyDSpace
+ cy.url().should('include', '/mydspace');
+ });
+ });
+
+ it('should let you import from external sources', () => {
+ cy.visit('/mydspace');
+
+ // This page is restricted, so we will be shown the login form. Fill it out & submit.
+ cy.loginViaForm(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
+
+ // Open the New Import dropdown
+ cy.get('button[data-test="import-dropdown"]').click();
+ // Click on the "Item" type in that dropdown
+ cy.get('#importControlsDropdownMenu button[title="none"]').click();
+
+ // New URL should include /import-external, as we've moved to the import page
+ cy.url().should('include', '/import-external');
+
+ // The external import searchbox should be visible
+ cy.get('ds-submission-import-external-searchbar').should('be.visible');
+ });
+
+});
diff --git a/cypress/integration/search-navbar.spec.ts b/cypress/integration/search-navbar.spec.ts
index 19a3d56ed4..babd9b9dfd 100644
--- a/cypress/integration/search-navbar.spec.ts
+++ b/cypress/integration/search-navbar.spec.ts
@@ -1,49 +1,66 @@
+import { TEST_SEARCH_TERM } from 'cypress/support';
+
const page = {
fillOutQueryInNavBar(query) {
// Click the magnifying glass
- cy.get('.navbar-container #search-navbar-container form a').click();
+ cy.get('ds-themed-navbar [data-test="header-search-icon"]').click();
// Fill out a query in input that appears
- cy.get('.navbar-container #search-navbar-container form input[name = "query"]').type(query);
+ cy.get('ds-themed-navbar [data-test="header-search-box"]').type(query);
},
submitQueryByPressingEnter() {
- cy.get('.navbar-container #search-navbar-container form input[name = "query"]').type('{enter}');
+ cy.get('ds-themed-navbar [data-test="header-search-box"]').type('{enter}');
},
submitQueryByPressingIcon() {
- cy.get('.navbar-container #search-navbar-container form .submit-icon').click();
+ cy.get('ds-themed-navbar [data-test="header-search-icon"]').click();
}
};
describe('Search from Navigation Bar', () => {
// NOTE: these tests currently assume this query will return results!
- const query = 'test';
+ const query = TEST_SEARCH_TERM;
it('should go to search page with correct query if submitted (from home)', () => {
cy.visit('/');
+ // This is the GET command that will actually run the search
+ cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results');
+ // Run the search
page.fillOutQueryInNavBar(query);
page.submitQueryByPressingEnter();
// New URL should include query param
cy.url().should('include', 'query=' + query);
+ // Wait for search results to come back from the above GET command
+ cy.wait('@search-results');
// At least one search result should be displayed
- cy.get('ds-item-search-result-list-element').should('be.visible');
+ cy.get('[data-test="list-object"]').should('be.visible');
});
it('should go to search page with correct query if submitted (from search)', () => {
cy.visit('/search');
+ // This is the GET command that will actually run the search
+ cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results');
+ // Run the search
page.fillOutQueryInNavBar(query);
page.submitQueryByPressingEnter();
// New URL should include query param
cy.url().should('include', 'query=' + query);
+ // Wait for search results to come back from the above GET command
+ cy.wait('@search-results');
// At least one search result should be displayed
- cy.get('ds-item-search-result-list-element').should('be.visible');
+ cy.get('[data-test="list-object"]').should('be.visible');
});
it('should allow user to also submit query by clicking icon', () => {
cy.visit('/');
+ // This is the GET command that will actually run the search
+ cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results');
+ // Run the search
page.fillOutQueryInNavBar(query);
page.submitQueryByPressingIcon();
// New URL should include query param
cy.url().should('include', 'query=' + query);
+ // Wait for search results to come back from the above GET command
+ cy.wait('@search-results');
// At least one search result should be displayed
- cy.get('ds-item-search-result-list-element').should('be.visible');
+ cy.get('[data-test="list-object"]').should('be.visible');
});
});
diff --git a/cypress/integration/search-page.spec.ts b/cypress/integration/search-page.spec.ts
index 859c765d2e..623c370c56 100644
--- a/cypress/integration/search-page.spec.ts
+++ b/cypress/integration/search-page.spec.ts
@@ -1,34 +1,30 @@
import { Options } from 'cypress-axe';
+import { TEST_SEARCH_TERM } from 'cypress/support';
import { testA11y } from 'cypress/support/utils';
describe('Search Page', () => {
- // unique ID of the search form (for selecting specific elements below)
- const SEARCHFORM_ID = '#search-form';
-
- it('should contain query value when navigating to page with query parameter', () => {
- const queryString = 'test query';
- cy.visit('/search?query=' + queryString);
- cy.get(SEARCHFORM_ID + ' input[name="query"]').should('have.value', queryString);
- });
-
it('should redirect to the correct url when query was set and submit button was triggered', () => {
const queryString = 'Another interesting query string';
cy.visit('/search');
// Type query in searchbox & click search button
- cy.get(SEARCHFORM_ID + ' input[name="query"]').type(queryString);
- cy.get(SEARCHFORM_ID + ' button.search-button').click();
+ cy.get('[data-test="search-box"]').type(queryString);
+ cy.get('[data-test="search-button"]').click();
cy.url().should('include', 'query=' + encodeURI(queryString));
});
- it('should pass accessibility tests', () => {
- cy.visit('/search');
+ it('should load results and pass accessibility tests', () => {
+ cy.visit('/search?query=' + TEST_SEARCH_TERM);
+ cy.get('[data-test="search-box"]').should('have.value', TEST_SEARCH_TERM);
// tag must be loaded
cy.get('ds-search-page').should('exist');
+ // At least one search result should be displayed
+ cy.get('[data-test="list-object"]').should('be.visible');
+
// Click each filter toggle to open *every* filter
// (As we want to scan filter section for accessibility issues as well)
- cy.get('.filter-toggle').click({ multiple: true });
+ cy.get('[data-test="filter-toggle"]').click({ multiple: true });
// Analyze for accessibility issues
testA11y(
@@ -48,16 +44,18 @@ describe('Search Page', () => {
);
});
- it('should pass accessibility tests in Grid view', () => {
- cy.visit('/search');
+ it('should have a working grid view that passes accessibility tests', () => {
+ cy.visit('/search?query=' + TEST_SEARCH_TERM);
- // Click to display grid view
- // TODO: These buttons should likely have an easier way to uniquely select
- cy.get('#search-sidebar-content > ds-view-mode-switch > .btn-group > [href="/search?view=grid"] > .fas').click();
+ // Click button in sidebar to display grid view
+ cy.get('ds-search-sidebar [data-test="grid-view"]').click();
// tag must be loaded
cy.get('ds-search-page').should('exist');
+ // At least one grid object (card) should be displayed
+ cy.get('[data-test="grid-object"]').should('be.visible');
+
// Analyze for accessibility issues
testA11y('ds-search-page',
{
diff --git a/cypress/integration/submission.spec.ts b/cypress/integration/submission.spec.ts
new file mode 100644
index 0000000000..9eef596b02
--- /dev/null
+++ b/cypress/integration/submission.spec.ts
@@ -0,0 +1,138 @@
+import { Options } from 'cypress-axe';
+import { TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD, TEST_SUBMIT_COLLECTION_NAME, TEST_SUBMIT_COLLECTION_UUID } from 'cypress/support';
+import { testA11y } from 'cypress/support/utils';
+
+describe('New Submission page', () => {
+ // NOTE: We already test that new submissions can be started from MyDSpace in my-dspace.spec.ts
+
+ it('should create a new submission when using /submit path & pass accessibility', () => {
+ // Test that calling /submit with collection & entityType will create a new submission
+ cy.visit('/submit?collection=' + TEST_SUBMIT_COLLECTION_UUID + '&entityType=none');
+
+ // This page is restricted, so we will be shown the login form. Fill it out & submit.
+ cy.loginViaForm(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
+
+ // Should redirect to /workspaceitems, as we've started a new submission
+ cy.url().should('include', '/workspaceitems');
+
+ // The Submission edit form tag should be visible
+ cy.get('ds-submission-edit').should('be.visible');
+
+ // A Collection menu button should exist & it's value should be the selected collection
+ cy.get('#collectionControlsMenuButton span').should('have.text', TEST_SUBMIT_COLLECTION_NAME);
+
+ // 4 sections should be visible by default
+ cy.get('div#section_traditionalpageone').should('be.visible');
+ cy.get('div#section_traditionalpagetwo').should('be.visible');
+ cy.get('div#section_upload').should('be.visible');
+ cy.get('div#section_license').should('be.visible');
+
+ // Discard button should work
+ // Clicking it will display a confirmation, which we will confirm with another click
+ cy.get('button#discard').click();
+ cy.get('button#discard_submit').click();
+ });
+
+ it('should block submission & show errors if required fields are missing', () => {
+ // Create a new submission
+ cy.visit('/submit?collection=' + TEST_SUBMIT_COLLECTION_UUID + '&entityType=none');
+
+ // This page is restricted, so we will be shown the login form. Fill it out & submit.
+ cy.loginViaForm(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
+
+ // Attempt an immediate deposit without filling out any fields
+ cy.get('button#deposit').click();
+
+ // A warning alert should display.
+ cy.get('ds-notification div.alert-success').should('not.exist');
+ cy.get('ds-notification div.alert-warning').should('be.visible');
+
+ // First section should have an exclamation error in the header
+ // (as it has required fields)
+ cy.get('div#traditionalpageone-header i.fa-exclamation-circle').should('be.visible');
+
+ // Title field should have class "is-invalid" applied, as it's required
+ cy.get('input#dc_title').should('have.class', 'is-invalid');
+
+ // Date Year field should also have "is-valid" class
+ cy.get('input#dc_date_issued_year').should('have.class', 'is-invalid');
+
+ // FINALLY, cleanup after ourselves. This also exercises the MyDSpace delete button.
+ // Get our Submission URL, to parse out the ID of this submission
+ cy.location().then(fullUrl => {
+ // This will be the full path (/workspaceitems/[id]/edit)
+ const path = fullUrl.pathname;
+ // Split on the slashes
+ const subpaths = path.split('/');
+ // Part 2 will be the [id] of the submission
+ const id = subpaths[2];
+
+ // Even though form is incomplete, the "Save for Later" button should still work
+ cy.get('button#saveForLater').click();
+
+ // "Save for Later" should send us to MyDSpace
+ cy.url().should('include', '/mydspace');
+
+ // A success alert should be visible
+ cy.get('ds-notification div.alert-success').should('be.visible');
+ // Now, dismiss any open alert boxes (may be multiple, as tests run quickly)
+ cy.get('[data-dismiss="alert"]').click({multiple: true});
+
+ // This is the GET command that will actually run the search
+ cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results');
+ // On MyDSpace, find the submission we just saved via its ID
+ cy.get('[data-test="search-box"]').type(id);
+ cy.get('[data-test="search-button"]').click();
+
+ // Wait for search results to come back from the above GET command
+ cy.wait('@search-results');
+
+ // Delete our created submission & confirm deletion
+ cy.get('button#delete_' + id).click();
+ cy.get('button#delete_confirm').click();
+ });
+ });
+
+ it('should allow for deposit if all required fields completed & file uploaded', () => {
+ // Create a new submission
+ cy.visit('/submit?collection=' + TEST_SUBMIT_COLLECTION_UUID + '&entityType=none');
+
+ // This page is restricted, so we will be shown the login form. Fill it out & submit.
+ cy.loginViaForm(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
+
+ // Fill out all required fields (Title, Date)
+ cy.get('input#dc_title').type('DSpace logo uploaded via e2e tests');
+ cy.get('input#dc_date_issued_year').type('2022');
+
+ // Confirm the required license by checking checkbox
+ // (NOTE: requires "force:true" cause Cypress claims this checkbox is covered by its own )
+ cy.get('input#granted').check( {force: true} );
+
+ // Before using Cypress drag & drop, we have to manually trigger the "dragover" event.
+ // This ensures our UI displays the dropzone that covers the entire submission page.
+ // (For some reason Cypress drag & drop doesn't trigger this even itself & upload won't work without this trigger)
+ cy.get('ds-uploader').trigger('dragover');
+
+ // This is the POST command that will upload the file
+ cy.intercept('POST', '/server/api/submission/workspaceitems/*').as('upload');
+
+ // Upload our DSpace logo via drag & drop onto submission form
+ // cy.get('div#section_upload')
+ cy.get('div.ds-document-drop-zone').selectFile('src/assets/images/dspace-logo.png', {
+ action: 'drag-drop'
+ });
+
+ // Wait for upload to complete before proceeding
+ cy.wait('@upload');
+ // Close the upload success notice
+ cy.get('[data-dismiss="alert"]').click({multiple: true});
+
+ // Wait for deposit button to not be disabled & click it.
+ cy.get('button#deposit').should('not.be.disabled').click();
+
+ // No warnings should exist. Instead, just successful deposit alert is displayed
+ cy.get('ds-notification div.alert-warning').should('not.exist');
+ cy.get('ds-notification div.alert-success').should('be.visible');
+ });
+
+});
diff --git a/cypress/plugins/index.ts b/cypress/plugins/index.ts
index c6eb874232..ead38afb92 100644
--- a/cypress/plugins/index.ts
+++ b/cypress/plugins/index.ts
@@ -1,15 +1,34 @@
+const fs = require('fs');
+
// Plugins enable you to tap into, modify, or extend the internal behavior of Cypress
// For more info, visit https://on.cypress.io/plugins-api
module.exports = (on, config) => {
- // Define "log" and "table" tasks, used for logging accessibility errors during CI
- // Borrowed from https://github.com/component-driven/cypress-axe#in-cypress-plugins-file
on('task', {
+ // Define "log" and "table" tasks, used for logging accessibility errors during CI
+ // Borrowed from https://github.com/component-driven/cypress-axe#in-cypress-plugins-file
log(message: string) {
console.log(message);
return null;
},
table(message: string) {
console.table(message);
+ return null;
+ },
+ // Cypress doesn't have access to the running application in Node.js.
+ // So, it's not possible to inject or load the AppConfig or environment of the Angular UI.
+ // Instead, we'll read our running application's config.json, which contains the configs &
+ // is regenerated at runtime each time the Angular UI application starts up.
+ readUIConfig() {
+ // Check if we have a config.json in the src/assets. If so, use that.
+ // This is where it's written when running "ng e2e" or "yarn serve"
+ if (fs.existsSync('./src/assets/config.json')) {
+ return fs.readFileSync('./src/assets/config.json', 'utf8');
+ // Otherwise, check the dist/browser/assets
+ // This is where it's written when running "serve:ssr", which is what CI uses to start the frontend
+ } else if (fs.existsSync('./dist/browser/assets/config.json')) {
+ return fs.readFileSync('./dist/browser/assets/config.json', 'utf8');
+ }
+
return null;
}
});
diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts
index af1f44a0fc..04c217aa0f 100644
--- a/cypress/support/commands.ts
+++ b/cypress/support/commands.ts
@@ -1,43 +1,110 @@
// ***********************************************
-// This example namespace declaration will help
-// with Intellisense and code completion in your
-// IDE or Text Editor.
+// This File is for Custom Cypress commands.
+// See docs at https://docs.cypress.io/api/cypress-api/custom-commands
// ***********************************************
-// declare namespace Cypress {
-// interface Chainable {
-// customCommand(param: any): typeof customCommand;
-// }
-// }
-//
-// function customCommand(param: any): void {
-// console.warn(param);
-// }
-//
-// NOTE: You can use it like so:
-// Cypress.Commands.add('customCommand', customCommand);
-//
-// ***********************************************
-// This example commands.js shows you how to
-// create various custom commands and overwrite
-// existing commands.
-//
-// For more comprehensive examples of custom
-// commands please read more here:
-// https://on.cypress.io/custom-commands
-// ***********************************************
-//
-//
-// -- This is a parent command --
-// Cypress.Commands.add("login", (email, password) => { ... })
-//
-//
-// -- This is a child command --
-// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
-//
-//
-// -- This is a dual command --
-// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
-//
-//
-// -- This will overwrite an existing command --
-// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
+
+import { AuthTokenInfo, TOKENITEM } from 'src/app/core/auth/models/auth-token-info.model';
+import { FALLBACK_TEST_REST_BASE_URL } from '.';
+
+// Declare Cypress namespace to help with Intellisense & code completion in IDEs
+// ALL custom commands MUST be listed here for code completion to work
+// tslint:disable-next-line:no-namespace
+declare global {
+ namespace Cypress {
+ interface Chainable {
+ /**
+ * Login to backend before accessing the next page. Ensures that the next
+ * call to "cy.visit()" will be authenticated as this user.
+ * @param email email to login as
+ * @param password password to login as
+ */
+ login(email: string, password: string): typeof login;
+
+ /**
+ * Login via form before accessing the next page. Useful to fill out login
+ * form when a cy.visit() call is to an a page which requires authentication.
+ * @param email email to login as
+ * @param password password to login as
+ */
+ loginViaForm(email: string, password: string): typeof loginViaForm;
+ }
+ }
+}
+
+/**
+ * Login user via REST API directly, and pass authentication token to UI via
+ * the UI's dsAuthInfo cookie.
+ * WARNING: WHILE THIS METHOD WORKS, OCCASIONALLY RANDOM AUTHENTICATION ERRORS OCCUR.
+ * At this time "loginViaForm()" seems more consistent/stable.
+ * @param email email to login as
+ * @param password password to login as
+ */
+function login(email: string, password: string): void {
+ // Cypress doesn't have access to the running application in Node.js.
+ // So, it's not possible to inject or load the AppConfig or environment of the Angular UI.
+ // Instead, we'll read our running application's config.json, which contains the configs &
+ // is regenerated at runtime each time the Angular UI application starts up.
+ cy.task('readUIConfig').then((str: string) => {
+ // Parse config into a JSON object
+ const config = JSON.parse(str);
+
+ // Find the URL of our REST API. Have a fallback ready, just in case 'rest.baseUrl' cannot be found.
+ let baseRestUrl = FALLBACK_TEST_REST_BASE_URL;
+ if (!config.rest.baseUrl) {
+ console.warn("Could not load 'rest.baseUrl' from config.json. Falling back to " + FALLBACK_TEST_REST_BASE_URL);
+ } else {
+ console.log("Found 'rest.baseUrl' in config.json. Using this REST API for login: " + config.rest.baseUrl);
+ baseRestUrl = config.rest.baseUrl;
+ }
+
+ // To login via REST, first we have to do a GET to obtain a valid CSRF token
+ cy.request( baseRestUrl + '/api/authn/status' )
+ .then((response) => {
+ // We should receive a CSRF token returned in a response header
+ expect(response.headers).to.have.property('dspace-xsrf-token');
+ const csrfToken = response.headers['dspace-xsrf-token'];
+
+ // Now, send login POST request including that CSRF token
+ cy.request({
+ method: 'POST',
+ url: baseRestUrl + '/api/authn/login',
+ headers: { 'X-XSRF-TOKEN' : csrfToken},
+ form: true, // indicates the body should be form urlencoded
+ body: { user: email, password: password }
+ }).then((resp) => {
+ // We expect a successful login
+ expect(resp.status).to.eq(200);
+ // We expect to have a valid authorization header returned (with our auth token)
+ expect(resp.headers).to.have.property('authorization');
+
+ // Initialize our AuthTokenInfo object from the authorization header.
+ const authheader = resp.headers.authorization as string;
+ const authinfo: AuthTokenInfo = new AuthTokenInfo(authheader);
+
+ // Save our AuthTokenInfo object to our dsAuthInfo UI cookie
+ // This ensures the UI will recognize we are logged in on next "visit()"
+ cy.setCookie(TOKENITEM, JSON.stringify(authinfo));
+ });
+ });
+
+ });
+}
+// Add as a Cypress command (i.e. assign to 'cy.login')
+Cypress.Commands.add('login', login);
+
+
+/**
+ * Login user via displayed login form
+ * @param email email to login as
+ * @param password password to login as
+ */
+ function loginViaForm(email: string, password: string): void {
+ // Enter email
+ cy.get('ds-log-in [data-test="email"]').type(email);
+ // Enter password
+ cy.get('ds-log-in [data-test="password"]').type(password);
+ // Click login button
+ cy.get('ds-log-in [data-test="login-button"]').click();
+}
+// Add as a Cypress command (i.e. assign to 'cy.loginViaForm')
+Cypress.Commands.add('loginViaForm', loginViaForm);
\ No newline at end of file
diff --git a/cypress/support/index.ts b/cypress/support/index.ts
index e8b10b9cfb..70da23f044 100644
--- a/cypress/support/index.ts
+++ b/cypress/support/index.ts
@@ -13,14 +13,51 @@
// https://on.cypress.io/configuration
// ***********************************************************
-// When a command from ./commands is ready to use, import with `import './commands'` syntax
-// import './commands';
+// Import all custom Commands (from commands.ts) for all tests
+import './commands';
// Import Cypress Axe tools for all tests
// https://github.com/component-driven/cypress-axe
import 'cypress-axe';
+// Runs once before the first test in each "block"
+beforeEach(() => {
+ // Pre-agree to all Klaro cookies by setting the klaro-anonymous cookie
+ // This just ensures it doesn't get in the way of matching other objects in the page.
+ cy.setCookie('klaro-anonymous', '{%22authentication%22:true%2C%22preferences%22:true%2C%22acknowledgement%22:true%2C%22google-analytics%22:true%2C%22google-recaptcha%22:true}');
+});
+
+// For better stability between tests, we visit "about:blank" (i.e. blank page) after each test.
+// This ensures any remaining/outstanding XHR requests are killed, so they don't affect the next test.
+// Borrowed from: https://glebbahmutov.com/blog/visit-blank-page-between-tests/
+afterEach(() => {
+ cy.window().then((win) => {
+ win.location.href = 'about:blank';
+ });
+});
+
+
// Global constants used in tests
-export const TEST_COLLECTION = '282164f5-d325-4740-8dd1-fa4d6d3e7200';
-export const TEST_COMMUNITY = '0958c910-2037-42a9-81c7-dca80e3892b4';
-export const TEST_ENTITY_PUBLICATION = 'e98b0f27-5c19-49a0-960d-eb6ad5287067';
+// May be overridden in our cypress.json config file using specified environment variables.
+// Default values listed here are all valid for the Demo Entities Data set available at
+// https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data
+// (This is the data set used in our CI environment)
+
+// NOTE: FALLBACK_TEST_REST_BASE_URL is only used if Cypress cannot read the REST API BaseURL
+// from the Angular UI's config.json. See 'getBaseRESTUrl()' in commands.ts
+export const FALLBACK_TEST_REST_BASE_URL = 'http://localhost:8080/server';
+
+// Admin account used for administrative tests
+export const TEST_ADMIN_USER = Cypress.env('DSPACE_TEST_ADMIN_USER') || 'dspacedemo+admin@gmail.com';
+export const TEST_ADMIN_PASSWORD = Cypress.env('DSPACE_TEST_ADMIN_PASSWORD') || 'dspace';
+// Community/collection/publication used for view/edit tests
+export const TEST_COLLECTION = Cypress.env('DSPACE_TEST_COLLECTION') || '282164f5-d325-4740-8dd1-fa4d6d3e7200';
+export const TEST_COMMUNITY = Cypress.env('DSPACE_TEST_COMMUNITY') || '0958c910-2037-42a9-81c7-dca80e3892b4';
+export const TEST_ENTITY_PUBLICATION = Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION') || 'e98b0f27-5c19-49a0-960d-eb6ad5287067';
+// Search term (should return results) used in search tests
+export const TEST_SEARCH_TERM = Cypress.env('DSPACE_TEST_SEARCH_TERM') || 'test';
+// Collection used for submission tests
+export const TEST_SUBMIT_COLLECTION_NAME = Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_NAME') || 'Sample Collection';
+export const TEST_SUBMIT_COLLECTION_UUID = Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_UUID') || '9d8334e9-25d3-4a67-9cea-3dffdef80144';
+export const TEST_SUBMIT_USER = Cypress.env('DSPACE_TEST_SUBMIT_USER') || 'dspacedemo+submit@gmail.com';
+export const TEST_SUBMIT_USER_PASSWORD = Cypress.env('DSPACE_TEST_SUBMIT_USER_PASSWORD') || 'dspace';
diff --git a/docker/README.md b/docker/README.md
index a2f4ef3362..1a9fee0a81 100644
--- a/docker/README.md
+++ b/docker/README.md
@@ -1,93 +1,95 @@
-# Docker Compose files
-
-***
-:warning: **NOT PRODUCTION READY** The below Docker Compose resources are not guaranteed "production ready" at this time. They have been built for development/testing only. Therefore, DSpace Docker images may not be fully secured or up-to-date. While you are welcome to base your own images on these DSpace images/resources, these should not be used "as is" in any production scenario.
-***
-
-## 'Dockerfile' in root directory
-This Dockerfile is used to build a *development* DSpace 7 Angular UI image, published as 'dspace/dspace-angular'
-
-```
-docker build -t dspace/dspace-angular:dspace-7_x .
-```
-
-This image is built *automatically* after each commit is made to the `main` branch.
-
-Admins to our DockerHub repo can manually publish with the following command.
-```
-docker push dspace/dspace-angular:dspace-7_x
-```
-
-## docker directory
-- docker-compose.yml
- - Starts DSpace Angular with Docker Compose from the current branch. This file assumes that a DSpace 7 REST instance will also be started in Docker.
-- docker-compose-rest.yml
- - Runs a published instance of the DSpace 7 REST API - persists data in Docker volumes
-- docker-compose-ci.yml
- - Runs a published instance of the DSpace 7 REST API for CI testing. The database is re-populated from a SQL dump on each startup.
-- cli.yml
- - Docker compose file that provides a DSpace CLI container to work with a running DSpace REST container.
-- cli.assetstore.yml
- - Docker compose file that will download and install data into a DSpace REST assetstore. This script points to a default dataset that will be utilized for CI testing.
-
-
-## To refresh / pull DSpace images from Dockerhub
-```
-docker-compose -f docker/docker-compose.yml pull
-```
-
-## To build DSpace images using code in your branch
-```
-docker-compose -f docker/docker-compose.yml build
-```
-
-## To start DSpace (REST and Angular) from your branch
-
-```
-docker-compose -p d7 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml up -d
-```
-
-## Run DSpace REST and DSpace Angular from local branches.
-_The system will be started in 2 steps. Each step shares the same docker network._
-
-From DSpace/DSpace (build as needed)
-```
-docker-compose -p d7 up -d
-```
-
-From DSpace/DSpace-angular
-```
-docker-compose -p d7 -f docker/docker-compose.yml up -d
-```
-
-## Ingest test data from AIPDIR
-
-Create an administrator
-```
-docker-compose -p d7 -f docker/cli.yml run --rm dspace-cli create-administrator -e test@test.edu -f admin -l user -p admin -c en
-```
-
-Load content from AIP files
-```
-docker-compose -p d7 -f docker/cli.yml -f ./docker/cli.ingest.yml run --rm dspace-cli
-```
-
-## Alternative Ingest - Use Entities dataset
-_Delete your docker volumes or use a unique project (-p) name_
-
-Start DSpace with Database Content from a database dump
-```
-docker-compose -p d7 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml -f docker/db.entities.yml up -d
-```
-
-Load assetstore content and trigger a re-index of the repository
-```
-docker-compose -p d7 -f docker/cli.yml -f docker/cli.assetstore.yml run --rm dspace-cli
-```
-
-## End to end testing of the rest api (runs in travis).
-_In this instance, only the REST api runs in Docker using the Entities dataset. Travis will perform CI testing of Angular using Node to drive the tests._
-
-```
-docker-compose -p d7ci -f docker/docker-compose-travis.yml up -d
-```
+# Docker Compose files
+
+***
+:warning: **THESE IMAGES ARE NOT PRODUCTION READY** The below Docker Compose images/resources were built for development/testing only. Therefore, they may not be fully secured or up-to-date, and should not be used in production.
+
+If you wish to run DSpace on Docker in production, we recommend building your own Docker images. You are welcome to borrow ideas/concepts from the below images in doing so. But, the below images should not be used "as is" in any production scenario.
+***
+
+## 'Dockerfile' in root directory
+This Dockerfile is used to build a *development* DSpace 7 Angular UI image, published as 'dspace/dspace-angular'
+
+```
+docker build -t dspace/dspace-angular:dspace-7_x .
+```
+
+This image is built *automatically* after each commit is made to the `main` branch.
+
+Admins to our DockerHub repo can manually publish with the following command.
+```
+docker push dspace/dspace-angular:dspace-7_x
+```
+
+## docker directory
+- docker-compose.yml
+ - Starts DSpace Angular with Docker Compose from the current branch. This file assumes that a DSpace 7 REST instance will also be started in Docker.
+- docker-compose-rest.yml
+ - Runs a published instance of the DSpace 7 REST API - persists data in Docker volumes
+- docker-compose-ci.yml
+ - Runs a published instance of the DSpace 7 REST API for CI testing. The database is re-populated from a SQL dump on each startup.
+- cli.yml
+ - Docker compose file that provides a DSpace CLI container to work with a running DSpace REST container.
+- cli.assetstore.yml
+ - Docker compose file that will download and install data into a DSpace REST assetstore. This script points to a default dataset that will be utilized for CI testing.
+
+
+## To refresh / pull DSpace images from Dockerhub
+```
+docker-compose -f docker/docker-compose.yml pull
+```
+
+## To build DSpace images using code in your branch
+```
+docker-compose -f docker/docker-compose.yml build
+```
+
+## To start DSpace (REST and Angular) from your branch
+
+```
+docker-compose -p d7 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml up -d
+```
+
+## Run DSpace REST and DSpace Angular from local branches.
+_The system will be started in 2 steps. Each step shares the same docker network._
+
+From DSpace/DSpace (build as needed)
+```
+docker-compose -p d7 up -d
+```
+
+From DSpace/DSpace-angular
+```
+docker-compose -p d7 -f docker/docker-compose.yml up -d
+```
+
+## Ingest test data from AIPDIR
+
+Create an administrator
+```
+docker-compose -p d7 -f docker/cli.yml run --rm dspace-cli create-administrator -e test@test.edu -f admin -l user -p admin -c en
+```
+
+Load content from AIP files
+```
+docker-compose -p d7 -f docker/cli.yml -f ./docker/cli.ingest.yml run --rm dspace-cli
+```
+
+## Alternative Ingest - Use Entities dataset
+_Delete your docker volumes or use a unique project (-p) name_
+
+Start DSpace with Database Content from a database dump
+```
+docker-compose -p d7 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml -f docker/db.entities.yml up -d
+```
+
+Load assetstore content and trigger a re-index of the repository
+```
+docker-compose -p d7 -f docker/cli.yml -f docker/cli.assetstore.yml run --rm dspace-cli
+```
+
+## End to end testing of the rest api (runs in travis).
+_In this instance, only the REST api runs in Docker using the Entities dataset. Travis will perform CI testing of Angular using Node to drive the tests._
+
+```
+docker-compose -p d7ci -f docker/docker-compose-travis.yml up -d
+```
diff --git a/docker/cli.assetstore.yml b/docker/cli.assetstore.yml
index c2846286d7..40e4974c7c 100644
--- a/docker/cli.assetstore.yml
+++ b/docker/cli.assetstore.yml
@@ -35,6 +35,6 @@ services:
tar xvfz /tmp/assetstore.tar.gz
fi
- /dspace/bin/dspace index-discovery
+ /dspace/bin/dspace index-discovery -b
/dspace/bin/dspace oai import
/dspace/bin/dspace oai clean-cache
diff --git a/docker/db.entities.yml b/docker/db.entities.yml
index 818d14877c..6473bf2e38 100644
--- a/docker/db.entities.yml
+++ b/docker/db.entities.yml
@@ -20,12 +20,12 @@ services:
environment:
# This LOADSQL should be kept in sync with the URL in DSpace/DSpace
# This SQL is available from https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data
- - LOADSQL=https://github.com/DSpace-Labs/AIP-Files/releases/download/demo-entities-data/dspace7-entities-2021-04-14.sql
+ - LOADSQL=https://github.com/DSpace-Labs/AIP-Files/releases/download/demo-entities-data/dspace7-entities-data.sql
dspace:
### OVERRIDE default 'entrypoint' in 'docker-compose-rest.yml' ####
# Ensure that the database is ready BEFORE starting tomcat
# 1. While a TCP connection to dspacedb port 5432 is not available, continue to sleep
- # 2. Then, run database migration to init database tables
+ # 2. Then, run database migration to init database tables (including any out-of-order ignored migrations, if any)
# 3. (Custom for Entities) enable Entity-specific collection submission mappings in item-submission.xml
# This 'sed' command inserts the sample configurations specific to the Entities data set, see:
# https://github.com/DSpace/DSpace/blob/main/dspace/config/item-submission.xml#L36-L49
@@ -35,7 +35,7 @@ services:
- '-c'
- |
while (! /dev/null 2>&1; do sleep 1; done;
- /dspace/bin/dspace database migrate
+ /dspace/bin/dspace database migrate ignored
sed -i '/name-map collection-handle="default".*/a \\n \
\
\
diff --git a/docker/docker-compose-ci.yml b/docker/docker-compose-ci.yml
index a895314a17..ef84c14f43 100644
--- a/docker/docker-compose-ci.yml
+++ b/docker/docker-compose-ci.yml
@@ -24,8 +24,8 @@ services:
# __D__ => "-" (e.g. google__D__metadata => google-metadata)
# dspace.dir, dspace.server.url and dspace.ui.url
dspace__P__dir: /dspace
- dspace__P__server__P__url: http://localhost:8080/server
- dspace__P__ui__P__url: http://localhost:4000
+ dspace__P__server__P__url: http://127.0.0.1:8080/server
+ dspace__P__ui__P__url: http://127.0.0.1:4000
# db.url: Ensure we are using the 'dspacedb' image for our database
db__P__url: 'jdbc:postgresql://dspacedb:5432/dspace'
# solr.server: Ensure we are using the 'dspacesolr' image for Solr
@@ -46,14 +46,14 @@ services:
- solr_configs:/dspace/solr
# Ensure that the database is ready BEFORE starting tomcat
# 1. While a TCP connection to dspacedb port 5432 is not available, continue to sleep
- # 2. Then, run database migration to init database tables
+ # 2. Then, run database migration to init database tables (including any out-of-order ignored migrations, if any)
# 3. Finally, start Tomcat
entrypoint:
- /bin/bash
- '-c'
- |
while (! /dev/null 2>&1; do sleep 1; done;
- /dspace/bin/dspace database migrate
+ /dspace/bin/dspace database migrate ignored
catalina.sh run
# DSpace database container
# NOTE: This is customized to use our loadsql image, so that we are using a database with existing test data
@@ -63,7 +63,7 @@ services:
# This LOADSQL should be kept in sync with the LOADSQL in
# https://github.com/DSpace/DSpace/blob/main/dspace/src/main/docker-compose/db.entities.yml
# This SQL is available from https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data
- LOADSQL: https://github.com/DSpace-Labs/AIP-Files/releases/download/demo-entities-data/dspace7-entities-2021-04-14.sql
+ LOADSQL: https://github.com/DSpace-Labs/AIP-Files/releases/download/demo-entities-data/dspace7-entities-data.sql
PGDATA: /pgdata
image: dspace/dspace-postgres-pgcrypto:loadsql
networks:
diff --git a/karma.conf.js b/karma.conf.js
index 24cd067fd1..8418312b1a 100644
--- a/karma.conf.js
+++ b/karma.conf.js
@@ -22,7 +22,7 @@ module.exports = function (config) {
reports: ['html', 'lcovonly', 'text-summary'],
fixWebpackSourcePaths: true
},
- reporters: ['mocha', 'kjhtml'],
+ reporters: ['mocha', 'kjhtml', 'coverage-istanbul'],
mochaReporter: {
ignoreSkipped: true,
output: 'autowatch'
diff --git a/package.json b/package.json
index 278afdf6c3..52b089be37 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "dspace-angular",
- "version": "0.0.0",
+ "version": "7.5.0-next",
"scripts": {
"ng": "ng",
"config:watch": "nodemon",
@@ -9,10 +9,11 @@
"start:dev": "nodemon --exec \"cross-env NODE_ENV=development yarn run serve\"",
"start:prod": "yarn run build:prod && cross-env NODE_ENV=production yarn run serve:ssr",
"start:mirador:prod": "yarn run build:mirador && yarn run start:prod",
+ "preserve": "yarn base-href",
"serve": "ts-node --project ./tsconfig.ts-node.json scripts/serve.ts",
"serve:ssr": "node dist/server/main",
"analyze": "webpack-bundle-analyzer dist/browser/stats.json",
- "build": "ng build",
+ "build": "ng build --configuration development",
"build:stats": "ng build --stats-json",
"build:prod": "yarn run build:ssr",
"build:ssr": "ng build --configuration production && ng run dspace-angular:server:production",
@@ -36,7 +37,9 @@
"merge-i18n": "ts-node --project ./tsconfig.ts-node.json scripts/merge-i18n-files.ts",
"cypress:open": "cypress open",
"cypress:run": "cypress run",
- "env:yaml": "ts-node --project ./tsconfig.ts-node.json scripts/env-to-yaml.ts"
+ "env:yaml": "ts-node --project ./tsconfig.ts-node.json scripts/env-to-yaml.ts",
+ "base-href": "ts-node --project ./tsconfig.ts-node.json scripts/base-href.ts",
+ "check-circ-deps": "npx madge --exclude '(bitstream|bundle|collection|config-submission-form|eperson|item|version)\\.model\\.ts$' --circular --extensions ts ./"
},
"browser": {
"fs": false,
@@ -47,146 +50,150 @@
"private": true,
"resolutions": {
"minimist": "^1.2.5",
- "webdriver-manager": "^12.1.8"
+ "webdriver-manager": "^12.1.8",
+ "ts-node": "10.2.1"
},
"dependencies": {
- "@angular/animations": "~11.2.14",
- "@angular/cdk": "^11.2.13",
- "@angular/common": "~11.2.14",
- "@angular/compiler": "~11.2.14",
- "@angular/core": "~11.2.14",
- "@angular/forms": "~11.2.14",
- "@angular/localize": "11.2.14",
- "@angular/platform-browser": "~11.2.14",
- "@angular/platform-browser-dynamic": "~11.2.14",
- "@angular/platform-server": "~11.2.14",
- "@angular/router": "~11.2.14",
- "@kolkov/ngx-gallery": "^1.2.3",
- "@ng-bootstrap/ng-bootstrap": "9.1.3",
- "@ng-dynamic-forms/core": "^13.0.0",
- "@ng-dynamic-forms/ui-ng-bootstrap": "^13.0.0",
- "@ngrx/effects": "^11.1.1",
- "@ngrx/router-store": "^11.1.1",
- "@ngrx/store": "^11.1.1",
- "@nguniversal/express-engine": "11.2.1",
+ "@angular/animations": "~13.3.12",
+ "@angular/cdk": "^13.2.6",
+ "@angular/common": "~13.3.12",
+ "@angular/compiler": "~13.3.12",
+ "@angular/core": "~13.3.12",
+ "@angular/forms": "~13.3.12",
+ "@angular/localize": "13.3.12",
+ "@angular/platform-browser": "~13.3.12",
+ "@angular/platform-browser-dynamic": "~13.3.12",
+ "@angular/platform-server": "~13.3.12",
+ "@angular/router": "~13.3.12",
+ "@babel/runtime": "7.17.2",
+ "@kolkov/ngx-gallery": "^2.0.1",
+ "@material-ui/core": "^4.11.0",
+ "@material-ui/icons": "^4.9.1",
+ "@ng-bootstrap/ng-bootstrap": "^11.0.0",
+ "@ng-dynamic-forms/core": "^15.0.0",
+ "@ng-dynamic-forms/ui-ng-bootstrap": "^15.0.0",
+ "@ngrx/effects": "^13.0.2",
+ "@ngrx/router-store": "^13.0.2",
+ "@ngrx/store": "^13.0.2",
+ "@nguniversal/express-engine": "^13.0.2",
"@ngx-translate/core": "^13.0.0",
- "@nicky-lenaers/ngx-scroll-to": "^9.0.0",
+ "@nicky-lenaers/ngx-scroll-to": "^13.0.0",
+ "@types/grecaptcha": "^3.0.4",
"angular-idle-preload": "3.0.0",
- "angular2-text-mask": "9.0.0",
- "angulartics2": "^10.0.0",
- "bootstrap": "4.3.1",
- "caniuse-lite": "^1.0.30001165",
+ "angulartics2": "^12.0.0",
+ "axios": "^0.27.2",
+ "bootstrap": "^4.6.1",
"cerialize": "0.1.18",
"cli-progress": "^3.8.0",
+ "colors": "^1.4.0",
"compression": "^1.7.4",
"cookie-parser": "1.4.5",
"core-js": "^3.7.0",
+ "date-fns": "^2.29.3",
+ "date-fns-tz": "^1.3.7",
"deepmerge": "^4.2.2",
"express": "^4.17.1",
"express-rate-limit": "^5.1.3",
"fast-json-patch": "^3.0.0-1",
- "file-saver": "^2.0.5",
"filesize": "^6.1.0",
- "font-awesome": "4.7.0",
"http-proxy-middleware": "^1.0.5",
- "https": "1.0.0",
"js-cookie": "2.2.1",
"js-yaml": "^4.1.0",
"json5": "^2.1.3",
"jsonschema": "1.4.0",
"jwt-decode": "^3.1.2",
- "klaro": "^0.7.10",
+ "klaro": "^0.7.18",
"lodash": "^4.17.21",
+ "markdown-it": "^13.0.1",
+ "markdown-it-mathjax3": "^4.3.1",
"mirador": "^3.3.0",
"mirador-dl-plugin": "^0.13.0",
"mirador-share-plugin": "^0.11.0",
- "moment": "^2.29.1",
"morgan": "^1.10.0",
- "ng-mocks": "11.11.2",
+ "ng-mocks": "^13.1.1",
"ng2-file-upload": "1.4.0",
"ng2-nouislider": "^1.8.3",
"ngx-infinite-scroll": "^10.0.1",
- "ngx-moment": "^5.0.0",
"ngx-pagination": "5.0.0",
"ngx-sortablejs": "^11.1.0",
+ "ngx-ui-switch": "^13.0.2",
"nouislider": "^14.6.3",
"pem": "1.14.4",
- "postcss-cli": "^8.3.0",
+ "prop-types": "^15.7.2",
+ "react-copy-to-clipboard": "^5.0.1",
"reflect-metadata": "^0.1.13",
- "rxjs": "^6.6.3",
+ "rxjs": "^7.5.5",
+ "sanitize-html": "^2.7.2",
"sortablejs": "1.13.0",
- "tslib": "^2.0.0",
- "url-parse": "^1.5.3",
"uuid": "^8.3.2",
"webfontloader": "1.6.28",
- "zone.js": "^0.10.3"
+ "zone.js": "~0.11.5"
},
"devDependencies": {
- "@angular-builders/custom-webpack": "10.0.1",
- "@angular-devkit/build-angular": "~0.1102.15",
- "@angular/cli": "~11.2.15",
- "@angular/compiler-cli": "~11.2.14",
- "@angular/language-service": "~11.2.14",
+ "@angular-builders/custom-webpack": "~13.1.0",
+ "@angular-devkit/build-angular": "~13.3.10",
+ "@angular-eslint/builder": "13.1.0",
+ "@angular-eslint/eslint-plugin": "13.1.0",
+ "@angular-eslint/eslint-plugin-template": "13.1.0",
+ "@angular-eslint/schematics": "13.1.0",
+ "@angular-eslint/template-parser": "13.1.0",
+ "@angular/cli": "~13.3.10",
+ "@angular/compiler-cli": "~13.3.12",
+ "@angular/language-service": "~13.3.12",
"@cypress/schematic": "^1.5.0",
- "@fortawesome/fontawesome-free": "^5.5.0",
- "@ngrx/store-devtools": "^11.1.1",
- "@ngtools/webpack": "10.2.3",
- "@nguniversal/builders": "~11.2.1",
+ "@fortawesome/fontawesome-free": "^6.2.1",
+ "@ngrx/store-devtools": "^13.0.2",
+ "@ngtools/webpack": "^13.2.6",
+ "@nguniversal/builders": "^13.1.1",
"@types/deep-freeze": "0.1.2",
"@types/express": "^4.17.9",
- "@types/file-saver": "^2.0.1",
"@types/jasmine": "~3.6.0",
- "@types/jasminewd2": "~2.0.8",
"@types/js-cookie": "2.2.6",
"@types/lodash": "^4.14.165",
"@types/node": "^14.14.9",
- "axe-core": "^4.3.3",
- "codelyzer": "^6.0.0",
- "compression-webpack-plugin": "^3.0.1",
+ "@types/sanitize-html": "^2.6.2",
+ "@typescript-eslint/eslint-plugin": "5.11.0",
+ "@typescript-eslint/parser": "5.11.0",
+ "axe-core": "^4.4.3",
+ "compression-webpack-plugin": "^9.2.0",
"copy-webpack-plugin": "^6.4.1",
"cross-env": "^7.0.3",
- "css-loader": "3.4.0",
- "cssnano": "^4.1.10",
- "cypress": "8.6.0",
- "cypress-axe": "^0.13.0",
- "debug-loader": "^0.0.1",
+ "cypress": "9.7.0",
+ "cypress-axe": "^0.14.0",
"deep-freeze": "0.0.1",
- "dotenv": "^8.2.0",
- "fork-ts-checker-webpack-plugin": "^6.0.3",
- "html-loader": "^1.3.2",
- "html-webpack-plugin": "^4.5.0",
- "jasmine-core": "~3.6.0",
- "jasmine-marbles": "0.6.0",
- "jasmine-spec-reporter": "~5.0.0",
- "karma": "^5.2.3",
+ "eslint": "^8.2.0",
+ "eslint-plugin-deprecation": "^1.3.2",
+ "eslint-plugin-import": "^2.25.4",
+ "eslint-plugin-jsdoc": "^39.6.4",
+ "eslint-plugin-lodash": "^7.4.0",
+ "eslint-plugin-unused-imports": "^2.0.0",
+ "express-static-gzip": "^2.1.5",
+ "jasmine-core": "^3.8.0",
+ "jasmine-marbles": "0.9.2",
+ "karma": "^6.3.14",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage-istanbul-reporter": "~3.0.2",
"karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "^1.5.0",
"karma-mocha-reporter": "2.2.5",
- "nodemon": "^2.0.15",
- "optimize-css-assets-webpack-plugin": "^5.0.4",
- "postcss-apply": "0.11.0",
- "postcss-import": "^12.0.1",
- "postcss-loader": "^3.0.0",
- "postcss-preset-env": "6.7.0",
+ "ngx-mask": "^13.1.7",
+ "nodemon": "^2.0.20",
+ "postcss": "^8.1",
+ "postcss-apply": "0.12.0",
+ "postcss-import": "^14.0.0",
+ "postcss-loader": "^4.0.3",
+ "postcss-preset-env": "^7.4.2",
"postcss-responsive-type": "1.0.0",
- "protractor": "^7.0.0",
- "protractor-istanbul-plugin": "2.0.0",
- "raw-loader": "0.5.1",
"react": "^16.14.0",
"react-dom": "^16.14.0",
"rimraf": "^3.0.2",
- "rxjs-spy": "^7.5.3",
+ "rxjs-spy": "^8.0.2",
+ "sass": "~1.33.0",
+ "sass-loader": "^12.6.0",
"sass-resources-loader": "^2.1.1",
- "script-ext-html-webpack-plugin": "2.1.5",
- "string-replace-loader": "^2.3.0",
- "terser-webpack-plugin": "^2.3.1",
- "ts-loader": "^5.2.0",
"ts-node": "^8.10.2",
- "tslint": "^6.1.3",
- "typescript": "~4.0.5",
- "webpack": "^4.44.2",
+ "typescript": "~4.5.5",
+ "webpack": "^5.69.1",
"webpack-bundle-analyzer": "^4.4.0",
"webpack-cli": "^4.2.0",
"webpack-dev-server": "^4.5.0"
diff --git a/scripts/base-href.ts b/scripts/base-href.ts
new file mode 100644
index 0000000000..7212e1c516
--- /dev/null
+++ b/scripts/base-href.ts
@@ -0,0 +1,36 @@
+import { existsSync, writeFileSync } from 'fs';
+import { join } from 'path';
+
+import { AppConfig } from '../src/config/app-config.interface';
+import { buildAppConfig } from '../src/config/config.server';
+
+/**
+ * Script to set baseHref as `ui.nameSpace` for development mode. Adds `baseHref` to angular.json build options.
+ *
+ * Usage (see package.json):
+ *
+ * yarn base-href
+ */
+
+const appConfig: AppConfig = buildAppConfig();
+
+const angularJsonPath = join(process.cwd(), 'angular.json');
+
+if (!existsSync(angularJsonPath)) {
+ console.error(`Error:\n${angularJsonPath} does not exist\n`);
+ process.exit(1);
+}
+
+try {
+ const angularJson = require(angularJsonPath);
+
+ const baseHref = `${appConfig.ui.nameSpace}${appConfig.ui.nameSpace.endsWith('/') ? '' : '/'}`;
+
+ console.log(`Setting baseHref to ${baseHref} in angular.json`);
+
+ angularJson.projects['dspace-angular'].architect.build.options.baseHref = baseHref;
+
+ writeFileSync(angularJsonPath, JSON.stringify(angularJson, null, 2) + '\n');
+} catch (e) {
+ console.error(e);
+}
diff --git a/scripts/env-to-yaml.ts b/scripts/env-to-yaml.ts
index edcdfd90b4..6e8153f4c1 100644
--- a/scripts/env-to-yaml.ts
+++ b/scripts/env-to-yaml.ts
@@ -1,5 +1,5 @@
-import * as fs from 'fs';
-import * as yaml from 'js-yaml';
+import { existsSync, writeFileSync } from 'fs';
+import { dump } from 'js-yaml';
import { join } from 'path';
/**
@@ -18,18 +18,18 @@ if (args[0] === undefined) {
const envFullPath = join(process.cwd(), args[0]);
-if (!fs.existsSync(envFullPath)) {
+if (!existsSync(envFullPath)) {
console.error(`Error:\n${envFullPath} does not exist\n`);
process.exit(1);
}
try {
- const env = require(envFullPath);
+ const env = require(envFullPath).environment;
- const config = yaml.dump(env);
+ const config = dump(env);
if (args[1]) {
const ymlFullPath = join(process.cwd(), args[1]);
- fs.writeFileSync(ymlFullPath, config);
+ writeFileSync(ymlFullPath, config);
} else {
console.log(config);
}
diff --git a/scripts/serve.ts b/scripts/serve.ts
index bf5506b8bd..ee8570a45c 100644
--- a/scripts/serve.ts
+++ b/scripts/serve.ts
@@ -1,4 +1,4 @@
-import * as child from 'child_process';
+import { spawn } from 'child_process';
import { AppConfig } from '../src/config/app-config.interface';
import { buildAppConfig } from '../src/config/config.server';
@@ -7,8 +7,9 @@ const appConfig: AppConfig = buildAppConfig();
/**
* Calls `ng serve` with the following arguments configured for the UI in the app config: host, port, nameSpace, ssl
+ * Any CLI arguments given to this script are patched through to `ng serve` as well.
*/
-child.spawn(
- `ng serve --host ${appConfig.ui.host} --port ${appConfig.ui.port} --serve-path ${appConfig.ui.nameSpace} --ssl ${appConfig.ui.ssl}`,
+spawn(
+ `ng serve --host ${appConfig.ui.host} --port ${appConfig.ui.port} --serve-path ${appConfig.ui.nameSpace} --ssl ${appConfig.ui.ssl} ${process.argv.slice(2).join(' ')} --configuration development`,
{ stdio: 'inherit', shell: true }
);
diff --git a/scripts/sync-i18n-files.ts b/scripts/sync-i18n-files.ts
old mode 100755
new mode 100644
index ad8a712f21..96ba0d4010
--- a/scripts/sync-i18n-files.ts
+++ b/scripts/sync-i18n-files.ts
@@ -1,4 +1,5 @@
-import { projectRoot} from '../webpack/helpers';
+import { projectRoot } from '../webpack/helpers';
+
const commander = require('commander');
const fs = require('fs');
const JSON5 = require('json5');
@@ -119,7 +120,7 @@ function syncFileWithSource(pathToTargetFile, pathToOutputFile) {
outputChunks.forEach(function (chunk) {
progressBar.increment();
chunk.split("\n").forEach(function (line) {
- file.write(" " + line + "\n");
+ file.write((line === '' ? '' : ` ${line}`) + "\n");
});
});
file.write("\n}");
@@ -192,7 +193,10 @@ function createNewChunkComparingSourceAndTarget(correspondingTargetChunk, source
const targetList = correspondingTargetChunk.split("\n");
const oldKeyValueInTargetComments = getSubStringWithRegex(correspondingTargetChunk, "\\s*\\/\\/\\s*\".*");
- const keyValueTarget = targetList[targetList.length - 1];
+ let keyValueTarget = targetList[targetList.length - 1];
+ if (!keyValueTarget.endsWith(",")) {
+ keyValueTarget = keyValueTarget + ",";
+ }
if (oldKeyValueInTargetComments != null) {
const oldKeyValueUncommented = getSubStringWithRegex(oldKeyValueInTargetComments[0], "\".*")[0];
diff --git a/scripts/test-rest.ts b/scripts/test-rest.ts
index aa3b64f62b..9066777c42 100644
--- a/scripts/test-rest.ts
+++ b/scripts/test-rest.ts
@@ -1,9 +1,9 @@
-import * as http from 'http';
-import * as https from 'https';
+import { request } from 'http';
+import { request as https_request } from 'https';
import { AppConfig } from '../src/config/app-config.interface';
import { buildAppConfig } from '../src/config/config.server';
-
+
const appConfig: AppConfig = buildAppConfig();
/**
@@ -20,9 +20,15 @@ console.log(`...Testing connection to REST API at ${restUrl}...\n`);
// If SSL enabled, test via HTTPS, else via HTTP
if (appConfig.rest.ssl) {
- const req = https.request(restUrl, (res) => {
+ const req = https_request(restUrl, (res) => {
console.log(`RESPONSE: ${res.statusCode} ${res.statusMessage} \n`);
- res.on('data', (data) => {
+ // We will keep reading data until the 'end' event fires.
+ // This ensures we don't just read the first chunk.
+ let data = '';
+ res.on('data', (chunk) => {
+ data += chunk;
+ });
+ res.on('end', () => {
checkJSONResponse(data);
});
});
@@ -33,9 +39,15 @@ if (appConfig.rest.ssl) {
req.end();
} else {
- const req = http.request(restUrl, (res) => {
+ const req = request(restUrl, (res) => {
console.log(`RESPONSE: ${res.statusCode} ${res.statusMessage} \n`);
- res.on('data', (data) => {
+ // We will keep reading data until the 'end' event fires.
+ // This ensures we don't just read the first chunk.
+ let data = '';
+ res.on('data', (chunk) => {
+ data += chunk;
+ });
+ res.on('end', () => {
checkJSONResponse(data);
});
});
diff --git a/server.ts b/server.ts
index da3b877bc1..608c214076 100644
--- a/server.ts
+++ b/server.ts
@@ -15,16 +15,21 @@
* import for `ngExpressEngine`.
*/
-import 'zone.js/dist/zone-node';
+import 'zone.js/node';
import 'reflect-metadata';
import 'rxjs';
-import * as pem from 'pem';
-import * as https from 'https';
+/* eslint-disable import/no-namespace */
import * as morgan from 'morgan';
import * as express from 'express';
-import * as bodyParser from 'body-parser';
import * as compression from 'compression';
+import * as expressStaticGzip from 'express-static-gzip';
+/* eslint-enable import/no-namespace */
+
+import axios from 'axios';
+import { createCertificate } from 'pem';
+import { createServer } from 'https';
+import { json } from 'body-parser';
import { existsSync, readFileSync } from 'fs';
import { join } from 'path';
@@ -37,15 +42,16 @@ import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens';
import { environment } from './src/environments/environment';
import { createProxyMiddleware } from 'http-proxy-middleware';
-import { hasValue, hasNoValue } from './src/app/shared/empty.util';
+import { hasNoValue, hasValue } from './src/app/shared/empty.util';
import { UIServerConfig } from './src/config/ui-server-config.interface';
import { ServerAppModule } from './src/main.server';
import { buildAppConfig } from './src/config/config.server';
-import { AppConfig, APP_CONFIG } from './src/config/app-config.interface';
+import { APP_CONFIG, AppConfig } from './src/config/app-config.interface';
import { extendEnvironmentWithAppConfig } from './src/config/config.util';
+import { logStartupMessage } from './startup-message';
/*
* Set path for the browser application's dist folder
@@ -66,19 +72,29 @@ extendEnvironmentWithAppConfig(environment, appConfig);
// The Express app is exported so that it can be used by serverless Functions.
export function app() {
+ const router = express.Router();
+
/*
* Create a new express application
*/
const server = express();
+ // Tell Express to trust X-FORWARDED-* headers from proxies
+ // See https://expressjs.com/en/guide/behind-proxies.html
+ server.set('trust proxy', environment.ui.useProxies);
+
/*
* If production mode is enabled in the environment file:
* - Enable Angular's production mode
- * - Enable compression for response bodies. See [compression](https://github.com/expressjs/compression)
+ * - Enable compression for SSR reponses. See [compression](https://github.com/expressjs/compression)
*/
if (environment.production) {
enableProdMode();
- server.use(compression());
+ server.use(compression({
+ // only compress responses we've marked as SSR
+ // otherwise, this middleware may compress files we've chosen not to compress via compression-webpack-plugin
+ filter: (_, res) => res.locals.ssr,
+ }));
}
/*
@@ -97,7 +113,7 @@ export function app() {
* Add parser for request bodies
* See [morgan](https://github.com/expressjs/body-parser)
*/
- server.use(bodyParser.json());
+ server.use(json());
// Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine)
server.engine('html', (_, options, callback) =>
@@ -133,7 +149,11 @@ export function app() {
/**
* Proxy the sitemaps
*/
- server.use('/sitemap**', createProxyMiddleware({ target: `${environment.rest.baseUrl}/sitemaps`, changeOrigin: true }));
+ router.use('/sitemap**', createProxyMiddleware({
+ target: `${environment.rest.baseUrl}/sitemaps`,
+ pathRewrite: path => path.replace(environment.ui.nameSpace, '/'),
+ changeOrigin: true
+ }));
/**
* Checks if the rateLimiter property is present
@@ -150,15 +170,28 @@ export function app() {
/*
* Serve static resources (images, i18n messages, …)
+ * Handle pre-compressed files with [express-static-gzip](https://github.com/tkoenig89/express-static-gzip)
*/
- server.get('*.*', cacheControl, express.static(DIST_FOLDER, { index: false }));
+ router.get('*.*', cacheControl, expressStaticGzip(DIST_FOLDER, {
+ index: false,
+ enableBrotli: true,
+ orderPreference: ['br', 'gzip'],
+ }));
+
/*
* Fallthrough to the IIIF viewer (must be included in the build).
*/
- server.use('/iiif', express.static(IIIF_VIEWER, {index:false}));
+ router.use('/iiif', express.static(IIIF_VIEWER, { index: false }));
+
+ /**
+ * Checking server status
+ */
+ server.get('/app/health', healthCheck);
// Register the ngApp callback function to handle incoming requests
- server.get('*', ngApp);
+ router.get('*', ngApp);
+
+ server.use(environment.ui.nameSpace, router);
return server;
}
@@ -180,6 +213,7 @@ function ngApp(req, res) {
providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }]
}, (err, data) => {
if (hasNoValue(err) && hasValue(data)) {
+ res.locals.ssr = true; // mark response as SSR
res.send(data);
} else if (hasValue(err) && err.code === 'ERR_HTTP_HEADERS_SENT') {
// When this error occurs we can't fall back to CSR because the response has already been
@@ -191,13 +225,25 @@ function ngApp(req, res) {
if (hasValue(err)) {
console.warn('Error details : ', err);
}
- res.sendFile(DIST_FOLDER + '/index.html');
+ res.render(indexHtml, {
+ req,
+ providers: [{
+ provide: APP_BASE_HREF,
+ useValue: req.baseUrl
+ }]
+ });
}
});
} else {
// If preboot is disabled, just serve the client
console.log('Universal off, serving for direct CSR');
- res.sendFile(DIST_FOLDER + '/index.html');
+ res.render(indexHtml, {
+ req,
+ providers: [{
+ provide: APP_BASE_HREF,
+ useValue: req.baseUrl
+ }]
+ });
}
}
@@ -223,7 +269,7 @@ function serverStarted() {
* @param keys SSL credentials
*/
function createHttpsServer(keys) {
- https.createServer({
+ createServer({
key: keys.serviceKey,
cert: keys.certificate
}, app).listen(environment.ui.port, environment.ui.host, () => {
@@ -243,6 +289,8 @@ function run() {
}
function start() {
+ logStartupMessage(environment);
+
/*
* If SSL is enabled
* - Read credentials from configuration files
@@ -275,7 +323,7 @@ function start() {
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; // lgtm[js/disabling-certificate-validation]
- pem.createCertificate({
+ createCertificate({
days: 1,
selfSigned: true
}, (error, keys) => {
@@ -287,6 +335,21 @@ function start() {
}
}
+/*
+ * The callback function to serve health check requests
+ */
+function healthCheck(req, res) {
+ const baseUrl = `${environment.rest.baseUrl}${environment.actuators.endpointPath}`;
+ axios.get(baseUrl)
+ .then((response) => {
+ res.status(response.status).send(response.data);
+ })
+ .catch((error) => {
+ res.status(error.response.status).send({
+ error: error.message
+ });
+ });
+}
// Webpack will replace 'require' with '__webpack_require__'
// '__non_webpack_require__' is a proxy to Node 'require'
// The below code is to ensure that the server is run only when not requiring the bundle.
diff --git a/src/app/access-control/access-control.module.ts b/src/app/access-control/access-control.module.ts
index 891238bbed..afb92a9111 100644
--- a/src/app/access-control/access-control.module.ts
+++ b/src/app/access-control/access-control.module.ts
@@ -10,6 +10,16 @@ import { MembersListComponent } from './group-registry/group-form/members-list/m
import { SubgroupsListComponent } from './group-registry/group-form/subgroup-list/subgroups-list.component';
import { GroupsRegistryComponent } from './group-registry/groups-registry.component';
import { FormModule } from '../shared/form/form.module';
+import { DYNAMIC_ERROR_MESSAGES_MATCHER, DynamicErrorMessagesMatcher } from '@ng-dynamic-forms/core';
+import { AbstractControl } from '@angular/forms';
+
+/**
+ * Condition for displaying error messages on email form field
+ */
+export const ValidateEmailErrorStateMatcher: DynamicErrorMessagesMatcher =
+ (control: AbstractControl, model: any, hasFocus: boolean) => {
+ return (control.touched && !hasFocus) || (control.errors?.emailTaken && hasFocus);
+ };
@NgModule({
imports: [
@@ -26,6 +36,12 @@ import { FormModule } from '../shared/form/form.module';
GroupFormComponent,
SubgroupsListComponent,
MembersListComponent
+ ],
+ providers: [
+ {
+ provide: DYNAMIC_ERROR_MESSAGES_MATCHER,
+ useValue: ValidateEmailErrorStateMatcher
+ },
]
})
/**
diff --git a/src/app/access-control/epeople-registry/epeople-registry.actions.ts b/src/app/access-control/epeople-registry/epeople-registry.actions.ts
index b8b1044362..a07ea37df2 100644
--- a/src/app/access-control/epeople-registry/epeople-registry.actions.ts
+++ b/src/app/access-control/epeople-registry/epeople-registry.actions.ts
@@ -1,3 +1,4 @@
+/* eslint-disable max-classes-per-file */
import { Action } from '@ngrx/store';
import { EPerson } from '../../core/eperson/models/eperson.model';
import { type } from '../../shared/ngrx/type';
@@ -16,7 +17,6 @@ export const EPeopleRegistryActionTypes = {
CANCEL_EDIT_EPERSON: type('dspace/epeople-registry/CANCEL_EDIT_EPERSON'),
};
-/* tslint:disable:max-classes-per-file */
/**
* Used to edit an EPerson in the EPeople registry
*/
@@ -37,7 +37,6 @@ export class EPeopleRegistryCancelEPersonAction implements Action {
type = EPeopleRegistryActionTypes.CANCEL_EDIT_EPERSON;
}
-/* tslint:enable:max-classes-per-file */
/**
* Export a type alias of all actions in this action group
diff --git a/src/app/access-control/epeople-registry/epeople-registry.component.html b/src/app/access-control/epeople-registry/epeople-registry.component.html
index 7ef02a76cf..2d87f21d26 100644
--- a/src/app/access-control/epeople-registry/epeople-registry.component.html
+++ b/src/app/access-control/epeople-registry/epeople-registry.component.html
@@ -45,7 +45,7 @@
-
+ 0 && !(searching$ | async)"
[paginationOptions]="config"
diff --git a/src/app/access-control/epeople-registry/epeople-registry.component.spec.ts b/src/app/access-control/epeople-registry/epeople-registry.component.spec.ts
index bcf7e8f1d9..c0d70fd0b2 100644
--- a/src/app/access-control/epeople-registry/epeople-registry.component.spec.ts
+++ b/src/app/access-control/epeople-registry/epeople-registry.component.spec.ts
@@ -9,7 +9,6 @@ import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core';
import { buildPaginatedList, PaginatedList } from '../../core/data/paginated-list.model';
import { RemoteData } from '../../core/data/remote-data';
-import { FindListOptions } from '../../core/data/request.models';
import { EPersonDataService } from '../../core/eperson/eperson-data.service';
import { EPerson } from '../../core/eperson/models/eperson.model';
import { PageInfo } from '../../core/shared/page-info.model';
@@ -27,6 +26,7 @@ import { AuthorizationDataService } from '../../core/data/feature-authorization/
import { RequestService } from '../../core/data/request.service';
import { PaginationService } from '../../core/pagination/pagination.service';
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
+import { FindListOptions } from '../../core/data/find-list-options.model';
describe('EPeopleRegistryComponent', () => {
let component: EPeopleRegistryComponent;
diff --git a/src/app/access-control/epeople-registry/epeople-registry.component.ts b/src/app/access-control/epeople-registry/epeople-registry.component.ts
index b99304d037..55233d8173 100644
--- a/src/app/access-control/epeople-registry/epeople-registry.component.ts
+++ b/src/app/access-control/epeople-registry/epeople-registry.component.ts
@@ -238,7 +238,6 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
this.epersonService.deleteEPerson(ePerson).pipe(getFirstCompletedRemoteData()).subscribe((restResponse: RemoteData) => {
if (restResponse.hasSucceeded) {
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', {name: ePerson.name}));
- this.reset();
} else {
this.notificationsService.error('Error occured when trying to delete EPerson with id: ' + ePerson.id + ' with code: ' + restResponse.statusCode + ' and message: ' + restResponse.errorMessage);
}
diff --git a/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.html b/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.html
index 41ae67423c..e9cc48aee3 100644
--- a/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.html
+++ b/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.html
@@ -36,12 +36,12 @@
-
+