mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 18:14:17 +00:00
Merge branch 'main' into SpanishTranslationKeys
This commit is contained in:
@@ -25,4 +25,6 @@ npm-debug.log.*
|
|||||||
|
|
||||||
# Webpack files
|
# Webpack files
|
||||||
webpack.records.json
|
webpack.records.json
|
||||||
package-lock.json
|
|
||||||
|
# Yarn no longer used
|
||||||
|
yarn.lock
|
||||||
|
@@ -165,6 +165,7 @@
|
|||||||
"@angular-eslint/no-output-native": "warn",
|
"@angular-eslint/no-output-native": "warn",
|
||||||
"@angular-eslint/no-output-on-prefix": "warn",
|
"@angular-eslint/no-output-on-prefix": "warn",
|
||||||
"@angular-eslint/no-conflicting-lifecycle": "warn",
|
"@angular-eslint/no-conflicting-lifecycle": "warn",
|
||||||
|
"@angular-eslint/use-lifecycle-interface": "error",
|
||||||
|
|
||||||
"@typescript-eslint/no-inferrable-types":[
|
"@typescript-eslint/no-inferrable-types":[
|
||||||
"error",
|
"error",
|
||||||
|
298
.github/dependabot.yml
vendored
Normal file
298
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
#-------------------
|
||||||
|
# DSpace's dependabot rules. Enables npm updates for all dependencies on a weekly basis
|
||||||
|
# for main and any maintenance branches. Security updates only apply to main.
|
||||||
|
#-------------------
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
###############
|
||||||
|
## Main branch
|
||||||
|
###############
|
||||||
|
# NOTE: At this time, "security-updates" rules only apply if "target-branch" is unspecified
|
||||||
|
# So, only this first section can include "applies-to: security-updates"
|
||||||
|
- package-ecosystem: "npm"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
# Allow up to 10 open PRs for dependencies
|
||||||
|
open-pull-requests-limit: 10
|
||||||
|
# Group together Angular package upgrades
|
||||||
|
groups:
|
||||||
|
# Group together all minor/patch version updates for Angular in a single PR
|
||||||
|
angular:
|
||||||
|
applies-to: version-updates
|
||||||
|
patterns:
|
||||||
|
- "@angular*"
|
||||||
|
update-types:
|
||||||
|
- "minor"
|
||||||
|
- "patch"
|
||||||
|
# Group together all security updates for Angular. Only accept minor/patch types.
|
||||||
|
angular-security:
|
||||||
|
applies-to: security-updates
|
||||||
|
patterns:
|
||||||
|
- "@angular*"
|
||||||
|
update-types:
|
||||||
|
- "minor"
|
||||||
|
- "patch"
|
||||||
|
# Group together all minor/patch version updates for NgRx in a single PR
|
||||||
|
ngrx:
|
||||||
|
applies-to: version-updates
|
||||||
|
patterns:
|
||||||
|
- "@ngrx*"
|
||||||
|
update-types:
|
||||||
|
- "minor"
|
||||||
|
- "patch"
|
||||||
|
# Group together all security updates for NgRx. Only accept minor/patch types.
|
||||||
|
ngrx-security:
|
||||||
|
applies-to: security-updates
|
||||||
|
patterns:
|
||||||
|
- "@ngrx*"
|
||||||
|
update-types:
|
||||||
|
- "minor"
|
||||||
|
- "patch"
|
||||||
|
# Group together all patch version updates for eslint in a single PR
|
||||||
|
eslint:
|
||||||
|
applies-to: version-updates
|
||||||
|
patterns:
|
||||||
|
- "@typescript-eslint*"
|
||||||
|
- "eslint*"
|
||||||
|
update-types:
|
||||||
|
- "minor"
|
||||||
|
- "patch"
|
||||||
|
# Group together all security updates for eslint.
|
||||||
|
eslint-security:
|
||||||
|
applies-to: security-updates
|
||||||
|
patterns:
|
||||||
|
- "@typescript-eslint*"
|
||||||
|
- "eslint*"
|
||||||
|
update-types:
|
||||||
|
- "minor"
|
||||||
|
- "patch"
|
||||||
|
# Group together any testing related version updates
|
||||||
|
testing:
|
||||||
|
applies-to: version-updates
|
||||||
|
patterns:
|
||||||
|
- "@cypress*"
|
||||||
|
- "axe-*"
|
||||||
|
- "cypress*"
|
||||||
|
- "jasmine*"
|
||||||
|
- "karma*"
|
||||||
|
- "ng-mocks"
|
||||||
|
update-types:
|
||||||
|
- "minor"
|
||||||
|
- "patch"
|
||||||
|
# Group together any testing related security updates
|
||||||
|
testing-security:
|
||||||
|
applies-to: security-updates
|
||||||
|
patterns:
|
||||||
|
- "@cypress*"
|
||||||
|
- "axe-*"
|
||||||
|
- "cypress*"
|
||||||
|
- "jasmine*"
|
||||||
|
- "karma*"
|
||||||
|
- "ng-mocks"
|
||||||
|
update-types:
|
||||||
|
- "minor"
|
||||||
|
- "patch"
|
||||||
|
# Group together any postcss related version updates
|
||||||
|
postcss:
|
||||||
|
applies-to: version-updates
|
||||||
|
patterns:
|
||||||
|
- "postcss*"
|
||||||
|
update-types:
|
||||||
|
- "minor"
|
||||||
|
- "patch"
|
||||||
|
# Group together any postcss related security updates
|
||||||
|
postcss-security:
|
||||||
|
applies-to: security-updates
|
||||||
|
patterns:
|
||||||
|
- "postcss*"
|
||||||
|
update-types:
|
||||||
|
- "minor"
|
||||||
|
- "patch"
|
||||||
|
# Group together any sass related version updates
|
||||||
|
sass:
|
||||||
|
applies-to: version-updates
|
||||||
|
patterns:
|
||||||
|
- "sass*"
|
||||||
|
update-types:
|
||||||
|
- "minor"
|
||||||
|
- "patch"
|
||||||
|
# Group together any sass related security updates
|
||||||
|
sass-security:
|
||||||
|
applies-to: security-updates
|
||||||
|
patterns:
|
||||||
|
- "sass*"
|
||||||
|
update-types:
|
||||||
|
- "minor"
|
||||||
|
- "patch"
|
||||||
|
# Group together any webpack related version updates
|
||||||
|
webpack:
|
||||||
|
applies-to: version-updates
|
||||||
|
patterns:
|
||||||
|
- "webpack*"
|
||||||
|
update-types:
|
||||||
|
- "minor"
|
||||||
|
- "patch"
|
||||||
|
# Group together any webpack related seurity updates
|
||||||
|
webpack-security:
|
||||||
|
applies-to: security-updates
|
||||||
|
patterns:
|
||||||
|
- "webpack*"
|
||||||
|
update-types:
|
||||||
|
- "minor"
|
||||||
|
- "patch"
|
||||||
|
ignore:
|
||||||
|
# Ignore all major version updates for all dependencies. We'll only automate minor/patch updates.
|
||||||
|
- dependency-name: "*"
|
||||||
|
update-types: ["version-update:semver-major"]
|
||||||
|
#####################
|
||||||
|
## dspace-8_x branch
|
||||||
|
#####################
|
||||||
|
- package-ecosystem: "npm"
|
||||||
|
directory: "/"
|
||||||
|
target-branch: dspace-8_x
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
# Allow up to 10 open PRs for dependencies
|
||||||
|
open-pull-requests-limit: 10
|
||||||
|
# Group together Angular package upgrades
|
||||||
|
groups:
|
||||||
|
# Group together all patch version updates for Angular in a single PR
|
||||||
|
angular:
|
||||||
|
applies-to: version-updates
|
||||||
|
patterns:
|
||||||
|
- "@angular*"
|
||||||
|
update-types:
|
||||||
|
- "minor"
|
||||||
|
- "patch"
|
||||||
|
# Group together all minor/patch version updates for NgRx in a single PR
|
||||||
|
ngrx:
|
||||||
|
applies-to: version-updates
|
||||||
|
patterns:
|
||||||
|
- "@ngrx*"
|
||||||
|
update-types:
|
||||||
|
- "minor"
|
||||||
|
- "patch"
|
||||||
|
# Group together all patch version updates for eslint in a single PR
|
||||||
|
eslint:
|
||||||
|
applies-to: version-updates
|
||||||
|
patterns:
|
||||||
|
- "@typescript-eslint*"
|
||||||
|
- "eslint*"
|
||||||
|
update-types:
|
||||||
|
- "minor"
|
||||||
|
- "patch"
|
||||||
|
# Group together any testing related version updates
|
||||||
|
testing:
|
||||||
|
applies-to: version-updates
|
||||||
|
patterns:
|
||||||
|
- "@cypress*"
|
||||||
|
- "axe-*"
|
||||||
|
- "cypress*"
|
||||||
|
- "jasmine*"
|
||||||
|
- "karma*"
|
||||||
|
- "ng-mocks"
|
||||||
|
update-types:
|
||||||
|
- "minor"
|
||||||
|
- "patch"
|
||||||
|
# Group together any postcss related version updates
|
||||||
|
postcss:
|
||||||
|
applies-to: version-updates
|
||||||
|
patterns:
|
||||||
|
- "postcss*"
|
||||||
|
update-types:
|
||||||
|
- "minor"
|
||||||
|
- "patch"
|
||||||
|
# Group together any sass related version updates
|
||||||
|
sass:
|
||||||
|
applies-to: version-updates
|
||||||
|
patterns:
|
||||||
|
- "sass*"
|
||||||
|
update-types:
|
||||||
|
- "minor"
|
||||||
|
- "patch"
|
||||||
|
# Group together any webpack related version updates
|
||||||
|
webpack:
|
||||||
|
applies-to: version-updates
|
||||||
|
patterns:
|
||||||
|
- "webpack*"
|
||||||
|
update-types:
|
||||||
|
- "minor"
|
||||||
|
- "patch"
|
||||||
|
ignore:
|
||||||
|
# Ignore all major version updates for all dependencies. We'll only automate minor/patch updates.
|
||||||
|
- dependency-name: "*"
|
||||||
|
update-types: ["version-update:semver-major"]
|
||||||
|
#####################
|
||||||
|
## dspace-7_x branch
|
||||||
|
#####################
|
||||||
|
- package-ecosystem: "npm"
|
||||||
|
directory: "/"
|
||||||
|
target-branch: dspace-7_x
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
# Allow up to 10 open PRs for dependencies
|
||||||
|
open-pull-requests-limit: 10
|
||||||
|
# Group together Angular package upgrades
|
||||||
|
groups:
|
||||||
|
# Group together all minor/patch version updates for Angular in a single PR
|
||||||
|
angular:
|
||||||
|
applies-to: version-updates
|
||||||
|
patterns:
|
||||||
|
- "@angular*"
|
||||||
|
update-types:
|
||||||
|
- "minor"
|
||||||
|
- "patch"
|
||||||
|
# Group together all minor/patch version updates for NgRx in a single PR
|
||||||
|
ngrx:
|
||||||
|
applies-to: version-updates
|
||||||
|
patterns:
|
||||||
|
- "@ngrx*"
|
||||||
|
update-types:
|
||||||
|
- "minor"
|
||||||
|
- "patch"
|
||||||
|
# Group together all patch version updates for eslint in a single PR
|
||||||
|
eslint:
|
||||||
|
applies-to: version-updates
|
||||||
|
patterns:
|
||||||
|
- "@typescript-eslint*"
|
||||||
|
- "eslint*"
|
||||||
|
update-types:
|
||||||
|
- "minor"
|
||||||
|
- "patch"
|
||||||
|
# Group together any testing related version updates
|
||||||
|
testing:
|
||||||
|
applies-to: version-updates
|
||||||
|
patterns:
|
||||||
|
- "@cypress*"
|
||||||
|
- "axe-*"
|
||||||
|
- "cypress*"
|
||||||
|
- "jasmine*"
|
||||||
|
- "karma*"
|
||||||
|
- "ng-mocks"
|
||||||
|
update-types:
|
||||||
|
- "minor"
|
||||||
|
- "patch"
|
||||||
|
# Group together any postcss related version updates
|
||||||
|
postcss:
|
||||||
|
applies-to: version-updates
|
||||||
|
patterns:
|
||||||
|
- "postcss*"
|
||||||
|
update-types:
|
||||||
|
- "minor"
|
||||||
|
- "patch"
|
||||||
|
# Group together any sass related version updates
|
||||||
|
sass:
|
||||||
|
applies-to: version-updates
|
||||||
|
patterns:
|
||||||
|
- "sass*"
|
||||||
|
update-types:
|
||||||
|
- "minor"
|
||||||
|
- "patch"
|
||||||
|
ignore:
|
||||||
|
# 7.x Cannot update Webpack past v5.76.1 as later versions not supported by Angular 15
|
||||||
|
# See also https://github.com/DSpace/dspace-angular/pull/3283#issuecomment-2372488489
|
||||||
|
- dependency-name: "webpack"
|
||||||
|
# Ignore all major version updates for all dependencies. We'll only automate minor/patch updates.
|
||||||
|
- dependency-name: "*"
|
||||||
|
update-types: ["version-update:semver-major"]
|
4
.github/pull_request_template.md
vendored
4
.github/pull_request_template.md
vendored
@@ -21,8 +21,8 @@ However, reviewers may request that you complete any actions in this list if you
|
|||||||
|
|
||||||
- [ ] My PR is **created against the `main` branch** of code (unless it is a backport or is fixing an issue specific to an older branch).
|
- [ ] My PR is **created against the `main` branch** of code (unless it is a backport or is fixing an issue specific to an older branch).
|
||||||
- [ ] My PR is **small in size** (e.g. less than 1,000 lines of code, not including comments & specs/tests), or I have provided reasons as to why that's not possible.
|
- [ ] My PR is **small in size** (e.g. less than 1,000 lines of code, not including comments & specs/tests), or I have provided reasons as to why that's not possible.
|
||||||
- [ ] My PR **passes [ESLint](https://eslint.org/)** validation using `yarn lint`
|
- [ ] My PR **passes [ESLint](https://eslint.org/)** validation using `npm run lint`
|
||||||
- [ ] My PR **doesn't introduce circular dependencies** (verified via `yarn check-circ-deps`)
|
- [ ] My PR **doesn't introduce circular dependencies** (verified via `npm run check-circ-deps`)
|
||||||
- [ ] My PR **includes [TypeDoc](https://typedoc.org/) comments** for _all new (or modified) public methods and classes_. It also includes TypeDoc for large or complex private methods.
|
- [ ] My PR **includes [TypeDoc](https://typedoc.org/) comments** for _all new (or modified) public methods and classes_. It also includes TypeDoc for large or complex private methods.
|
||||||
- [ ] My PR **passes all specs/tests and includes new/updated specs or tests** based on the [Code Testing Guide](https://wiki.lyrasis.org/display/DSPACE/Code+Testing+Guide).
|
- [ ] My PR **passes all specs/tests and includes new/updated specs or tests** based on the [Code Testing Guide](https://wiki.lyrasis.org/display/DSPACE/Code+Testing+Guide).
|
||||||
- [ ] My PR **aligns with [Accessibility guidelines](https://wiki.lyrasis.org/display/DSDOC8x/Accessibility)** if it makes changes to the user interface.
|
- [ ] My PR **aligns with [Accessibility guidelines](https://wiki.lyrasis.org/display/DSDOC8x/Accessibility)** if it makes changes to the user interface.
|
||||||
|
54
.github/workflows/build.yml
vendored
54
.github/workflows/build.yml
vendored
@@ -7,7 +7,8 @@ name: Build
|
|||||||
on: [push, pull_request]
|
on: [push, pull_request]
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read # to fetch code (actions/checkout)
|
contents: read # to fetch code (actions/checkout)
|
||||||
|
packages: read # to fetch private images from GitHub Container Registry (GHCR)
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
tests:
|
tests:
|
||||||
@@ -35,6 +36,9 @@ jobs:
|
|||||||
NODE_OPTIONS: '--max-old-space-size=4096'
|
NODE_OPTIONS: '--max-old-space-size=4096'
|
||||||
# Project name to use when running "docker compose" prior to e2e tests
|
# Project name to use when running "docker compose" prior to e2e tests
|
||||||
COMPOSE_PROJECT_NAME: 'ci'
|
COMPOSE_PROJECT_NAME: 'ci'
|
||||||
|
# Docker Registry to use for Docker compose scripts below.
|
||||||
|
# We use GitHub's Container Registry to avoid aggressive rate limits at DockerHub.
|
||||||
|
DOCKER_REGISTRY: ghcr.io
|
||||||
strategy:
|
strategy:
|
||||||
# Create a matrix of Node versions to test against (in parallel)
|
# Create a matrix of Node versions to test against (in parallel)
|
||||||
matrix:
|
matrix:
|
||||||
@@ -69,39 +73,39 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
google-chrome --version
|
google-chrome --version
|
||||||
|
|
||||||
# https://github.com/actions/cache/blob/main/examples.md#node---yarn
|
# https://github.com/actions/cache/blob/main/examples.md#node---npm
|
||||||
- name: Get Yarn cache directory
|
- name: Get NPM cache directory
|
||||||
id: yarn-cache-dir-path
|
id: npm-cache-dir
|
||||||
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
|
run: echo "dir=$(npm config get cache)" >> $GITHUB_OUTPUT
|
||||||
- name: Cache Yarn dependencies
|
- name: Cache NPM dependencies
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
# Cache entire Yarn cache directory (see previous step)
|
# Cache entire NPM cache directory (see previous step)
|
||||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
path: ${{ steps.npm-cache-dir.outputs.dir }}
|
||||||
# Cache key is hash of yarn.lock. Therefore changes to yarn.lock will invalidate cache
|
# Cache key is hash of package-lock.json. Therefore changes to package-lock.json will invalidate cache
|
||||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
|
||||||
restore-keys: ${{ runner.os }}-yarn-
|
restore-keys: ${{ runner.os }}-npm-
|
||||||
|
|
||||||
- name: Install Yarn dependencies
|
- name: Install NPM dependencies
|
||||||
run: yarn install --frozen-lockfile
|
run: npm clean-install
|
||||||
|
|
||||||
- name: Build lint plugins
|
- name: Build lint plugins
|
||||||
run: yarn run build:lint
|
run: npm run build:lint
|
||||||
|
|
||||||
- name: Run lint plugin tests
|
- name: Run lint plugin tests
|
||||||
run: yarn run test:lint:nobuild
|
run: npm run test:lint:nobuild
|
||||||
|
|
||||||
- name: Run lint
|
- name: Run lint
|
||||||
run: yarn run lint:nobuild --quiet
|
run: npm run lint:nobuild -- --quiet
|
||||||
|
|
||||||
- name: Check for circular dependencies
|
- name: Check for circular dependencies
|
||||||
run: yarn run check-circ-deps
|
run: npm run check-circ-deps
|
||||||
|
|
||||||
- name: Run build
|
- name: Run build
|
||||||
run: yarn run build:prod
|
run: npm run build:prod
|
||||||
|
|
||||||
- name: Run specs (unit tests)
|
- name: Run specs (unit tests)
|
||||||
run: yarn run test:headless
|
run: npm run test:headless
|
||||||
|
|
||||||
# Upload code coverage report to artifact (for one version of Node only),
|
# Upload code coverage report to artifact (for one version of Node only),
|
||||||
# so that it can be shared with the 'codecov' job (see below)
|
# so that it can be shared with the 'codecov' job (see below)
|
||||||
@@ -114,6 +118,14 @@ jobs:
|
|||||||
path: 'coverage/dspace-angular/lcov.info'
|
path: 'coverage/dspace-angular/lcov.info'
|
||||||
retention-days: 14
|
retention-days: 14
|
||||||
|
|
||||||
|
# Login to our Docker registry, so that we can access private Docker images using "docker compose" below.
|
||||||
|
- name: Login to ${{ env.DOCKER_REGISTRY }}
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.DOCKER_REGISTRY }}
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
# Using "docker compose" start backend using CI configuration
|
# Using "docker compose" start backend using CI configuration
|
||||||
# and load assetstore from a cached copy
|
# and load assetstore from a cached copy
|
||||||
- name: Start DSpace REST Backend via Docker (for e2e tests)
|
- name: Start DSpace REST Backend via Docker (for e2e tests)
|
||||||
@@ -131,7 +143,7 @@ jobs:
|
|||||||
# Run tests in Chrome, headless mode (default)
|
# Run tests in Chrome, headless mode (default)
|
||||||
browser: chrome
|
browser: chrome
|
||||||
# Start app before running tests (will be stopped automatically after tests finish)
|
# Start app before running tests (will be stopped automatically after tests finish)
|
||||||
start: yarn run serve:ssr
|
start: npm run serve:ssr
|
||||||
# Wait for backend & frontend to be available
|
# Wait for backend & frontend to be available
|
||||||
# NOTE: We use the 'sites' REST endpoint to also ensure the database is ready
|
# NOTE: We use the 'sites' REST endpoint to also ensure the database is ready
|
||||||
wait-on: http://127.0.0.1:8080/server/api/core/sites, http://127.0.0.1:4000
|
wait-on: http://127.0.0.1:8080/server/api/core/sites, http://127.0.0.1:4000
|
||||||
@@ -167,7 +179,7 @@ jobs:
|
|||||||
# Start up the app with SSR enabled (run in background)
|
# Start up the app with SSR enabled (run in background)
|
||||||
- name: Start app in SSR (server-side rendering) mode
|
- name: Start app in SSR (server-side rendering) mode
|
||||||
run: |
|
run: |
|
||||||
nohup yarn run serve:ssr &
|
nohup npm run serve:ssr &
|
||||||
printf 'Waiting for app to start'
|
printf 'Waiting for app to start'
|
||||||
until curl --output /dev/null --silent --head --fail http://127.0.0.1:4000/home; do
|
until curl --output /dev/null --silent --head --fail http://127.0.0.1:4000/home; do
|
||||||
printf '.'
|
printf '.'
|
||||||
|
3
.github/workflows/docker.yml
vendored
3
.github/workflows/docker.yml
vendored
@@ -16,7 +16,8 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read # to fetch code (actions/checkout)
|
contents: read # to fetch code (actions/checkout)
|
||||||
|
packages: write # to write images to GitHub Container Registry (GHCR)
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
#############################################################
|
#############################################################
|
||||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@@ -28,12 +28,12 @@ webpack.records.json
|
|||||||
|
|
||||||
morgan.log
|
morgan.log
|
||||||
|
|
||||||
|
# Yarn no longer used
|
||||||
|
yarn.lock
|
||||||
yarn-error.log
|
yarn-error.log
|
||||||
|
|
||||||
*.css
|
*.css
|
||||||
|
|
||||||
package-lock.json
|
|
||||||
|
|
||||||
.java-version
|
.java-version
|
||||||
|
|
||||||
.env
|
.env
|
||||||
|
10
Dockerfile
10
Dockerfile
@@ -1,7 +1,7 @@
|
|||||||
# This image will be published as dspace/dspace-angular
|
# This image will be published as dspace/dspace-angular
|
||||||
# See https://github.com/DSpace/dspace-angular/tree/main/docker for usage details
|
# See https://github.com/DSpace/dspace-angular/tree/main/docker for usage details
|
||||||
|
|
||||||
FROM node:18-alpine
|
FROM docker.io/node:18-alpine
|
||||||
|
|
||||||
# Ensure Python and other build tools are available
|
# Ensure Python and other build tools are available
|
||||||
# These are needed to install some node modules, especially on linux/arm64
|
# These are needed to install some node modules, especially on linux/arm64
|
||||||
@@ -11,9 +11,7 @@ WORKDIR /app
|
|||||||
ADD . /app/
|
ADD . /app/
|
||||||
EXPOSE 4000
|
EXPOSE 4000
|
||||||
|
|
||||||
# We run yarn install with an increased network timeout (5min) to avoid "ESOCKETTIMEDOUT" errors from hub.docker.com
|
RUN npm install
|
||||||
# See, for example https://github.com/yarnpkg/yarn/issues/5540
|
|
||||||
RUN yarn install --network-timeout 300000
|
|
||||||
|
|
||||||
# When running in dev mode, 4GB of memory is required to build & launch the app.
|
# When running in dev mode, 4GB of memory is required to build & launch the app.
|
||||||
# This default setting can be overridden as needed in your shell, via an env file or in docker-compose.
|
# This default setting can be overridden as needed in your shell, via an env file or in docker-compose.
|
||||||
@@ -24,5 +22,5 @@ ENV NODE_OPTIONS="--max_old_space_size=4096"
|
|||||||
# Listen / accept connections from all IP addresses.
|
# Listen / accept connections from all IP addresses.
|
||||||
# NOTE: At this time it is only possible to run Docker container in Production mode
|
# NOTE: At this time it is only possible to run Docker container in Production mode
|
||||||
# if you have a public URL. See https://github.com/DSpace/dspace-angular/issues/1485
|
# if you have a public URL. See https://github.com/DSpace/dspace-angular/issues/1485
|
||||||
ENV NODE_ENV development
|
ENV NODE_ENV=development
|
||||||
CMD yarn serve --host 0.0.0.0
|
CMD npm run serve -- --host 0.0.0.0
|
||||||
|
@@ -4,18 +4,18 @@
|
|||||||
# Test build:
|
# Test build:
|
||||||
# docker build -f Dockerfile.dist -t dspace/dspace-angular:latest-dist .
|
# docker build -f Dockerfile.dist -t dspace/dspace-angular:latest-dist .
|
||||||
|
|
||||||
FROM node:18-alpine AS build
|
FROM docker.io/node:18-alpine AS build
|
||||||
|
|
||||||
# Ensure Python and other build tools are available
|
# Ensure Python and other build tools are available
|
||||||
# These are needed to install some node modules, especially on linux/arm64
|
# These are needed to install some node modules, especially on linux/arm64
|
||||||
RUN apk add --update python3 make g++ && rm -rf /var/cache/apk/*
|
RUN apk add --update python3 make g++ && rm -rf /var/cache/apk/*
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY package.json yarn.lock ./
|
COPY package.json package-lock.json ./
|
||||||
RUN yarn install --network-timeout 300000
|
RUN npm install
|
||||||
|
|
||||||
ADD . /app/
|
ADD . /app/
|
||||||
RUN yarn build:prod
|
RUN npm run build:prod
|
||||||
|
|
||||||
FROM node:18-alpine
|
FROM node:18-alpine
|
||||||
RUN npm install --global pm2
|
RUN npm install --global pm2
|
||||||
@@ -26,6 +26,6 @@ COPY --chown=node:node docker/dspace-ui.json /app/dspace-ui.json
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
USER node
|
USER node
|
||||||
ENV NODE_ENV production
|
ENV NODE_ENV=production
|
||||||
EXPOSE 4000
|
EXPOSE 4000
|
||||||
CMD pm2-runtime start dspace-ui.json --json
|
CMD pm2-runtime start dspace-ui.json --json
|
||||||
|
86
README.md
86
README.md
@@ -35,7 +35,7 @@ https://wiki.lyrasis.org/display/DSDOC7x/Installing+DSpace
|
|||||||
Quick start
|
Quick start
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
**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`**
|
**Ensure you're running [Node](https://nodejs.org) `v16.x` or `v18.x`, [npm](https://www.npmjs.com/) >= `v5.x`**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# clone the repo
|
# clone the repo
|
||||||
@@ -45,10 +45,10 @@ git clone https://github.com/DSpace/dspace-angular.git
|
|||||||
cd dspace-angular
|
cd dspace-angular
|
||||||
|
|
||||||
# install the local dependencies
|
# install the local dependencies
|
||||||
yarn install
|
npm install
|
||||||
|
|
||||||
# start the server
|
# start the server
|
||||||
yarn start
|
npm start
|
||||||
```
|
```
|
||||||
|
|
||||||
Then go to [http://localhost:4000](http://localhost:4000) in your browser
|
Then go to [http://localhost:4000](http://localhost:4000) in your browser
|
||||||
@@ -77,7 +77,7 @@ Table of Contents
|
|||||||
- [Recommended Editors/IDEs](#recommended-editorsides)
|
- [Recommended Editors/IDEs](#recommended-editorsides)
|
||||||
- [Collaborating](#collaborating)
|
- [Collaborating](#collaborating)
|
||||||
- [File Structure](#file-structure)
|
- [File Structure](#file-structure)
|
||||||
- [Managing Dependencies (via yarn)](#managing-dependencies-via-yarn)
|
- [Managing Dependencies (via npm)](#managing-dependencies-via-npm)
|
||||||
- [Frequently asked questions](#frequently-asked-questions)
|
- [Frequently asked questions](#frequently-asked-questions)
|
||||||
- [License](#license)
|
- [License](#license)
|
||||||
|
|
||||||
@@ -89,15 +89,15 @@ You can find more information on the technologies used in this project (Angular.
|
|||||||
Requirements
|
Requirements
|
||||||
------------
|
------------
|
||||||
|
|
||||||
- [Node.js](https://nodejs.org) and [yarn](https://yarnpkg.com)
|
- [Node.js](https://nodejs.org)
|
||||||
- Ensure you're running node `v16.x` or `v18.x` and yarn == `v1.x`
|
- Ensure you're running node `v16.x` or `v18.x`
|
||||||
|
|
||||||
If you have [`nvm`](https://github.com/creationix/nvm#install-script) or [`nvm-windows`](https://github.com/coreybutler/nvm-windows) installed, which is highly recommended, you can run `nvm install --lts && nvm use` to install and start using the latest Node LTS.
|
If you have [`nvm`](https://github.com/creationix/nvm#install-script) or [`nvm-windows`](https://github.com/coreybutler/nvm-windows) installed, which is highly recommended, you can run `nvm install --lts && nvm use` to install and start using the latest Node LTS.
|
||||||
|
|
||||||
Installing
|
Installing
|
||||||
----------
|
----------
|
||||||
|
|
||||||
- `yarn install` to install the local dependencies
|
- `npm install` to install the local dependencies
|
||||||
|
|
||||||
### Configuring
|
### Configuring
|
||||||
|
|
||||||
@@ -202,7 +202,7 @@ import { environment } from '../environment.ts';
|
|||||||
Running the app
|
Running the app
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
After you have installed all dependencies you can now run the app. Run `yarn run start:dev` to start a local server which will watch for changes, rebuild the code, and reload the server for you. You can visit it at `http://localhost:4000`.
|
After you have installed all dependencies you can now run the app. Run `npm run start:dev` to start a local server which will watch for changes, rebuild the code, and reload the server for you. You can visit it at `http://localhost:4000`.
|
||||||
|
|
||||||
### Running in production mode
|
### Running in production mode
|
||||||
|
|
||||||
@@ -211,20 +211,20 @@ When building for production we're using Ahead of Time (AoT) compilation. With A
|
|||||||
To build the app for production and start the server (in one command) run:
|
To build the app for production and start the server (in one command) run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn start
|
npm start
|
||||||
```
|
```
|
||||||
This will run the application in an instance of the Express server, which is included.
|
This will run the application in an instance of the Express server, which is included.
|
||||||
|
|
||||||
If you only want to build for production, without starting, run:
|
If you only want to build for production, without starting, run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn run build:prod
|
npm 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`.
|
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:
|
After building the app for production, it can be started by running:
|
||||||
```bash
|
```bash
|
||||||
yarn run serve:ssr
|
npm run serve:ssr
|
||||||
```
|
```
|
||||||
|
|
||||||
### Running the application with Docker
|
### Running the application with Docker
|
||||||
@@ -238,14 +238,14 @@ Cleaning
|
|||||||
--------
|
--------
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# clean everything, including node_modules. You'll need to run yarn install again afterwards.
|
# clean everything, including node_modules. You'll need to run npm install again afterwards.
|
||||||
yarn run clean
|
npm run clean
|
||||||
|
|
||||||
# clean files generated by the production build (.ngfactory files, css files, etc)
|
# clean files generated by the production build (.ngfactory files, css files, etc)
|
||||||
yarn run clean:prod
|
npm run clean:prod
|
||||||
|
|
||||||
# cleans the distribution directory
|
# cleans the distribution directory
|
||||||
yarn run clean:dist
|
npm run clean:dist
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
@@ -259,9 +259,9 @@ If you would like to contribute by testing a Pull Request (PR), here's how to do
|
|||||||
1. Pull down the branch that the Pull Request was built from. Easy instructions for doing so can be found on the Pull Request itself.
|
1. Pull down the branch that the Pull Request was built from. Easy instructions for doing so can be found on the Pull Request itself.
|
||||||
* Next to the "Merge" button, you'll see a link that says "command line instructions".
|
* Next to the "Merge" button, you'll see a link that says "command line instructions".
|
||||||
* Click it, and follow "Step 1" of those instructions to checkout the pull down the PR branch.
|
* Click it, and follow "Step 1" of those instructions to checkout the pull down the PR branch.
|
||||||
2. `yarn run clean` (This resets your local dependencies to ensure you are up-to-date with this PR)
|
2. `npm run clean` (This resets your local dependencies to ensure you are up-to-date with this PR)
|
||||||
3. `yarn install` (Updates your local dependencies to those in the PR)
|
3. `npm install` (Updates your local dependencies to those in the PR)
|
||||||
4. `yarn start` (Rebuilds the project, and deploys to localhost:4000, by default)
|
4. `npm start` (Rebuilds the project, and deploys to localhost:4000, by default)
|
||||||
5. At this point, the code from the PR will be deployed to http://localhost:4000. Test it out, and ensure that it does what is described in the PR (or fixes the bug described in the ticket linked to the PR).
|
5. At this point, the code from the PR will be deployed to http://localhost:4000. Test it out, and ensure that it does what is described in the PR (or fixes the bug described in the ticket linked to the PR).
|
||||||
|
|
||||||
Once you have tested the Pull Request, please add a comment and/or approval to the PR to let us know whether you found it to be successful (or not). Thanks!
|
Once you have tested the Pull Request, please add a comment and/or approval to the PR to let us know whether you found it to be successful (or not). Thanks!
|
||||||
@@ -271,13 +271,13 @@ Once you have tested the Pull Request, please add a comment and/or approval to t
|
|||||||
|
|
||||||
Unit tests use the [Jasmine test framework](https://jasmine.github.io/), and are run via [Karma](https://karma-runner.github.io/).
|
Unit tests use the [Jasmine test framework](https://jasmine.github.io/), and are run via [Karma](https://karma-runner.github.io/).
|
||||||
|
|
||||||
You can find the Karma configuration file at the same level of this README file:`./karma.conf.js` If you are going to use a remote test environment you need to edit the `./karma.conf.js`. Follow the instructions you will find inside it. To executing tests whenever any file changes you can modify the 'autoWatch' option to 'true' and 'singleRun' option to 'false'. A coverage report is also available at: http://localhost:9876/ after you run: `yarn run coverage`.
|
You can find the Karma configuration file at the same level of this README file:`./karma.conf.js` If you are going to use a remote test environment you need to edit the `./karma.conf.js`. Follow the instructions you will find inside it. To executing tests whenever any file changes you can modify the 'autoWatch' option to 'true' and 'singleRun' option to 'false'. A coverage report is also available at: http://localhost:9876/ after you run: `npm run coverage`.
|
||||||
|
|
||||||
The default browser is Google Chrome.
|
The default browser is Google Chrome.
|
||||||
|
|
||||||
Place your tests in the same location of the application source code files that they test, e.g. ending with `*.component.spec.ts`
|
Place your tests in the same location of the application source code files that they test, e.g. ending with `*.component.spec.ts`
|
||||||
|
|
||||||
and run: `yarn test`
|
and run: `npm test`
|
||||||
|
|
||||||
If you run into odd test errors, see the Angular guide to debugging tests: https://angular.io/guide/test-debugging
|
If you run into odd test errors, see the Angular guide to debugging tests: https://angular.io/guide/test-debugging
|
||||||
|
|
||||||
@@ -330,9 +330,9 @@ 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_.
|
* 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.
|
* 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
|
* 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.
|
* 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 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.
|
||||||
* 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.
|
* 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.
|
* 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.
|
* 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.
|
* 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.
|
||||||
@@ -357,14 +357,14 @@ Some UI specific configuration documentation is also found in the [`./docs`](doc
|
|||||||
|
|
||||||
To build the code documentation we use [TYPEDOC](http://typedoc.org). TYPEDOC is a documentation generator for TypeScript projects. It extracts information from properly formatted comments that can be written within the code files. Follow the instructions [here](http://typedoc.org/guides/doccomments/) to know how to make those comments.
|
To build the code documentation we use [TYPEDOC](http://typedoc.org). TYPEDOC is a documentation generator for TypeScript projects. It extracts information from properly formatted comments that can be written within the code files. Follow the instructions [here](http://typedoc.org/guides/doccomments/) to know how to make those comments.
|
||||||
|
|
||||||
Run:`yarn run docs` to produce the documentation that will be available in the 'doc' folder.
|
Run:`npm run docs` to produce the documentation that will be available in the 'doc' folder.
|
||||||
|
|
||||||
Other commands
|
Other commands
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
There are many more commands in the `scripts` section of `package.json`. Most of these are executed by one of the commands mentioned above.
|
There are many more commands in the `scripts` section of `package.json`. Most of these are executed by one of the commands mentioned above.
|
||||||
|
|
||||||
A command with a name that starts with `pre` or `post` will be executed automatically before or after the script with the matching name. e.g. if you type `yarn run start` the `prestart` script will run first, then the `start` script will trigger.
|
A command with a name that starts with `pre` or `post` will be executed automatically before or after the script with the matching name. e.g. if you type `npm run start` the `prestart` script will run first, then the `start` script will trigger.
|
||||||
|
|
||||||
Recommended Editors/IDEs
|
Recommended Editors/IDEs
|
||||||
------------------------
|
------------------------
|
||||||
@@ -456,6 +456,7 @@ dspace-angular
|
|||||||
├── LICENSES_THIRD_PARTY *
|
├── LICENSES_THIRD_PARTY *
|
||||||
├── nodemon.json * Nodemon (https://nodemon.io/) configuration
|
├── nodemon.json * Nodemon (https://nodemon.io/) configuration
|
||||||
├── package.json * This file describes the npm package for this project, its dependencies, scripts, etc.
|
├── package.json * This file describes the npm package for this project, its dependencies, scripts, etc.
|
||||||
|
├── package-lock.json * npm lockfile (https://docs.npmjs.com/cli/v10/configuring-npm/package-lock-json)
|
||||||
├── postcss.config.js * PostCSS (http://postcss.org/) configuration
|
├── postcss.config.js * PostCSS (http://postcss.org/) configuration
|
||||||
├── README.md * This document
|
├── README.md * This document
|
||||||
├── SECURITY.md *
|
├── SECURITY.md *
|
||||||
@@ -466,30 +467,29 @@ dspace-angular
|
|||||||
├── tsconfig.spec.json * TypeScript config for tests
|
├── tsconfig.spec.json * TypeScript config for tests
|
||||||
├── tsconfig.ts-node.json * TypeScript config for using ts-node directly
|
├── tsconfig.ts-node.json * TypeScript config for using ts-node directly
|
||||||
├── tslint.json * TSLint (https://palantir.github.io/tslint/) configuration
|
├── tslint.json * TSLint (https://palantir.github.io/tslint/) configuration
|
||||||
├── typedoc.json * TYPEDOC configuration
|
└── typedoc.json * TYPEDOC configuration
|
||||||
└── yarn.lock * Yarn lockfile (https://yarnpkg.com/en/docs/yarn-lock)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Managing Dependencies (via yarn)
|
Managing Dependencies (via npm)
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
This project makes use of [`yarn`](https://yarnpkg.com/en/) to ensure that the exact same dependency versions are used every time you install it.
|
This project makes use of [`npm`](https://docs.npmjs.com/about-npm) to ensure that the exact same dependency versions are used every time you install it.
|
||||||
|
|
||||||
* `yarn` creates a [`yarn.lock`](https://yarnpkg.com/en/docs/yarn-lock) to track those versions. That file is updated automatically by whenever dependencies are added/updated/removed via yarn.
|
* `npm` creates a [`package-lock.json`](https://docs.npmjs.com/cli/v10/configuring-npm/package-lock-json) to track those versions. That file is updated automatically by whenever dependencies are added/updated/removed via npm.
|
||||||
* **Adding new dependencies**: To install/add a new dependency (third party library), use [`yarn add`](https://yarnpkg.com/en/docs/cli/add). For example: `yarn add some-lib`.
|
* **Adding new dependencies**: To install/add a new dependency (third party library), use [`npm install`](https://docs.npmjs.com/cli/v10/commands/npm-install). For example: `npm install some-lib`.
|
||||||
* If you are adding a new build tool dependency (to `devDependencies`), use `yarn add some-lib --dev`
|
* If you are adding a new build tool dependency (to `devDependencies`), use `npm install some-lib --save--dev`
|
||||||
* **Upgrading existing dependencies**: To upgrade existing dependencies, you can use [`yarn upgrade`](https://yarnpkg.com/en/docs/cli/upgrade). For example: `yarn upgrade some-lib` or `yarn upgrade some-lib@version`
|
* **Upgrading existing dependencies**: To upgrade existing dependencies, you can use [`npm update`](https://docs.npmjs.com/cli/v10/commands/npm-update). For example: `npm update some-lib` or `npm update some-lib@version`
|
||||||
* **Removing dependencies**: If a dependency is no longer needed, or replaced, use [`yarn remove`](https://yarnpkg.com/en/docs/cli/remove) to remove it.
|
* **Removing dependencies**: If a dependency is no longer needed, or replaced, use [`npm uninstall`](https://docs.npmjs.com/cli/v10/commands/npm-uninstall) to remove it.
|
||||||
|
|
||||||
As you can see above, using `yarn` commandline tools means that you should never need to modify the `package.json` manually. *We recommend always using `yarn` to keep dependencies updated / in sync.*
|
As you can see above, using `npm` commandline tools means that you should never need to modify the `package.json` manually. *We recommend always using `npm` to keep dependencies updated / in sync.*
|
||||||
|
|
||||||
### Adding Typings for libraries
|
### Adding Typings for libraries
|
||||||
|
|
||||||
If the library does not include typings, you can install them using yarn:
|
If the library does not include typings, you can install them using npm:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn add d3
|
npm install d3
|
||||||
yarn add @types/d3 --dev
|
npm install @types/d3 --save-dev
|
||||||
```
|
```
|
||||||
|
|
||||||
If the library doesn't have typings available at `@types/`, you can still use it by manually adding typings for it:
|
If the library doesn't have typings available at `@types/`, you can still use it by manually adding typings for it:
|
||||||
@@ -527,13 +527,13 @@ Frequently asked questions
|
|||||||
- What are the naming conventions for Angular?
|
- What are the naming conventions for Angular?
|
||||||
- See [the official angular style guide](https://angular.io/styleguide)
|
- See [the official angular style guide](https://angular.io/styleguide)
|
||||||
- Why is the size of my app larger in development?
|
- Why is the size of my app larger in development?
|
||||||
- The production build uses a whole host of techniques (ahead-of-time compilation, rollup to remove unreachable code, minification, etc.) to reduce the size, that aren't used during development in the intrest of build speed.
|
- The production build uses a whole host of techniques (ahead-of-time compilation, rollup to remove unreachable code, minification, etc.) to reduce the size, that aren't used during development in the interest of build speed.
|
||||||
- node-pre-gyp ERR in yarn install (Windows)
|
- node-pre-gyp ERR in npm install (Windows)
|
||||||
- install Python x86 version between 2.5 and 3.0 on windows. See [this issue](https://github.com/AngularClass/angular2-webpack-starter/issues/626)
|
- install Python x86 version between 2.5 and 3.0 on windows. See [this issue](https://github.com/AngularClass/angular2-webpack-starter/issues/626)
|
||||||
- How do I handle merge conflicts in yarn.lock?
|
- How do I handle merge conflicts in package-lock.json?
|
||||||
- first check out the yarn.lock file from the branch you're merging in to yours: e.g. `git checkout --theirs yarn.lock`
|
- first check out the package-lock.json file from the branch you're merging in to yours: e.g. `git checkout --theirs package-lock.json`
|
||||||
- now run `yarn install` again. Yarn will create a new lockfile that contains both sets of changes.
|
- now run `npm install` again. NPM will create a new lockfile that contains both sets of changes.
|
||||||
- then run `git add yarn.lock` to stage the lockfile for commit
|
- then run `git add package-lock.json` to stage the lockfile for commit
|
||||||
- and `git commit` to conclude the merge
|
- and `git commit` to conclude the merge
|
||||||
|
|
||||||
Getting Help
|
Getting Help
|
||||||
|
@@ -30,7 +30,6 @@
|
|||||||
"lodash",
|
"lodash",
|
||||||
"jwt-decode",
|
"jwt-decode",
|
||||||
"uuid",
|
"uuid",
|
||||||
"webfontloader",
|
|
||||||
"zone.js"
|
"zone.js"
|
||||||
],
|
],
|
||||||
"outputPath": "dist/browser",
|
"outputPath": "dist/browser",
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
# NOTE: will log all redux actions and transfers in console
|
# NOTE: will log all redux actions and transfers in console
|
||||||
debug: false
|
debug: false
|
||||||
|
|
||||||
# Angular Universal server settings
|
# Angular User Inteface settings
|
||||||
# NOTE: these settings define where Node.js will start your UI application. Therefore, these
|
# 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" settings usually specify a localhost port/URL which is later proxied to a public URL (using Apache or similar)
|
||||||
ui:
|
ui:
|
||||||
@@ -17,12 +17,12 @@ ui:
|
|||||||
# Trust X-FORWARDED-* headers from proxies (default = true)
|
# Trust X-FORWARDED-* headers from proxies (default = true)
|
||||||
useProxies: true
|
useProxies: true
|
||||||
|
|
||||||
universal:
|
# Angular Server Side Rendering (SSR) settings
|
||||||
# Whether to inline "critical" styles into the server-side rendered HTML.
|
ssr:
|
||||||
# Determining which styles are critical is a relatively expensive operation;
|
# Whether to tell Angular to inline "critical" styles into the server-side rendered HTML.
|
||||||
# this option can be disabled to boost server performance at the expense of
|
# Determining which styles are critical is a relatively expensive operation; this option is
|
||||||
# loading smoothness.
|
# disabled (false) by default to boost server performance at the expense of loading smoothness.
|
||||||
inlineCriticalCss: true
|
inlineCriticalCss: false
|
||||||
|
|
||||||
# The REST API server settings
|
# The REST API server settings
|
||||||
# NOTE: these settings define which (publicly available) REST API to use. They are usually
|
# NOTE: these settings define which (publicly available) REST API to use. They are usually
|
||||||
@@ -59,7 +59,7 @@ cache:
|
|||||||
# Set to true to see all cache hits/misses/refreshes in your console logs. Useful for debugging SSR caching issues.
|
# Set to true to see all cache hits/misses/refreshes in your console logs. Useful for debugging SSR caching issues.
|
||||||
debug: false
|
debug: false
|
||||||
# When enabled (i.e. max > 0), known bots will be sent pages from a server side cache specific for bots.
|
# When enabled (i.e. max > 0), known bots will be sent pages from a server side cache specific for bots.
|
||||||
# (Keep in mind, bot detection cannot be guarranteed. It is possible some bots will bypass this cache.)
|
# (Keep in mind, bot detection cannot be guaranteed. It is possible some bots will bypass this cache.)
|
||||||
botCache:
|
botCache:
|
||||||
# Maximum number of pages to cache for known bots. Set to zero (0) to disable server side caching for bots.
|
# Maximum number of pages to cache for known bots. Set to zero (0) to disable server side caching for bots.
|
||||||
# Default is 1000, which means the 1000 most recently accessed public pages will be cached.
|
# Default is 1000, which means the 1000 most recently accessed public pages will be cached.
|
||||||
@@ -503,6 +503,16 @@ notifyMetrics:
|
|||||||
description: 'admin-notify-dashboard.NOTIFY.outgoing.delivered.description'
|
description: 'admin-notify-dashboard.NOTIFY.outgoing.delivered.description'
|
||||||
|
|
||||||
|
|
||||||
|
# Live Region configuration
|
||||||
|
# Live Region as defined by w3c, https://www.w3.org/TR/wai-aria-1.1/#terms:
|
||||||
|
# Live regions are perceivable regions of a web page that are typically updated as a
|
||||||
|
# result of an external event when user focus may be elsewhere.
|
||||||
|
#
|
||||||
|
# The DSpace live region is a component present at the bottom of all pages that is invisible by default, but is useful
|
||||||
|
# for screen readers. Any message pushed to the live region will be announced by the screen reader. These messages
|
||||||
|
# usually contain information about changes on the page that might not be in focus.
|
||||||
|
liveRegion:
|
||||||
|
# The duration after which messages disappear from the live region in milliseconds
|
||||||
|
messageTimeOutDurationMs: 30000
|
||||||
|
# The visibility of the live region. Setting this to true is only useful for debugging purposes.
|
||||||
|
isVisible: false
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { defineConfig } from 'cypress';
|
import { defineConfig } from 'cypress';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
video: true,
|
||||||
videosFolder: 'cypress/videos',
|
videosFolder: 'cypress/videos',
|
||||||
screenshotsFolder: 'cypress/screenshots',
|
screenshotsFolder: 'cypress/screenshots',
|
||||||
fixturesFolder: 'cypress/fixtures',
|
fixturesFolder: 'cypress/fixtures',
|
||||||
@@ -18,6 +19,7 @@ export default defineConfig({
|
|||||||
|
|
||||||
// Admin account used for administrative tests
|
// Admin account used for administrative tests
|
||||||
DSPACE_TEST_ADMIN_USER: 'dspacedemo+admin@gmail.com',
|
DSPACE_TEST_ADMIN_USER: 'dspacedemo+admin@gmail.com',
|
||||||
|
DSPACE_TEST_ADMIN_USER_UUID: '335647b6-8a52-4ecb-a8c1-7ebabb199bda',
|
||||||
DSPACE_TEST_ADMIN_PASSWORD: 'dspace',
|
DSPACE_TEST_ADMIN_PASSWORD: 'dspace',
|
||||||
// Community/collection/publication used for view/edit tests
|
// Community/collection/publication used for view/edit tests
|
||||||
DSPACE_TEST_COMMUNITY: '0958c910-2037-42a9-81c7-dca80e3892b4',
|
DSPACE_TEST_COMMUNITY: '0958c910-2037-42a9-81c7-dca80e3892b4',
|
||||||
@@ -33,6 +35,8 @@ export default defineConfig({
|
|||||||
// Account used to test basic submission process
|
// Account used to test basic submission process
|
||||||
DSPACE_TEST_SUBMIT_USER: 'dspacedemo+submit@gmail.com',
|
DSPACE_TEST_SUBMIT_USER: 'dspacedemo+submit@gmail.com',
|
||||||
DSPACE_TEST_SUBMIT_USER_PASSWORD: 'dspace',
|
DSPACE_TEST_SUBMIT_USER_PASSWORD: 'dspace',
|
||||||
|
// Administrator users group
|
||||||
|
DSPACE_ADMINISTRATOR_GROUP: 'e59f5659-bff9-451e-b28f-439e7bd467e4'
|
||||||
},
|
},
|
||||||
e2e: {
|
e2e: {
|
||||||
// Setup our plugins for e2e tests
|
// Setup our plugins for e2e tests
|
||||||
|
48
cypress/e2e/admin-add-new-modals.cy.ts
Normal file
48
cypress/e2e/admin-add-new-modals.cy.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('Admin Add New Modals', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Must login as an Admin for sidebar to appear
|
||||||
|
cy.visit('/login');
|
||||||
|
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Add new Community modal should pass accessibility tests', () => {
|
||||||
|
// Pin the sidebar open
|
||||||
|
cy.get('[data-test="sidebar-collapse-toggle"]').click();
|
||||||
|
|
||||||
|
// Click on entry of menu
|
||||||
|
cy.get('[data-test="admin-menu-section-new-title"]').click();
|
||||||
|
|
||||||
|
cy.get('a[data-test="menu.section.new_community"]').click();
|
||||||
|
|
||||||
|
// Analyze <ds-create-community-parent-selector> for accessibility
|
||||||
|
testA11y('ds-create-community-parent-selector');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Add new Collection modal should pass accessibility tests', () => {
|
||||||
|
// Pin the sidebar open
|
||||||
|
cy.get('[data-test="sidebar-collapse-toggle"]').click();
|
||||||
|
|
||||||
|
// Click on entry of menu
|
||||||
|
cy.get('[data-test="admin-menu-section-new-title"]').click();
|
||||||
|
|
||||||
|
cy.get('a[data-test="menu.section.new_collection"]').click();
|
||||||
|
|
||||||
|
// Analyze <ds-create-collection-parent-selector> for accessibility
|
||||||
|
testA11y('ds-create-collection-parent-selector');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Add new Item modal should pass accessibility tests', () => {
|
||||||
|
// Pin the sidebar open
|
||||||
|
cy.get('[data-test="sidebar-collapse-toggle"]').click();
|
||||||
|
|
||||||
|
// Click on entry of menu
|
||||||
|
cy.get('[data-test="admin-menu-section-new-title"]').click();
|
||||||
|
|
||||||
|
cy.get('a[data-test="menu.section.new_item"]').click();
|
||||||
|
|
||||||
|
// Analyze <ds-create-item-parent-selector> for accessibility
|
||||||
|
testA11y('ds-create-item-parent-selector');
|
||||||
|
});
|
||||||
|
});
|
16
cypress/e2e/admin-curation-tasks.cy.ts
Normal file
16
cypress/e2e/admin-curation-tasks.cy.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('Admin Curation Tasks', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Must login as an Admin to see the page
|
||||||
|
cy.visit('/admin/curation-tasks');
|
||||||
|
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
// Page must first be visible
|
||||||
|
cy.get('ds-admin-curation-task').should('be.visible');
|
||||||
|
// Analyze <ds-admin-curation-task> for accessibility issues
|
||||||
|
testA11y('ds-admin-curation-task');
|
||||||
|
});
|
||||||
|
});
|
48
cypress/e2e/admin-edit-modals.cy.ts
Normal file
48
cypress/e2e/admin-edit-modals.cy.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('Admin Edit Modals', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Must login as an Admin for sidebar to appear
|
||||||
|
cy.visit('/login');
|
||||||
|
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Edit Community modal should pass accessibility tests', () => {
|
||||||
|
// Pin the sidebar open
|
||||||
|
cy.get('[data-test="sidebar-collapse-toggle"]').click();
|
||||||
|
|
||||||
|
// Click on entry of menu
|
||||||
|
cy.get('#admin-menu-section-edit-title').click();
|
||||||
|
|
||||||
|
cy.get('a[data-test="menu.section.edit_community"]').click();
|
||||||
|
|
||||||
|
// Analyze <ds-edit-community-selector> for accessibility
|
||||||
|
testA11y('ds-edit-community-selector');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Edit Collection modal should pass accessibility tests', () => {
|
||||||
|
// Pin the sidebar open
|
||||||
|
cy.get('[data-test="sidebar-collapse-toggle"]').click();
|
||||||
|
|
||||||
|
// Click on entry of menu
|
||||||
|
cy.get('#admin-menu-section-edit-title').click();
|
||||||
|
|
||||||
|
cy.get('a[data-test="menu.section.edit_collection"]').click();
|
||||||
|
|
||||||
|
// Analyze <ds-edit-collection-selector> for accessibility
|
||||||
|
testA11y('ds-edit-collection-selector');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Edit Item modal should pass accessibility tests', () => {
|
||||||
|
// Pin the sidebar open
|
||||||
|
cy.get('[data-test="sidebar-collapse-toggle"]').click();
|
||||||
|
|
||||||
|
// Click on entry of menu
|
||||||
|
cy.get('#admin-menu-section-edit-title').click();
|
||||||
|
|
||||||
|
cy.get('a[data-test="menu.section.edit_item"]').click();
|
||||||
|
|
||||||
|
// Analyze <ds-edit-item-selector> for accessibility
|
||||||
|
testA11y('ds-edit-item-selector');
|
||||||
|
});
|
||||||
|
});
|
35
cypress/e2e/admin-export-modals.cy.ts
Normal file
35
cypress/e2e/admin-export-modals.cy.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('Admin Export Modals', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Must login as an Admin for sidebar to appear
|
||||||
|
cy.visit('/login');
|
||||||
|
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Export metadata modal should pass accessibility tests', () => {
|
||||||
|
// Pin the sidebar open
|
||||||
|
cy.get('[data-test="sidebar-collapse-toggle"]').click();
|
||||||
|
|
||||||
|
// Click on entry of menu
|
||||||
|
cy.get('#admin-menu-section-export-title').click();
|
||||||
|
|
||||||
|
cy.get('a[data-test="menu.section.export_metadata"]').click();
|
||||||
|
|
||||||
|
// Analyze <ds-export-metadata-selector> for accessibility
|
||||||
|
testA11y('ds-export-metadata-selector');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Export batch modal should pass accessibility tests', () => {
|
||||||
|
// Pin the sidebar open
|
||||||
|
cy.get('[data-test="sidebar-collapse-toggle"]').click();
|
||||||
|
|
||||||
|
// Click on entry of menu
|
||||||
|
cy.get('#admin-menu-section-export-title').click();
|
||||||
|
|
||||||
|
cy.get('a[data-test="menu.section.export_batch"]').click();
|
||||||
|
|
||||||
|
// Analyze <ds-export-batch-selector> for accessibility
|
||||||
|
testA11y('ds-export-batch-selector');
|
||||||
|
});
|
||||||
|
});
|
17
cypress/e2e/admin-notifications-publication-claim-page.cy.ts
Normal file
17
cypress/e2e/admin-notifications-publication-claim-page.cy.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('Admin Notifications Publication Claim Page', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Must login as an Admin to see the page
|
||||||
|
cy.visit('/admin/notifications/publication-claim');
|
||||||
|
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
|
||||||
|
//Page must first be visible
|
||||||
|
cy.get('ds-admin-notifications-publication-claim-page').should('be.visible');
|
||||||
|
// Analyze <ds-admin-notifications-publication-claim-page> for accessibility issues
|
||||||
|
testA11y('ds-admin-notifications-publication-claim-page');
|
||||||
|
});
|
||||||
|
});
|
21
cypress/e2e/admin-search-page.cy.ts
Normal file
21
cypress/e2e/admin-search-page.cy.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('Admin Search Page', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Must login as an Admin to see the page
|
||||||
|
cy.visit('/admin/search');
|
||||||
|
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
//Page must first be visible
|
||||||
|
cy.get('ds-admin-search-page').should('be.visible');
|
||||||
|
// 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('[data-test="filter-toggle"]').click({ multiple: true });
|
||||||
|
// Analyze <ds-admin-search-page> for accessibility issues
|
||||||
|
testA11y('ds-admin-search-page');
|
||||||
|
});
|
||||||
|
});
|
@@ -10,7 +10,7 @@ describe('Admin Sidebar', () => {
|
|||||||
|
|
||||||
it('should be pinnable and pass accessibility tests', () => {
|
it('should be pinnable and pass accessibility tests', () => {
|
||||||
// Pin the sidebar open
|
// Pin the sidebar open
|
||||||
cy.get('#sidebar-collapse-toggle').click();
|
cy.get('[data-test="sidebar-collapse-toggle"]').click();
|
||||||
|
|
||||||
// Click on every expandable section to open all menus
|
// Click on every expandable section to open all menus
|
||||||
cy.get('ds-expandable-admin-sidebar-section').click({ multiple: true });
|
cy.get('ds-expandable-admin-sidebar-section').click({ multiple: true });
|
||||||
|
21
cypress/e2e/admin-workflow-page.cy.ts
Normal file
21
cypress/e2e/admin-workflow-page.cy.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('Admin Workflow Page', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Must login as an Admin to see the page
|
||||||
|
cy.visit('/admin/workflow');
|
||||||
|
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
// Page must first be visible
|
||||||
|
cy.get('ds-admin-workflow-page').should('be.visible');
|
||||||
|
// 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('[data-test="filter-toggle"]').click({ multiple: true });
|
||||||
|
// Analyze <ds-admin-workflow-page> for accessibility issues
|
||||||
|
testA11y('ds-admin-workflow-page');
|
||||||
|
});
|
||||||
|
});
|
16
cypress/e2e/batch-import-page.cy.ts
Normal file
16
cypress/e2e/batch-import-page.cy.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('Batch Import Page', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Must login as an Admin to see processes
|
||||||
|
cy.visit('/admin/batch-import');
|
||||||
|
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
// Batch import form must first be visible
|
||||||
|
cy.get('ds-batch-import-page').should('be.visible');
|
||||||
|
// Analyze <ds-batch-import-page> for accessibility issues
|
||||||
|
testA11y('ds-batch-import-page');
|
||||||
|
});
|
||||||
|
});
|
16
cypress/e2e/bitstreams-format.cy.ts
Normal file
16
cypress/e2e/bitstreams-format.cy.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('Bitstreams Formats', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Must login as an Admin to see the page
|
||||||
|
cy.visit('/admin/registries/bitstream-formats');
|
||||||
|
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
// Page must first be visible
|
||||||
|
cy.get('ds-bitstream-formats').should('be.visible');
|
||||||
|
// Analyze <ds-bitstream-formats> for accessibility issues
|
||||||
|
testA11y('ds-bitstream-formats');
|
||||||
|
});
|
||||||
|
});
|
31
cypress/e2e/bulk-access.cy.ts
Normal file
31
cypress/e2e/bulk-access.cy.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
import { Options } from 'cypress-axe';
|
||||||
|
|
||||||
|
describe('Bulk Access', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Must login as an Admin to see the page
|
||||||
|
cy.visit('/access-control/bulk-access');
|
||||||
|
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
// Page must first be visible
|
||||||
|
cy.get('ds-bulk-access').should('be.visible');
|
||||||
|
// 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('[data-test="filter-toggle"]').click({ multiple: true });
|
||||||
|
// Analyze <ds-bulk-access> for accessibility issues
|
||||||
|
testA11y('ds-bulk-access', {
|
||||||
|
rules: {
|
||||||
|
// All panels are accordians & fail "aria-required-children" and "nested-interactive".
|
||||||
|
// Seem to require updating ng-bootstrap and https://github.com/DSpace/dspace-angular/issues/2216
|
||||||
|
'aria-required-children': { enabled: false },
|
||||||
|
'nested-interactive': { enabled: false },
|
||||||
|
// Card titles fail this test currently
|
||||||
|
'heading-order': { enabled: false },
|
||||||
|
},
|
||||||
|
} as Options);
|
||||||
|
});
|
||||||
|
});
|
16
cypress/e2e/create-eperson.cy.ts
Normal file
16
cypress/e2e/create-eperson.cy.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('Create Eperson', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Must login as an Admin to see the page
|
||||||
|
cy.visit('/access-control/epeople/create');
|
||||||
|
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
// Form must first be visible
|
||||||
|
cy.get('ds-eperson-form').should('be.visible');
|
||||||
|
// Analyze <ds-eperson-form> for accessibility issues
|
||||||
|
testA11y('ds-eperson-form');
|
||||||
|
});
|
||||||
|
});
|
16
cypress/e2e/create-group.cy.ts
Normal file
16
cypress/e2e/create-group.cy.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('Create Group', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Must login as an Admin to see the page
|
||||||
|
cy.visit('/access-control/groups/create');
|
||||||
|
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
// Form must first be visible
|
||||||
|
cy.get('ds-group-form').should('be.visible');
|
||||||
|
// Analyze <ds-group-form> for accessibility issues
|
||||||
|
testA11y('ds-group-form');
|
||||||
|
});
|
||||||
|
});
|
16
cypress/e2e/edit-eperson.cy.ts
Normal file
16
cypress/e2e/edit-eperson.cy.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('Edit Eperson', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Must login as an Admin to see the page
|
||||||
|
cy.visit('/access-control/epeople/'.concat(Cypress.env('DSPACE_TEST_ADMIN_USER_UUID')).concat('/edit'));
|
||||||
|
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
// Form must first be visible
|
||||||
|
cy.get('ds-eperson-form').should('be.visible');
|
||||||
|
// Analyze <ds-eperson-form> for accessibility issues
|
||||||
|
testA11y('ds-eperson-form');
|
||||||
|
});
|
||||||
|
});
|
16
cypress/e2e/edit-group.cy.ts
Normal file
16
cypress/e2e/edit-group.cy.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('Edit Group', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Must login as an Admin to see the page
|
||||||
|
cy.visit('/access-control/groups/'.concat(Cypress.env('DSPACE_ADMINISTRATOR_GROUP')).concat('/edit'));
|
||||||
|
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
// Form must first be visible
|
||||||
|
cy.get('ds-group-form').should('be.visible');
|
||||||
|
// Analyze <ds-group-form> for accessibility issues
|
||||||
|
testA11y('ds-group-form');
|
||||||
|
});
|
||||||
|
});
|
13
cypress/e2e/end-user-agreement.cy.ts
Normal file
13
cypress/e2e/end-user-agreement.cy.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('End User Agreement', () => {
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
cy.visit('/info/end-user-agreement');
|
||||||
|
|
||||||
|
// Page must first be visible
|
||||||
|
cy.get('ds-end-user-agreement').should('be.visible');
|
||||||
|
|
||||||
|
// Analyze <ds-end-user-agreement> for accessibility
|
||||||
|
testA11y('ds-end-user-agreement');
|
||||||
|
});
|
||||||
|
});
|
16
cypress/e2e/epeople-registry.cy.ts
Normal file
16
cypress/e2e/epeople-registry.cy.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('Epeople registry', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Must login as an Admin to see the page
|
||||||
|
cy.visit('/access-control/epeople');
|
||||||
|
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
// Epeople registry page must first be visible
|
||||||
|
cy.get('ds-epeople-registry').should('be.visible');
|
||||||
|
// Analyze <ds-epeople-registry> for accessibility issues
|
||||||
|
testA11y('ds-epeople-registry');
|
||||||
|
});
|
||||||
|
});
|
13
cypress/e2e/feedback.cy.ts
Normal file
13
cypress/e2e/feedback.cy.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('Feedback', () => {
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
cy.visit('/info/feedback');
|
||||||
|
|
||||||
|
// Page must first be visible
|
||||||
|
cy.get('ds-feedback').should('be.visible');
|
||||||
|
|
||||||
|
// Analyze <ds-feedback> for accessibility
|
||||||
|
testA11y('ds-feedback');
|
||||||
|
});
|
||||||
|
});
|
16
cypress/e2e/groups-registry.cy.ts
Normal file
16
cypress/e2e/groups-registry.cy.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('Groups registry', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Must login as an Admin to see the page
|
||||||
|
cy.visit('/access-control/groups');
|
||||||
|
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
// Epeople registry page must first be visible
|
||||||
|
cy.get('ds-groups-registry').should('be.visible');
|
||||||
|
// Analyze <ds-groups-registry> for accessibility issues
|
||||||
|
testA11y('ds-groups-registry');
|
||||||
|
});
|
||||||
|
});
|
@@ -10,4 +10,29 @@ describe('Header', () => {
|
|||||||
// Analyze <ds-header> for accessibility
|
// Analyze <ds-header> for accessibility
|
||||||
testA11y('ds-header');
|
testA11y('ds-header');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should allow for changing language to German (for example)', () => {
|
||||||
|
cy.visit('/');
|
||||||
|
|
||||||
|
// Click the language switcher (globe) in header
|
||||||
|
cy.get('a[data-test="lang-switch"]').click();
|
||||||
|
// Click on the "Deusch" language in dropdown
|
||||||
|
cy.get('#language-menu-list li').contains('Deutsch').click();
|
||||||
|
|
||||||
|
// HTML "lang" attribute should switch to "de"
|
||||||
|
cy.get('html').invoke('attr', 'lang').should('eq', 'de');
|
||||||
|
|
||||||
|
// Login menu should now be in German
|
||||||
|
cy.get('a[data-test="login-menu"]').contains('Anmelden');
|
||||||
|
|
||||||
|
// Change back to English from language switcher
|
||||||
|
cy.get('a[data-test="lang-switch"]').click();
|
||||||
|
cy.get('#language-menu-list li').contains('English').click();
|
||||||
|
|
||||||
|
// HTML "lang" attribute should switch to "en"
|
||||||
|
cy.get('html').invoke('attr', 'lang').should('eq', 'en');
|
||||||
|
|
||||||
|
// Login menu should now be in English
|
||||||
|
cy.get('a[data-test="login-menu"]').contains('Log In');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
62
cypress/e2e/health-page.cy.ts
Normal file
62
cypress/e2e/health-page.cy.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
import { Options } from 'cypress-axe';
|
||||||
|
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Must login as an Admin to see the page
|
||||||
|
cy.visit('/health');
|
||||||
|
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Health Page > Status Tab', () => {
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
cy.intercept('GET', '/server/actuator/health').as('status');
|
||||||
|
cy.wait('@status');
|
||||||
|
|
||||||
|
cy.get('a[data-test="health-page.status-tab"]').click();
|
||||||
|
// Page must first be visible
|
||||||
|
cy.get('ds-health-page').should('be.visible');
|
||||||
|
cy.get('ds-health-panel').should('be.visible');
|
||||||
|
|
||||||
|
// wait for all the ds-health-info-component components to be rendered
|
||||||
|
cy.get('div[role="tabpanel"]').each(($panel: HTMLDivElement) => {
|
||||||
|
cy.wrap($panel).find('ds-health-component').should('be.visible');
|
||||||
|
});
|
||||||
|
// Analyze <ds-health-page> for accessibility issues
|
||||||
|
testA11y('ds-health-page', {
|
||||||
|
rules: {
|
||||||
|
// All panels are accordians & fail "aria-required-children" and "nested-interactive".
|
||||||
|
// Seem to require updating ng-bootstrap and https://github.com/DSpace/dspace-angular/issues/2216
|
||||||
|
'aria-required-children': { enabled: false },
|
||||||
|
'nested-interactive': { enabled: false },
|
||||||
|
},
|
||||||
|
} as Options);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Health Page > Info Tab', () => {
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
cy.intercept('GET', '/server/actuator/info').as('info');
|
||||||
|
cy.wait('@info');
|
||||||
|
|
||||||
|
cy.get('a[data-test="health-page.info-tab"]').click();
|
||||||
|
// Page must first be visible
|
||||||
|
cy.get('ds-health-page').should('be.visible');
|
||||||
|
cy.get('ds-health-info').should('be.visible');
|
||||||
|
|
||||||
|
// wait for all the ds-health-info-component components to be rendered
|
||||||
|
cy.get('div[role="tabpanel"]').each(($panel: HTMLDivElement) => {
|
||||||
|
cy.wrap($panel).find('ds-health-info-component').should('be.visible');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Analyze <ds-health-info> for accessibility issues
|
||||||
|
testA11y('ds-health-info', {
|
||||||
|
rules: {
|
||||||
|
// All panels are accordions & fail "aria-required-children" and "nested-interactive".
|
||||||
|
// Seem to require updating ng-bootstrap and https://github.com/DSpace/dspace-angular/issues/2216
|
||||||
|
'aria-required-children': { enabled: false },
|
||||||
|
'nested-interactive': { enabled: false },
|
||||||
|
},
|
||||||
|
} as Options);
|
||||||
|
});
|
||||||
|
});
|
@@ -17,7 +17,7 @@ describe('Site Statistics Page', () => {
|
|||||||
|
|
||||||
cy.visit('/statistics');
|
cy.visit('/statistics');
|
||||||
|
|
||||||
// <ds-site-statistics-page> tag must be visable
|
// <ds-site-statistics-page> tag must be visible
|
||||||
cy.get('ds-site-statistics-page').should('be.visible');
|
cy.get('ds-site-statistics-page').should('be.visible');
|
||||||
|
|
||||||
// Verify / wait until "Total Visits" table's *last* label is non-empty
|
// Verify / wait until "Total Visits" table's *last* label is non-empty
|
||||||
|
@@ -15,6 +15,9 @@ describe('Edit Item > Edit Metadata tab', () => {
|
|||||||
it('should pass accessibility tests', () => {
|
it('should pass accessibility tests', () => {
|
||||||
cy.get('a[data-test="metadata"]').click();
|
cy.get('a[data-test="metadata"]').click();
|
||||||
|
|
||||||
|
// Our selected tab should be active
|
||||||
|
cy.get('a[data-test="metadata"]').should('have.class', 'active');
|
||||||
|
|
||||||
// <ds-edit-item-page> tag must be loaded
|
// <ds-edit-item-page> tag must be loaded
|
||||||
cy.get('ds-edit-item-page').should('be.visible');
|
cy.get('ds-edit-item-page').should('be.visible');
|
||||||
|
|
||||||
@@ -33,6 +36,9 @@ describe('Edit Item > Status tab', () => {
|
|||||||
it('should pass accessibility tests', () => {
|
it('should pass accessibility tests', () => {
|
||||||
cy.get('a[data-test="status"]').click();
|
cy.get('a[data-test="status"]').click();
|
||||||
|
|
||||||
|
// Our selected tab should be active
|
||||||
|
cy.get('a[data-test="status"]').should('have.class', 'active');
|
||||||
|
|
||||||
// <ds-item-status> tag must be loaded
|
// <ds-item-status> tag must be loaded
|
||||||
cy.get('ds-item-status').should('be.visible');
|
cy.get('ds-item-status').should('be.visible');
|
||||||
|
|
||||||
@@ -46,6 +52,9 @@ describe('Edit Item > Bitstreams tab', () => {
|
|||||||
it('should pass accessibility tests', () => {
|
it('should pass accessibility tests', () => {
|
||||||
cy.get('a[data-test="bitstreams"]').click();
|
cy.get('a[data-test="bitstreams"]').click();
|
||||||
|
|
||||||
|
// Our selected tab should be active
|
||||||
|
cy.get('a[data-test="bitstreams"]').should('have.class', 'active');
|
||||||
|
|
||||||
// <ds-item-bitstreams> tag must be loaded
|
// <ds-item-bitstreams> tag must be loaded
|
||||||
cy.get('ds-item-bitstreams').should('be.visible');
|
cy.get('ds-item-bitstreams').should('be.visible');
|
||||||
|
|
||||||
@@ -70,6 +79,9 @@ describe('Edit Item > Curate tab', () => {
|
|||||||
it('should pass accessibility tests', () => {
|
it('should pass accessibility tests', () => {
|
||||||
cy.get('a[data-test="curate"]').click();
|
cy.get('a[data-test="curate"]').click();
|
||||||
|
|
||||||
|
// Our selected tab should be active
|
||||||
|
cy.get('a[data-test="curate"]').should('have.class', 'active');
|
||||||
|
|
||||||
// <ds-item-curate> tag must be loaded
|
// <ds-item-curate> tag must be loaded
|
||||||
cy.get('ds-item-curate').should('be.visible');
|
cy.get('ds-item-curate').should('be.visible');
|
||||||
|
|
||||||
@@ -83,6 +95,9 @@ describe('Edit Item > Relationships tab', () => {
|
|||||||
it('should pass accessibility tests', () => {
|
it('should pass accessibility tests', () => {
|
||||||
cy.get('a[data-test="relationships"]').click();
|
cy.get('a[data-test="relationships"]').click();
|
||||||
|
|
||||||
|
// Our selected tab should be active
|
||||||
|
cy.get('a[data-test="relationships"]').should('have.class', 'active');
|
||||||
|
|
||||||
// <ds-item-relationships> tag must be loaded
|
// <ds-item-relationships> tag must be loaded
|
||||||
cy.get('ds-item-relationships').should('be.visible');
|
cy.get('ds-item-relationships').should('be.visible');
|
||||||
|
|
||||||
@@ -96,6 +111,9 @@ describe('Edit Item > Version History tab', () => {
|
|||||||
it('should pass accessibility tests', () => {
|
it('should pass accessibility tests', () => {
|
||||||
cy.get('a[data-test="versionhistory"]').click();
|
cy.get('a[data-test="versionhistory"]').click();
|
||||||
|
|
||||||
|
// Our selected tab should be active
|
||||||
|
cy.get('a[data-test="versionhistory"]').should('have.class', 'active');
|
||||||
|
|
||||||
// <ds-item-version-history> tag must be loaded
|
// <ds-item-version-history> tag must be loaded
|
||||||
cy.get('ds-item-version-history').should('be.visible');
|
cy.get('ds-item-version-history').should('be.visible');
|
||||||
|
|
||||||
@@ -109,6 +127,9 @@ describe('Edit Item > Access Control tab', () => {
|
|||||||
it('should pass accessibility tests', () => {
|
it('should pass accessibility tests', () => {
|
||||||
cy.get('a[data-test="access-control"]').click();
|
cy.get('a[data-test="access-control"]').click();
|
||||||
|
|
||||||
|
// Our selected tab should be active
|
||||||
|
cy.get('a[data-test="access-control"]').should('have.class', 'active');
|
||||||
|
|
||||||
// <ds-item-access-control> tag must be loaded
|
// <ds-item-access-control> tag must be loaded
|
||||||
cy.get('ds-item-access-control').should('be.visible');
|
cy.get('ds-item-access-control').should('be.visible');
|
||||||
|
|
||||||
@@ -122,6 +143,9 @@ describe('Edit Item > Collection Mapper tab', () => {
|
|||||||
it('should pass accessibility tests', () => {
|
it('should pass accessibility tests', () => {
|
||||||
cy.get('a[data-test="mapper"]').click();
|
cy.get('a[data-test="mapper"]').click();
|
||||||
|
|
||||||
|
// Our selected tab should be active
|
||||||
|
cy.get('a[data-test="mapper"]').should('have.class', 'active');
|
||||||
|
|
||||||
// <ds-item-collection-mapper> tag must be loaded
|
// <ds-item-collection-mapper> tag must be loaded
|
||||||
cy.get('ds-item-collection-mapper').should('be.visible');
|
cy.get('ds-item-collection-mapper').should('be.visible');
|
||||||
|
|
||||||
|
@@ -3,31 +3,31 @@ import { testA11y } from 'cypress/support/utils';
|
|||||||
const page = {
|
const page = {
|
||||||
openLoginMenu() {
|
openLoginMenu() {
|
||||||
// Click the "Log In" dropdown menu in header
|
// Click the "Log In" dropdown menu in header
|
||||||
cy.get('ds-header [data-test="login-menu"]').click();
|
cy.get('[data-test="login-menu"]').click();
|
||||||
},
|
},
|
||||||
openUserMenu() {
|
openUserMenu() {
|
||||||
// Once logged in, click the User menu in header
|
// Once logged in, click the User menu in header
|
||||||
cy.get('ds-header [data-test="user-menu"]').click();
|
cy.get('[data-test="user-menu"]').click();
|
||||||
},
|
},
|
||||||
submitLoginAndPasswordByPressingButton(email, password) {
|
submitLoginAndPasswordByPressingButton(email, password) {
|
||||||
// Enter email
|
// Enter email
|
||||||
cy.get('ds-header [data-test="email"]').type(email);
|
cy.get('[data-test="email"]').type(email);
|
||||||
// Enter password
|
// Enter password
|
||||||
cy.get('ds-header [data-test="password"]').type(password);
|
cy.get('[data-test="password"]').type(password);
|
||||||
// Click login button
|
// Click login button
|
||||||
cy.get('ds-header [data-test="login-button"]').click();
|
cy.get('[data-test="login-button"]').click();
|
||||||
},
|
},
|
||||||
submitLoginAndPasswordByPressingEnter(email, password) {
|
submitLoginAndPasswordByPressingEnter(email, password) {
|
||||||
// In opened Login modal, fill out email & password, then click Enter
|
// In opened Login modal, fill out email & password, then click Enter
|
||||||
cy.get('ds-header [data-test="email"]').type(email);
|
cy.get('[data-test="email"]').type(email);
|
||||||
cy.get('ds-header [data-test="password"]').type(password);
|
cy.get('[data-test="password"]').type(password);
|
||||||
cy.get('ds-header [data-test="password"]').type('{enter}');
|
cy.get('[data-test="password"]').type('{enter}');
|
||||||
},
|
},
|
||||||
submitLogoutByPressingButton() {
|
submitLogoutByPressingButton() {
|
||||||
// This is the POST command that will actually log us out
|
// This is the POST command that will actually log us out
|
||||||
cy.intercept('POST', '/server/api/authn/logout').as('logout');
|
cy.intercept('POST', '/server/api/authn/logout').as('logout');
|
||||||
// Click logout button
|
// Click logout button
|
||||||
cy.get('ds-header [data-test="logout-button"]').click();
|
cy.get('[data-test="logout-button"]').click();
|
||||||
// Wait until above POST command responds before continuing
|
// Wait until above POST command responds before continuing
|
||||||
// (This ensures next action waits until logout completes)
|
// (This ensures next action waits until logout completes)
|
||||||
cy.wait('@logout');
|
cy.wait('@logout');
|
||||||
@@ -67,7 +67,7 @@ describe('Login Modal', () => {
|
|||||||
|
|
||||||
// Login, and the <ds-log-in> tag should no longer exist
|
// Login, and the <ds-log-in> tag should no longer exist
|
||||||
page.submitLoginAndPasswordByPressingEnter(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
page.submitLoginAndPasswordByPressingEnter(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||||
cy.get('.form-login').should('not.exist');
|
cy.get('ds-log-in').should('not.exist');
|
||||||
|
|
||||||
// Verify we are still on homepage
|
// Verify we are still on homepage
|
||||||
cy.url().should('include', '/home');
|
cy.url().should('include', '/home');
|
||||||
@@ -142,7 +142,7 @@ describe('Login Modal', () => {
|
|||||||
page.submitLoginAndPasswordByPressingButton(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
page.submitLoginAndPasswordByPressingButton(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||||
cy.get('ds-log-in').should('not.exist');
|
cy.get('ds-log-in').should('not.exist');
|
||||||
|
|
||||||
// Open user menu, verify user menu accesibility
|
// Open user menu, verify user menu accessibility
|
||||||
page.openUserMenu();
|
page.openUserMenu();
|
||||||
cy.get('ds-user-menu').should('be.visible');
|
cy.get('ds-user-menu').should('be.visible');
|
||||||
testA11y('ds-user-menu');
|
testA11y('ds-user-menu');
|
||||||
|
16
cypress/e2e/metadata-import-page.cy.ts
Normal file
16
cypress/e2e/metadata-import-page.cy.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('Metadata Import Page', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Must login as an Admin to see the page
|
||||||
|
cy.visit('/admin/metadata-import');
|
||||||
|
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
// Metadata import form must first be visible
|
||||||
|
cy.get('ds-metadata-import-page').should('be.visible');
|
||||||
|
// Analyze <ds-metadata-import-page> for accessibility issues
|
||||||
|
testA11y('ds-metadata-import-page');
|
||||||
|
});
|
||||||
|
});
|
16
cypress/e2e/metadata-registry.cy.ts
Normal file
16
cypress/e2e/metadata-registry.cy.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('Metadata Registry', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Must login as an Admin to see the page
|
||||||
|
cy.visit('/admin/registries/metadata');
|
||||||
|
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
// Page must first be visible
|
||||||
|
cy.get('ds-metadata-registry').should('be.visible');
|
||||||
|
// Analyze <ds-metadata-registry> for accessibility issues
|
||||||
|
testA11y('ds-metadata-registry');
|
||||||
|
});
|
||||||
|
});
|
16
cypress/e2e/metadata-schema.cy.ts
Normal file
16
cypress/e2e/metadata-schema.cy.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('Metadata Schema', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Must login as an Admin to see the page
|
||||||
|
cy.visit('/admin/registries/metadata/dc');
|
||||||
|
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
// Page must first be visible
|
||||||
|
cy.get('ds-metadata-schema').should('be.visible');
|
||||||
|
// Analyze <ds-metadata-schema> for accessibility issues
|
||||||
|
testA11y('ds-metadata-schema');
|
||||||
|
});
|
||||||
|
});
|
16
cypress/e2e/new-process.cy.ts
Normal file
16
cypress/e2e/new-process.cy.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('New Process', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Must login as an Admin to see the page
|
||||||
|
cy.visit('/processes/new');
|
||||||
|
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
// Process form must first be visible
|
||||||
|
cy.get('ds-new-process').should('be.visible');
|
||||||
|
// Analyze <ds-new-process> for accessibility issues
|
||||||
|
testA11y('ds-new-process');
|
||||||
|
});
|
||||||
|
});
|
@@ -1,7 +1,7 @@
|
|||||||
import { testA11y } from 'cypress/support/utils';
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
describe('PageNotFound', () => {
|
describe('PageNotFound', () => {
|
||||||
it('should contain element ds-pagenotfound when navigating to page that doesnt exist', () => {
|
it('should contain element ds-pagenotfound when navigating to page that does not exist', () => {
|
||||||
// request an invalid page (UUIDs at root path aren't valid)
|
// request an invalid page (UUIDs at root path aren't valid)
|
||||||
cy.visit('/e9019a69-d4f1-4773-b6a3-bd362caa46f2', { failOnStatusCode: false });
|
cy.visit('/e9019a69-d4f1-4773-b6a3-bd362caa46f2', { failOnStatusCode: false });
|
||||||
cy.get('ds-pagenotfound').should('be.visible');
|
cy.get('ds-pagenotfound').should('be.visible');
|
||||||
|
13
cypress/e2e/privacy.cy.ts
Normal file
13
cypress/e2e/privacy.cy.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('Privacy', () => {
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
cy.visit('/info/privacy');
|
||||||
|
|
||||||
|
// Page must first be visible
|
||||||
|
cy.get('ds-privacy').should('be.visible');
|
||||||
|
|
||||||
|
// Analyze <ds-privacy> for accessibility
|
||||||
|
testA11y('ds-privacy');
|
||||||
|
});
|
||||||
|
});
|
17
cypress/e2e/processes-overview.cy.ts
Normal file
17
cypress/e2e/processes-overview.cy.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('Processes Overview', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Must login as an Admin to see the page
|
||||||
|
cy.visit('/processes');
|
||||||
|
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
|
||||||
|
// Process overview must first be visible
|
||||||
|
cy.get('ds-process-overview').should('be.visible');
|
||||||
|
// Analyze <ds-process-overview> for accessibility issues
|
||||||
|
testA11y('ds-process-overview');
|
||||||
|
});
|
||||||
|
});
|
16
cypress/e2e/profile-page.cy.ts
Normal file
16
cypress/e2e/profile-page.cy.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('Profile page', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Must login as an Admin to see the page
|
||||||
|
cy.visit('/profile');
|
||||||
|
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
// Process form must first be visible
|
||||||
|
cy.get('ds-profile-page').should('be.visible');
|
||||||
|
// Analyze <ds-profile-page> for accessibility issues
|
||||||
|
testA11y('ds-profile-page');
|
||||||
|
});
|
||||||
|
});
|
16
cypress/e2e/quality-assurance-source-page.cy.ts
Normal file
16
cypress/e2e/quality-assurance-source-page.cy.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('Quality Assurance Source Page', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Must login as an Admin to see the page
|
||||||
|
cy.visit('/notifications/quality-assurance');
|
||||||
|
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
// Source page must first be visible
|
||||||
|
cy.get('ds-quality-assurance-source-page-component').should('be.visible');
|
||||||
|
// Analyze <ds-quality-assurance-source-page-component> for accessibility issues
|
||||||
|
testA11y('ds-quality-assurance-source-page-component');
|
||||||
|
});
|
||||||
|
});
|
@@ -34,7 +34,7 @@ describe('New Submission page', () => {
|
|||||||
// Author & Subject fields have invalid "aria-multiline" attrs.
|
// Author & Subject fields have invalid "aria-multiline" attrs.
|
||||||
// See https://github.com/DSpace/dspace-angular/issues/1272
|
// See https://github.com/DSpace/dspace-angular/issues/1272
|
||||||
'aria-allowed-attr': { enabled: false },
|
'aria-allowed-attr': { enabled: false },
|
||||||
// All panels are accordians & fail "aria-required-children" and "nested-interactive".
|
// All panels are accordions & fail "aria-required-children" and "nested-interactive".
|
||||||
// Seem to require updating ng-bootstrap and https://github.com/DSpace/dspace-angular/issues/2216
|
// Seem to require updating ng-bootstrap and https://github.com/DSpace/dspace-angular/issues/2216
|
||||||
'aria-required-children': { enabled: false },
|
'aria-required-children': { enabled: false },
|
||||||
'nested-interactive': { enabled: false },
|
'nested-interactive': { enabled: false },
|
||||||
@@ -192,7 +192,7 @@ describe('New Submission page', () => {
|
|||||||
testA11y('ds-submission-edit',
|
testA11y('ds-submission-edit',
|
||||||
{
|
{
|
||||||
rules: {
|
rules: {
|
||||||
// All panels are accordians & fail "aria-required-children" and "nested-interactive".
|
// All panels are accordions & fail "aria-required-children" and "nested-interactive".
|
||||||
// Seem to require updating ng-bootstrap and https://github.com/DSpace/dspace-angular/issues/2216
|
// Seem to require updating ng-bootstrap and https://github.com/DSpace/dspace-angular/issues/2216
|
||||||
'aria-required-children': { enabled: false },
|
'aria-required-children': { enabled: false },
|
||||||
'nested-interactive': { enabled: false },
|
'nested-interactive': { enabled: false },
|
||||||
|
16
cypress/e2e/system-wide-alert.cy.ts
Normal file
16
cypress/e2e/system-wide-alert.cy.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('System Wide Alert', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Must login as an Admin to see the page
|
||||||
|
cy.visit('/admin/system-wide-alert');
|
||||||
|
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
// Page must first be visible
|
||||||
|
cy.get('ds-system-wide-alert-form').should('be.visible');
|
||||||
|
// Analyze <ds-system-wide-alert-form> for accessibility issues
|
||||||
|
testA11y('ds-system-wide-alert-form');
|
||||||
|
});
|
||||||
|
});
|
@@ -101,11 +101,11 @@ Cypress.Commands.add('login', login);
|
|||||||
*/
|
*/
|
||||||
function loginViaForm(email: string, password: string): void {
|
function loginViaForm(email: string, password: string): void {
|
||||||
// Enter email
|
// Enter email
|
||||||
cy.get('ds-log-in [data-test="email"]').type(email);
|
cy.get('[data-test="email"]').type(email);
|
||||||
// Enter password
|
// Enter password
|
||||||
cy.get('ds-log-in [data-test="password"]').type(password);
|
cy.get('[data-test="password"]').type(password);
|
||||||
// Click login button
|
// Click login button
|
||||||
cy.get('ds-log-in [data-test="login-button"]').click();
|
cy.get('[data-test="login-button"]').click();
|
||||||
}
|
}
|
||||||
// Add as a Cypress command (i.e. assign to 'cy.loginViaForm')
|
// Add as a Cypress command (i.e. assign to 'cy.loginViaForm')
|
||||||
Cypress.Commands.add('loginViaForm', loginViaForm);
|
Cypress.Commands.add('loginViaForm', loginViaForm);
|
||||||
|
@@ -54,9 +54,9 @@ before(() => {
|
|||||||
|
|
||||||
// Runs once before the first test in each "block"
|
// Runs once before the first test in each "block"
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// Pre-agree to all Klaro cookies by setting the klaro-anonymous cookie
|
// Pre-agree to all Orejime cookies by setting the orejime-anonymous cookie
|
||||||
// This just ensures it doesn't get in the way of matching other objects in the page.
|
// 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}');
|
cy.setCookie('orejime-anonymous', '{"authentication":true,"preferences":true,"acknowledgement":true,"google-analytics":true}');
|
||||||
|
|
||||||
// Remove any CSRF cookies saved from prior tests
|
// Remove any CSRF cookies saved from prior tests
|
||||||
cy.clearCookie(DSPACE_XSRF_COOKIE);
|
cy.clearCookie(DSPACE_XSRF_COOKIE);
|
||||||
|
@@ -21,7 +21,7 @@ networks:
|
|||||||
external: true
|
external: true
|
||||||
services:
|
services:
|
||||||
dspace-cli:
|
dspace-cli:
|
||||||
image: "${DOCKER_OWNER:-dspace}/dspace-cli:${DSPACE_VER:-latest}"
|
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-cli:${DSPACE_VER:-latest}"
|
||||||
container_name: dspace-cli
|
container_name: dspace-cli
|
||||||
environment:
|
environment:
|
||||||
# Below syntax may look odd, but it is how to override dspace.cfg settings via env variables.
|
# Below syntax may look odd, but it is how to override dspace.cfg settings via env variables.
|
||||||
|
@@ -14,7 +14,7 @@
|
|||||||
# # Therefore, it should be kept in sync with that file
|
# # Therefore, it should be kept in sync with that file
|
||||||
services:
|
services:
|
||||||
dspacedb:
|
dspacedb:
|
||||||
image: "${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-latest}-loadsql"
|
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-latest}-loadsql"
|
||||||
environment:
|
environment:
|
||||||
# This LOADSQL should be kept in sync with the URL in DSpace/DSpace
|
# 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
|
# This SQL is available from https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data
|
||||||
|
@@ -33,7 +33,7 @@ services:
|
|||||||
# This allows us to generate statistics in e2e tests so that statistics pages can be tested thoroughly.
|
# This allows us to generate statistics in e2e tests so that statistics pages can be tested thoroughly.
|
||||||
solr__D__statistics__P__autoCommit: 'false'
|
solr__D__statistics__P__autoCommit: 'false'
|
||||||
LOGGING_CONFIG: /dspace/config/log4j2-container.xml
|
LOGGING_CONFIG: /dspace/config/log4j2-container.xml
|
||||||
image: "${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-latest-test}"
|
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-latest-test}"
|
||||||
depends_on:
|
depends_on:
|
||||||
- dspacedb
|
- dspacedb
|
||||||
networks:
|
networks:
|
||||||
@@ -60,7 +60,7 @@ services:
|
|||||||
# NOTE: This is customized to use our loadsql image, so that we are using a database with existing test data
|
# NOTE: This is customized to use our loadsql image, so that we are using a database with existing test data
|
||||||
dspacedb:
|
dspacedb:
|
||||||
container_name: dspacedb
|
container_name: dspacedb
|
||||||
image: "${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-latest}-loadsql"
|
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-latest}-loadsql"
|
||||||
environment:
|
environment:
|
||||||
# This LOADSQL should be kept in sync with the LOADSQL in
|
# 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
|
# https://github.com/DSpace/DSpace/blob/main/dspace/src/main/docker-compose/db.entities.yml
|
||||||
@@ -81,7 +81,7 @@ services:
|
|||||||
# DSpace Solr container
|
# DSpace Solr container
|
||||||
dspacesolr:
|
dspacesolr:
|
||||||
container_name: dspacesolr
|
container_name: dspacesolr
|
||||||
image: "${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-latest}"
|
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-latest}"
|
||||||
networks:
|
networks:
|
||||||
- dspacenet
|
- dspacenet
|
||||||
ports:
|
ports:
|
||||||
|
@@ -26,7 +26,7 @@ services:
|
|||||||
DSPACE_REST_HOST: sandbox.dspace.org
|
DSPACE_REST_HOST: sandbox.dspace.org
|
||||||
DSPACE_REST_PORT: 443
|
DSPACE_REST_PORT: 443
|
||||||
DSPACE_REST_NAMESPACE: /server
|
DSPACE_REST_NAMESPACE: /server
|
||||||
image: dspace/dspace-angular:${DSPACE_VER:-latest}-dist
|
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-angular:${DSPACE_VER:-latest}-dist"
|
||||||
build:
|
build:
|
||||||
context: ..
|
context: ..
|
||||||
dockerfile: Dockerfile.dist
|
dockerfile: Dockerfile.dist
|
||||||
|
@@ -40,7 +40,7 @@ services:
|
|||||||
# from the host machine. This IP range MUST correspond to the 'dspacenet' subnet defined above.
|
# from the host machine. This IP range MUST correspond to the 'dspacenet' subnet defined above.
|
||||||
proxies__P__trusted__P__ipranges: '172.23.0'
|
proxies__P__trusted__P__ipranges: '172.23.0'
|
||||||
LOGGING_CONFIG: /dspace/config/log4j2-container.xml
|
LOGGING_CONFIG: /dspace/config/log4j2-container.xml
|
||||||
image: "${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-latest-test}"
|
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-latest-test}"
|
||||||
depends_on:
|
depends_on:
|
||||||
- dspacedb
|
- dspacedb
|
||||||
networks:
|
networks:
|
||||||
@@ -68,7 +68,7 @@ services:
|
|||||||
dspacedb:
|
dspacedb:
|
||||||
container_name: dspacedb
|
container_name: dspacedb
|
||||||
# Uses a custom Postgres image with pgcrypto installed
|
# Uses a custom Postgres image with pgcrypto installed
|
||||||
image: "${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-latest}"
|
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-latest}"
|
||||||
environment:
|
environment:
|
||||||
PGDATA: /pgdata
|
PGDATA: /pgdata
|
||||||
POSTGRES_PASSWORD: dspace
|
POSTGRES_PASSWORD: dspace
|
||||||
@@ -85,7 +85,7 @@ services:
|
|||||||
# DSpace Solr container
|
# DSpace Solr container
|
||||||
dspacesolr:
|
dspacesolr:
|
||||||
container_name: dspacesolr
|
container_name: dspacesolr
|
||||||
image: "${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-latest}"
|
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-latest}"
|
||||||
networks:
|
networks:
|
||||||
- dspacenet
|
- dspacenet
|
||||||
ports:
|
ports:
|
||||||
|
@@ -23,7 +23,7 @@ services:
|
|||||||
DSPACE_REST_HOST: localhost
|
DSPACE_REST_HOST: localhost
|
||||||
DSPACE_REST_PORT: 8080
|
DSPACE_REST_PORT: 8080
|
||||||
DSPACE_REST_NAMESPACE: /server
|
DSPACE_REST_NAMESPACE: /server
|
||||||
image: dspace/dspace-angular:${DSPACE_VER:-latest}
|
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-angular:${DSPACE_VER:-latest}"
|
||||||
build:
|
build:
|
||||||
context: ..
|
context: ..
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
|
@@ -15,7 +15,7 @@ DSPACE_APP_CONFIG_PATH=/usr/local/dspace/config/config.yml
|
|||||||
Configuration options can be overridden by setting environment variables.
|
Configuration options can be overridden by setting environment variables.
|
||||||
|
|
||||||
## Nodejs server
|
## Nodejs server
|
||||||
When you start dspace-angular on node, it spins up an http server on which it listens for incoming connections. You can define the ip address and port the server should bind itsself to, and if ssl should be enabled not. By default it listens on `localhost:4000`. If you want it to listen on all your network connections, configure it to bind itself to `0.0.0.0`.
|
When you start dspace-angular on node, it spins up an http server on which it listens for incoming connections. You can define the ip address and port the server should bind itself to, and if ssl should be enabled not. By default it listens on `localhost:4000`. If you want it to listen on all your network connections, configure it to bind itself to `0.0.0.0`.
|
||||||
|
|
||||||
To change this configuration, change the options `ui.host`, `ui.port` and `ui.ssl` in the appropriate configuration file (see above):
|
To change this configuration, change the options `ui.host`, `ui.port` and `ui.ssl` in the appropriate configuration file (see above):
|
||||||
|
|
||||||
|
@@ -7,10 +7,8 @@
|
|||||||
*/
|
*/
|
||||||
import { TmplAstElement } from '@angular-eslint/bundled-angular-compiler';
|
import { TmplAstElement } from '@angular-eslint/bundled-angular-compiler';
|
||||||
import { TemplateParserServices } from '@angular-eslint/utils';
|
import { TemplateParserServices } from '@angular-eslint/utils';
|
||||||
import {
|
import { ESLintUtils } from '@typescript-eslint/utils';
|
||||||
ESLintUtils,
|
import { RuleContext } from '@typescript-eslint/utils/ts-eslint';
|
||||||
TSESLint,
|
|
||||||
} from '@typescript-eslint/utils';
|
|
||||||
|
|
||||||
import { fixture } from '../../../test/fixture';
|
import { fixture } from '../../../test/fixture';
|
||||||
import {
|
import {
|
||||||
@@ -52,7 +50,7 @@ The only exception to this rule are unit tests, where we may want to use the bas
|
|||||||
|
|
||||||
export const rule = ESLintUtils.RuleCreator.withoutDocs({
|
export const rule = ESLintUtils.RuleCreator.withoutDocs({
|
||||||
...info,
|
...info,
|
||||||
create(context: TSESLint.RuleContext<Message, unknown[]>) {
|
create(context: RuleContext<Message, unknown[]>) {
|
||||||
if (getFilename(context).includes('.spec.ts')) {
|
if (getFilename(context).includes('.spec.ts')) {
|
||||||
// skip inline templates in unit tests
|
// skip inline templates in unit tests
|
||||||
return {};
|
return {};
|
||||||
|
@@ -7,9 +7,9 @@
|
|||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
ESLintUtils,
|
ESLintUtils,
|
||||||
TSESLint,
|
|
||||||
TSESTree,
|
TSESTree,
|
||||||
} from '@typescript-eslint/utils';
|
} from '@typescript-eslint/utils';
|
||||||
|
import { RuleContext } from '@typescript-eslint/utils/ts-eslint';
|
||||||
|
|
||||||
import { fixture } from '../../../test/fixture';
|
import { fixture } from '../../../test/fixture';
|
||||||
import {
|
import {
|
||||||
@@ -57,7 +57,7 @@ export const info = {
|
|||||||
|
|
||||||
export const rule = ESLintUtils.RuleCreator.withoutDocs({
|
export const rule = ESLintUtils.RuleCreator.withoutDocs({
|
||||||
...info,
|
...info,
|
||||||
create(context: TSESLint.RuleContext<Message, unknown[]>) {
|
create(context: RuleContext<Message, unknown[]>) {
|
||||||
const filename = getFilename(context);
|
const filename = getFilename(context);
|
||||||
|
|
||||||
if (filename.endsWith('.spec.ts')) {
|
if (filename.endsWith('.spec.ts')) {
|
||||||
|
@@ -7,9 +7,9 @@
|
|||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
ESLintUtils,
|
ESLintUtils,
|
||||||
TSESLint,
|
|
||||||
TSESTree,
|
TSESTree,
|
||||||
} from '@typescript-eslint/utils';
|
} from '@typescript-eslint/utils';
|
||||||
|
import { RuleContext } from '@typescript-eslint/utils/ts-eslint';
|
||||||
|
|
||||||
import { fixture } from '../../../test/fixture';
|
import { fixture } from '../../../test/fixture';
|
||||||
import { getComponentSelectorNode } from '../../util/angular';
|
import { getComponentSelectorNode } from '../../util/angular';
|
||||||
@@ -58,7 +58,7 @@ Unit tests are exempt from this rule, because they may redefine components using
|
|||||||
|
|
||||||
export const rule = ESLintUtils.RuleCreator.withoutDocs({
|
export const rule = ESLintUtils.RuleCreator.withoutDocs({
|
||||||
...info,
|
...info,
|
||||||
create(context: TSESLint.RuleContext<Message, unknown[]>) {
|
create(context: RuleContext<Message, unknown[]>) {
|
||||||
const filename = getFilename(context);
|
const filename = getFilename(context);
|
||||||
|
|
||||||
if (filename.endsWith('.spec.ts')) {
|
if (filename.endsWith('.spec.ts')) {
|
||||||
|
@@ -7,9 +7,9 @@
|
|||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
ESLintUtils,
|
ESLintUtils,
|
||||||
TSESLint,
|
|
||||||
TSESTree,
|
TSESTree,
|
||||||
} from '@typescript-eslint/utils';
|
} from '@typescript-eslint/utils';
|
||||||
|
import { RuleContext } from '@typescript-eslint/utils/ts-eslint';
|
||||||
|
|
||||||
import { fixture } from '../../../test/fixture';
|
import { fixture } from '../../../test/fixture';
|
||||||
import {
|
import {
|
||||||
@@ -68,7 +68,7 @@ There are a few exceptions where the base class can still be used:
|
|||||||
|
|
||||||
export const rule = ESLintUtils.RuleCreator.withoutDocs({
|
export const rule = ESLintUtils.RuleCreator.withoutDocs({
|
||||||
...info,
|
...info,
|
||||||
create(context: TSESLint.RuleContext<Message, unknown[]>) {
|
create(context: RuleContext<Message, unknown[]>) {
|
||||||
const filename = getFilename(context);
|
const filename = getFilename(context);
|
||||||
|
|
||||||
function handleUnthemedUsagesInTypescript(node: TSESTree.Identifier) {
|
function handleUnthemedUsagesInTypescript(node: TSESTree.Identifier) {
|
||||||
|
@@ -5,13 +5,17 @@
|
|||||||
*
|
*
|
||||||
* http://www.dspace.org/license/
|
* http://www.dspace.org/license/
|
||||||
*/
|
*/
|
||||||
import { TSESLint } from '@typescript-eslint/utils';
|
import {
|
||||||
import { RuleTester } from 'eslint';
|
InvalidTestCase,
|
||||||
|
RuleMetaData,
|
||||||
|
RuleModule,
|
||||||
|
ValidTestCase,
|
||||||
|
} from '@typescript-eslint/utils/ts-eslint';
|
||||||
import { EnumType } from 'typescript';
|
import { EnumType } from 'typescript';
|
||||||
|
|
||||||
export type Meta = TSESLint.RuleMetaData<string>;
|
export type Meta = RuleMetaData<string, unknown[]>;
|
||||||
export type Valid = TSESLint.ValidTestCase<unknown[]> | RuleTester.ValidTestCase;
|
export type Valid = ValidTestCase<unknown[]>;
|
||||||
export type Invalid = TSESLint.InvalidTestCase<string, unknown[]> | RuleTester.InvalidTestCase;
|
export type Invalid = InvalidTestCase<string, unknown[]>;
|
||||||
|
|
||||||
export interface DSpaceESLintRuleInfo {
|
export interface DSpaceESLintRuleInfo {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -28,7 +32,7 @@ export interface NamedTests {
|
|||||||
export interface RuleExports {
|
export interface RuleExports {
|
||||||
Message: EnumType,
|
Message: EnumType,
|
||||||
info: DSpaceESLintRuleInfo,
|
info: DSpaceESLintRuleInfo,
|
||||||
rule: TSESLint.RuleModule<string>,
|
rule: RuleModule<string>,
|
||||||
tests: NamedTests,
|
tests: NamedTests,
|
||||||
default: unknown,
|
default: unknown,
|
||||||
}
|
}
|
||||||
|
@@ -5,17 +5,18 @@
|
|||||||
*
|
*
|
||||||
* http://www.dspace.org/license/
|
* http://www.dspace.org/license/
|
||||||
*/
|
*/
|
||||||
|
import { TSESTree } from '@typescript-eslint/utils';
|
||||||
import {
|
import {
|
||||||
TSESLint,
|
RuleContext,
|
||||||
TSESTree,
|
SourceCode,
|
||||||
} from '@typescript-eslint/utils';
|
} from '@typescript-eslint/utils/ts-eslint';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
match,
|
match,
|
||||||
toUnixStylePath,
|
toUnixStylePath,
|
||||||
} from './misc';
|
} from './misc';
|
||||||
|
|
||||||
export type AnyRuleContext = TSESLint.RuleContext<string, unknown[]>;
|
export type AnyRuleContext = RuleContext<string, unknown[]>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the current filename based on the ESLint rule context as a Unix-style path.
|
* Return the current filename based on the ESLint rule context as a Unix-style path.
|
||||||
@@ -27,7 +28,7 @@ export function getFilename(context: AnyRuleContext): string {
|
|||||||
return toUnixStylePath(context.getFilename());
|
return toUnixStylePath(context.getFilename());
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSourceCode(context: AnyRuleContext): TSESLint.SourceCode {
|
export function getSourceCode(context: AnyRuleContext): SourceCode {
|
||||||
// TSESLint claims this is deprecated, but the suggested alternative is undefined (could be a version mismatch between ESLint and TSESlint?)
|
// TSESLint claims this is deprecated, but the suggested alternative is undefined (could be a version mismatch between ESLint and TSESlint?)
|
||||||
// eslint-disable-next-line deprecation/deprecation
|
// eslint-disable-next-line deprecation/deprecation
|
||||||
return context.getSourceCode();
|
return context.getSourceCode();
|
||||||
|
@@ -7,7 +7,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { RuleTester as TypeScriptRuleTester } from '@typescript-eslint/rule-tester';
|
import { RuleTester as TypeScriptRuleTester } from '@typescript-eslint/rule-tester';
|
||||||
import { RuleTester } from 'eslint';
|
import { RuleTester } from '@typescript-eslint/utils/ts-eslint';
|
||||||
|
|
||||||
import { themeableComponents } from '../src/util/theme-support';
|
import { themeableComponents } from '../src/util/theme-support';
|
||||||
import {
|
import {
|
||||||
|
23310
package-lock.json
generated
Normal file
23310
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
194
package.json
194
package.json
@@ -5,27 +5,27 @@
|
|||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
"config:watch": "nodemon",
|
"config:watch": "nodemon",
|
||||||
"test:rest": "ts-node --project ./tsconfig.ts-node.json scripts/test-rest.ts",
|
"test:rest": "ts-node --project ./tsconfig.ts-node.json scripts/test-rest.ts",
|
||||||
"start": "yarn run start:prod",
|
"start": "npm run start:prod",
|
||||||
"start:dev": "nodemon --exec \"cross-env NODE_ENV=development yarn run serve\"",
|
"start:dev": "nodemon --exec \"cross-env NODE_ENV=development npm run serve\"",
|
||||||
"start:prod": "yarn run build:prod && cross-env NODE_ENV=production yarn run serve:ssr",
|
"start:prod": "npm run build:prod && cross-env NODE_ENV=production npm run serve:ssr",
|
||||||
"start:mirador:prod": "yarn run build:mirador && yarn run start:prod",
|
"start:mirador:prod": "npm run build:mirador && npm run start:prod",
|
||||||
"preserve": "yarn base-href",
|
"preserve": "npm run base-href",
|
||||||
"serve": "ts-node --project ./tsconfig.ts-node.json scripts/serve.ts",
|
"serve": "ts-node --project ./tsconfig.ts-node.json scripts/serve.ts",
|
||||||
"serve:ssr": "node dist/server/main",
|
"serve:ssr": "node dist/server/main",
|
||||||
"analyze": "webpack-bundle-analyzer dist/browser/stats.json",
|
"analyze": "webpack-bundle-analyzer dist/browser/stats.json",
|
||||||
"build": "ng build --configuration development",
|
"build": "ng build --configuration development",
|
||||||
"build:stats": "ng build --stats-json",
|
"build:stats": "ng build --stats-json",
|
||||||
"build:prod": "cross-env NODE_ENV=production yarn run build:ssr",
|
"build:prod": "cross-env NODE_ENV=production npm run build:ssr",
|
||||||
"build:ssr": "ng build --configuration production && ng run dspace-angular:server:production",
|
"build:ssr": "ng build --configuration production && ng run dspace-angular:server:production",
|
||||||
"build:lint": "rimraf 'lint/dist/**/*.js' 'lint/dist/**/*.js.map' && tsc -b lint/tsconfig.json",
|
"build:lint": "rimraf 'lint/dist/**/*.js' 'lint/dist/**/*.js.map' && tsc -b lint/tsconfig.json",
|
||||||
"test": "ng test --source-map=true --watch=false --configuration test",
|
"test": "ng test --source-map=true --watch=false --configuration test",
|
||||||
"test:watch": "nodemon --exec \"ng test --source-map=true --watch=true --configuration test\"",
|
"test:watch": "nodemon --exec \"ng test --source-map=true --watch=true --configuration test\"",
|
||||||
"test:headless": "ng test --source-map=true --watch=false --configuration test --browsers=ChromeHeadless --code-coverage",
|
"test:headless": "ng test --source-map=true --watch=false --configuration test --browsers=ChromeHeadless --code-coverage",
|
||||||
"test:lint": "yarn build:lint && yarn test:lint:nobuild",
|
"test:lint": "npm run build:lint && npm run test:lint:nobuild",
|
||||||
"test:lint:nobuild": "jasmine --config=lint/jasmine.json",
|
"test:lint:nobuild": "jasmine --config=lint/jasmine.json",
|
||||||
"lint": "yarn build:lint && yarn lint:nobuild",
|
"lint": "npm run build:lint && npm run lint:nobuild",
|
||||||
"lint:nobuild": "ng lint",
|
"lint:nobuild": "ng lint",
|
||||||
"lint-fix": "yarn build:lint && ng lint --fix=true",
|
"lint-fix": "npm run build:lint && ng lint --fix=true",
|
||||||
"docs:lint": "ts-node --project ./lint/tsconfig.json ./lint/generate-docs.ts",
|
"docs:lint": "ts-node --project ./lint/tsconfig.json ./lint/generate-docs.ts",
|
||||||
"e2e": "cross-env NODE_ENV=production ng e2e",
|
"e2e": "cross-env NODE_ENV=production ng e2e",
|
||||||
"clean:dev:config": "rimraf src/assets/config.json",
|
"clean:dev:config": "rimraf src/assets/config.json",
|
||||||
@@ -36,8 +36,8 @@
|
|||||||
"clean:json": "rimraf *.records.json",
|
"clean:json": "rimraf *.records.json",
|
||||||
"clean:node": "rimraf node_modules",
|
"clean:node": "rimraf node_modules",
|
||||||
"clean:cli": "rimraf .angular/cache",
|
"clean:cli": "rimraf .angular/cache",
|
||||||
"clean:prod": "yarn run clean:dist && yarn run clean:log && yarn run clean:doc && yarn run clean:coverage && yarn run clean:json",
|
"clean:prod": "npm run clean:dist && npm run clean:log && npm run clean:doc && npm run clean:coverage && npm run clean:json",
|
||||||
"clean": "yarn run clean:prod && yarn run clean:dev:config && yarn run clean:cli && yarn run clean:node",
|
"clean": "npm run clean:prod && npm run clean:dev:config && npm run clean:cli && npm run clean:node",
|
||||||
"sync-i18n": "ts-node --project ./tsconfig.ts-node.json scripts/sync-i18n-files.ts",
|
"sync-i18n": "ts-node --project ./tsconfig.ts-node.json scripts/sync-i18n-files.ts",
|
||||||
"build:mirador": "webpack --config webpack/webpack.mirador.config.ts",
|
"build:mirador": "webpack --config webpack/webpack.mirador.config.ts",
|
||||||
"merge-i18n": "ts-node --project ./tsconfig.ts-node.json scripts/merge-i18n-files.ts",
|
"merge-i18n": "ts-node --project ./tsconfig.ts-node.json scripts/merge-i18n-files.ts",
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
"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",
|
"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 ./",
|
"check-circ-deps": "npx madge --exclude '(bitstream|bundle|collection|config-submission-form|eperson|item|version)\\.model\\.ts$' --circular --extensions ts ./",
|
||||||
"postinstall": "yarn build:lint || echo 'Skipped DSpace ESLint plugins.'"
|
"postinstall": "npm run build:lint || echo 'Skipped DSpace ESLint plugins.'"
|
||||||
},
|
},
|
||||||
"browser": {
|
"browser": {
|
||||||
"fs": false,
|
"fs": false,
|
||||||
@@ -55,28 +55,61 @@
|
|||||||
"https": false
|
"https": false
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"resolutions": {
|
"overrides": {
|
||||||
"minimist": "^1.2.5",
|
"@kolkov/ngx-gallery": {
|
||||||
"webdriver-manager": "^12.1.8",
|
"@angular/animations": "^17.3.11",
|
||||||
"ts-node": "10.2.1"
|
"@angular/common": "^17.3.11",
|
||||||
|
"@angular/core": "^17.3.11"
|
||||||
|
},
|
||||||
|
"@ng-bootstrap/ng-bootstrap": {
|
||||||
|
"@angular/common": "^17.3.11",
|
||||||
|
"@angular/core": "^17.3.11",
|
||||||
|
"@angular/forms": "^17.3.11",
|
||||||
|
"@angular/localize": "^17.3.11"
|
||||||
|
},
|
||||||
|
"@ng-dynamic-forms/core": {
|
||||||
|
"@angular/common": "^17.3.11",
|
||||||
|
"@angular/core": "^17.3.11",
|
||||||
|
"@angular/forms": "^17.3.11"
|
||||||
|
},
|
||||||
|
"@ng-dynamic-forms/ui-ng-bootstrap": {
|
||||||
|
"ngx-mask": "14.2.4"
|
||||||
|
},
|
||||||
|
"@ngtools/webpack": {
|
||||||
|
"@angular/compiler-cli": "^17.3.11",
|
||||||
|
"typescript": "~5.4.5"
|
||||||
|
},
|
||||||
|
"@nicky-lenaers/ngx-scroll-to": {
|
||||||
|
"@angular/common": "^17.3.11",
|
||||||
|
"@angular/core": "^17.3.11"
|
||||||
|
},
|
||||||
|
"eslint-plugin-unused-imports": {
|
||||||
|
"@typescript-eslint/eslint-plugin": "^7.2.0"
|
||||||
|
},
|
||||||
|
"ng2-file-upload": {
|
||||||
|
"@angular/common": "^17.3.11",
|
||||||
|
"@angular/core": "^17.3.11"
|
||||||
|
},
|
||||||
|
"ngx-infinite-scroll": {
|
||||||
|
"@angular/common": "^17.3.11",
|
||||||
|
"@angular/core": "^17.3.11"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "^17.3.11",
|
"@angular/animations": "^17.3.12",
|
||||||
"@angular/cdk": "^17.3.10",
|
"@angular/cdk": "^17.3.10",
|
||||||
"@angular/common": "^17.3.11",
|
"@angular/common": "^17.3.12",
|
||||||
"@angular/compiler": "^17.3.11",
|
"@angular/compiler": "^17.3.12",
|
||||||
"@angular/core": "^17.3.11",
|
"@angular/core": "^17.3.12",
|
||||||
"@angular/forms": "^17.3.11",
|
"@angular/forms": "^17.3.12",
|
||||||
"@angular/localize": "17.3.11",
|
"@angular/localize": "^17.3.12",
|
||||||
"@angular/platform-browser": "^17.3.11",
|
"@angular/platform-browser": "^17.3.12",
|
||||||
"@angular/platform-browser-dynamic": "^17.3.11",
|
"@angular/platform-browser-dynamic": "^17.3.12",
|
||||||
"@angular/platform-server": "^17.3.11",
|
"@angular/platform-server": "^17.3.12",
|
||||||
"@angular/router": "^17.3.11",
|
"@angular/router": "^17.3.12",
|
||||||
"@angular/ssr": "^17.3.8",
|
"@angular/ssr": "^17.3.11",
|
||||||
"@babel/runtime": "7.21.0",
|
"@babel/runtime": "7.26.0",
|
||||||
"@kolkov/ngx-gallery": "^2.0.1",
|
"@kolkov/ngx-gallery": "^2.0.1",
|
||||||
"@material-ui/core": "^4.11.0",
|
|
||||||
"@material-ui/icons": "^4.11.3",
|
|
||||||
"@ng-bootstrap/ng-bootstrap": "^11.0.0",
|
"@ng-bootstrap/ng-bootstrap": "^11.0.0",
|
||||||
"@ng-dynamic-forms/core": "^16.0.0",
|
"@ng-dynamic-forms/core": "^16.0.0",
|
||||||
"@ng-dynamic-forms/ui-ng-bootstrap": "^16.0.0",
|
"@ng-dynamic-forms/ui-ng-bootstrap": "^16.0.0",
|
||||||
@@ -85,136 +118,123 @@
|
|||||||
"@ngrx/store": "^17.1.1",
|
"@ngrx/store": "^17.1.1",
|
||||||
"@ngx-translate/core": "^14.0.0",
|
"@ngx-translate/core": "^14.0.0",
|
||||||
"@nicky-lenaers/ngx-scroll-to": "^14.0.0",
|
"@nicky-lenaers/ngx-scroll-to": "^14.0.0",
|
||||||
"@types/grecaptcha": "^3.0.4",
|
|
||||||
"angular-idle-preload": "3.0.0",
|
|
||||||
"angulartics2": "^12.2.0",
|
"angulartics2": "^12.2.0",
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.7.4",
|
||||||
"bootstrap": "^4.6.1",
|
"bootstrap": "^4.6.1",
|
||||||
"cerialize": "0.1.18",
|
"cerialize": "0.1.18",
|
||||||
"cli-progress": "^3.12.0",
|
"cli-progress": "^3.12.0",
|
||||||
"colors": "^1.4.0",
|
"colors": "^1.4.0",
|
||||||
"compression": "^1.7.4",
|
"compression": "^1.7.5",
|
||||||
"cookie-parser": "1.4.6",
|
"cookie-parser": "1.4.7",
|
||||||
"core-js": "^3.30.1",
|
"core-js": "^3.39.0",
|
||||||
"date-fns": "^2.29.3",
|
"date-fns": "^2.29.3",
|
||||||
"date-fns-tz": "^1.3.7",
|
"date-fns-tz": "^1.3.7",
|
||||||
"deepmerge": "^4.3.1",
|
"deepmerge": "^4.3.1",
|
||||||
"ejs": "^3.1.10",
|
"ejs": "^3.1.10",
|
||||||
"express": "^4.19.2",
|
"express": "^4.21.1",
|
||||||
"express-rate-limit": "^5.1.3",
|
"express-rate-limit": "^5.1.3",
|
||||||
"fast-json-patch": "^3.1.1",
|
"fast-json-patch": "^3.1.1",
|
||||||
"filesize": "^6.1.0",
|
"filesize": "^6.1.0",
|
||||||
"http-proxy-middleware": "^1.0.5",
|
"http-proxy-middleware": "^2.0.7",
|
||||||
"http-terminator": "^3.2.0",
|
"http-terminator": "^3.2.0",
|
||||||
"isbot": "^3.6.10",
|
"isbot": "^5.1.17",
|
||||||
"js-cookie": "2.2.1",
|
"js-cookie": "2.2.1",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"json5": "^2.2.3",
|
"json5": "^2.2.3",
|
||||||
"jsonschema": "1.4.1",
|
"jsonschema": "1.4.1",
|
||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
"klaro": "^0.7.18",
|
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lru-cache": "^7.14.1",
|
"lru-cache": "^7.14.1",
|
||||||
"markdown-it": "^13.0.1",
|
"markdown-it": "^13.0.1",
|
||||||
"mirador": "^3.3.0",
|
"mirador": "^3.3.0",
|
||||||
"mirador-dl-plugin": "^0.13.0",
|
"mirador-dl-plugin": "^0.13.0",
|
||||||
"mirador-share-plugin": "^0.11.0",
|
"mirador-share-plugin": "^0.16.0",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"ng-mocks": "^14.10.0",
|
|
||||||
"ng2-file-upload": "5.0.0",
|
"ng2-file-upload": "5.0.0",
|
||||||
"ng2-nouislider": "^2.0.0",
|
"ng2-nouislider": "^2.0.0",
|
||||||
"ngx-infinite-scroll": "^16.0.0",
|
"ngx-infinite-scroll": "^16.0.0",
|
||||||
"ngx-pagination": "6.0.3",
|
"ngx-pagination": "6.0.3",
|
||||||
"ngx-ui-switch": "^14.1.0",
|
"ngx-ui-switch": "^14.1.0",
|
||||||
"nouislider": "^15.7.1",
|
"nouislider": "^15.7.1",
|
||||||
"pem": "1.14.7",
|
"orejime": "^2.3.0",
|
||||||
"prop-types": "^15.8.1",
|
"pem": "1.14.8",
|
||||||
"react-copy-to-clipboard": "^5.1.0",
|
"reflect-metadata": "^0.2.2",
|
||||||
"reflect-metadata": "^0.1.13",
|
|
||||||
"rxjs": "^7.8.0",
|
"rxjs": "^7.8.0",
|
||||||
"sanitize-html": "^2.12.1",
|
|
||||||
"sortablejs": "1.15.0",
|
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
"webfontloader": "1.6.28",
|
"zone.js": "~0.14.10"
|
||||||
"zone.js": "~0.14.4"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-builders/custom-webpack": "~17.0.2",
|
"@angular-builders/custom-webpack": "~17.0.2",
|
||||||
"@angular-devkit/build-angular": "^17.3.8",
|
"@angular-devkit/build-angular": "^17.3.11",
|
||||||
"@angular-eslint/builder": "17.2.1",
|
"@angular-eslint/builder": "^17.5.3",
|
||||||
"@angular-eslint/bundled-angular-compiler": "17.2.1",
|
"@angular-eslint/bundled-angular-compiler": "^17.5.3",
|
||||||
"@angular-eslint/eslint-plugin": "17.2.1",
|
"@angular-eslint/eslint-plugin": "^17.5.3",
|
||||||
"@angular-eslint/eslint-plugin-template": "17.2.1",
|
"@angular-eslint/eslint-plugin-template": "^17.5.3",
|
||||||
"@angular-eslint/schematics": "17.2.1",
|
"@angular-eslint/schematics": "^17.5.3",
|
||||||
"@angular-eslint/template-parser": "17.2.1",
|
"@angular-eslint/template-parser": "^17.5.3",
|
||||||
"@angular/cli": "^17.3.8",
|
"@angular-eslint/utils": "^17.5.3",
|
||||||
|
"@angular/cli": "^17.3.11",
|
||||||
"@angular/compiler-cli": "^17.3.11",
|
"@angular/compiler-cli": "^17.3.11",
|
||||||
"@angular/language-service": "^17.3.11",
|
"@angular/language-service": "^17.3.12",
|
||||||
"@cypress/schematic": "^1.5.0",
|
"@cypress/schematic": "^1.5.0",
|
||||||
"@fortawesome/fontawesome-free": "^6.4.0",
|
"@fortawesome/fontawesome-free": "^6.6.0",
|
||||||
"@ngrx/store-devtools": "^17.1.1",
|
"@ngrx/store-devtools": "^17.1.1",
|
||||||
"@ngtools/webpack": "^16.2.12",
|
"@ngtools/webpack": "^16.2.16",
|
||||||
"@types/deep-freeze": "0.1.2",
|
"@types/deep-freeze": "0.1.5",
|
||||||
"@types/ejs": "^3.1.2",
|
"@types/ejs": "^3.1.2",
|
||||||
"@types/express": "^4.17.17",
|
"@types/express": "^4.17.17",
|
||||||
|
"@types/grecaptcha": "^3.0.9",
|
||||||
"@types/jasmine": "~3.6.0",
|
"@types/jasmine": "~3.6.0",
|
||||||
"@types/js-cookie": "2.2.6",
|
"@types/js-cookie": "2.2.6",
|
||||||
"@types/lodash": "^4.14.194",
|
"@types/lodash": "^4.17.13",
|
||||||
"@types/node": "^14.14.9",
|
"@types/node": "^14.14.9",
|
||||||
"@types/sanitize-html": "^2.9.0",
|
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.2.0",
|
"@typescript-eslint/parser": "^7.18.0",
|
||||||
"@typescript-eslint/parser": "^7.2.0",
|
"@typescript-eslint/rule-tester": "^7.18.0",
|
||||||
"@typescript-eslint/rule-tester": "^7.2.0",
|
"@typescript-eslint/utils": "^7.18.0",
|
||||||
"@typescript-eslint/utils": "^7.2.0",
|
"axe-core": "^4.10.2",
|
||||||
"axe-core": "^4.7.2",
|
|
||||||
"browser-sync": "^3.0.0",
|
|
||||||
"compression-webpack-plugin": "^9.2.0",
|
"compression-webpack-plugin": "^9.2.0",
|
||||||
"copy-webpack-plugin": "^6.4.1",
|
"copy-webpack-plugin": "^6.4.1",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"cypress": "12.17.4",
|
"cypress": "^13.16.0",
|
||||||
"cypress-axe": "^1.4.0",
|
"cypress-axe": "^1.5.0",
|
||||||
"deep-freeze": "0.0.1",
|
"deep-freeze": "0.0.1",
|
||||||
"eslint": "^8.39.0",
|
"eslint": "^8.39.0",
|
||||||
"eslint-plugin-deprecation": "^1.4.1",
|
"eslint-plugin-deprecation": "^1.4.1",
|
||||||
"eslint-plugin-dspace-angular-html": "link:./lint/dist/src/rules/html",
|
"eslint-plugin-dspace-angular-html": "file:./lint/dist/src/rules/html",
|
||||||
"eslint-plugin-dspace-angular-ts": "link:./lint/dist/src/rules/ts",
|
"eslint-plugin-dspace-angular-ts": "file:./lint/dist/src/rules/ts",
|
||||||
"eslint-plugin-import": "^2.27.5",
|
"eslint-plugin-import": "^2.31.0",
|
||||||
"eslint-plugin-import-newlines": "^1.3.1",
|
"eslint-plugin-import-newlines": "^1.3.1",
|
||||||
"eslint-plugin-jsdoc": "^45.0.0",
|
"eslint-plugin-jsdoc": "^45.0.0",
|
||||||
"eslint-plugin-jsonc": "^2.6.0",
|
"eslint-plugin-jsonc": "^2.6.0",
|
||||||
"eslint-plugin-lodash": "^7.4.0",
|
"eslint-plugin-lodash": "^7.4.0",
|
||||||
"eslint-plugin-rxjs": "^5.0.3",
|
"eslint-plugin-rxjs": "^5.0.3",
|
||||||
"eslint-plugin-simple-import-sort": "^10.0.0",
|
"eslint-plugin-simple-import-sort": "^10.0.0",
|
||||||
"eslint-plugin-unused-imports": "^2.0.0",
|
"eslint-plugin-unused-imports": "^3.2.0",
|
||||||
"express-static-gzip": "^2.1.7",
|
"express-static-gzip": "^2.1.8",
|
||||||
"jasmine": "^3.8.0",
|
"jasmine": "^3.8.0",
|
||||||
"jasmine-core": "^3.8.0",
|
"jasmine-core": "^3.8.0",
|
||||||
"jasmine-marbles": "0.9.2",
|
"jasmine-marbles": "0.9.2",
|
||||||
"karma": "^6.4.2",
|
"karma": "^6.4.4",
|
||||||
"karma-chrome-launcher": "~3.2.0",
|
"karma-chrome-launcher": "~3.2.0",
|
||||||
"karma-coverage-istanbul-reporter": "~3.0.3",
|
"karma-coverage-istanbul-reporter": "~3.0.3",
|
||||||
"karma-jasmine": "~4.0.0",
|
"karma-jasmine": "~4.0.0",
|
||||||
"karma-jasmine-html-reporter": "^1.5.0",
|
"karma-jasmine-html-reporter": "^1.5.0",
|
||||||
"karma-mocha-reporter": "2.2.5",
|
"karma-mocha-reporter": "2.2.5",
|
||||||
|
"ng-mocks": "^14.13.1",
|
||||||
"ngx-mask": "14.2.4",
|
"ngx-mask": "14.2.4",
|
||||||
"nodemon": "^2.0.22",
|
"nodemon": "^2.0.22",
|
||||||
"postcss": "^8.4",
|
"postcss": "^8.4",
|
||||||
"postcss-apply": "0.12.0",
|
|
||||||
"postcss-import": "^14.0.0",
|
"postcss-import": "^14.0.0",
|
||||||
"postcss-loader": "^4.0.3",
|
"postcss-loader": "^4.0.3",
|
||||||
"postcss-preset-env": "^7.4.2",
|
"postcss-preset-env": "^7.4.2",
|
||||||
"postcss-responsive-type": "1.0.0",
|
|
||||||
"react": "^16.14.0",
|
|
||||||
"react-dom": "^16.14.0",
|
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"rxjs-spy": "^8.0.2",
|
"sass": "~1.80.6",
|
||||||
"sass": "~1.62.0",
|
|
||||||
"sass-loader": "^12.6.0",
|
"sass-loader": "^12.6.0",
|
||||||
"sass-resources-loader": "^2.2.5",
|
"sass-resources-loader": "^2.2.5",
|
||||||
"ts-node": "^8.10.2",
|
"ts-node": "^8.10.2",
|
||||||
"typescript": "~5.3.3",
|
"typescript": "~5.4.5",
|
||||||
"webpack": "5.90.3",
|
"webpack": "5.96.1",
|
||||||
"webpack-bundle-analyzer": "^4.8.0",
|
|
||||||
"webpack-cli": "^5.1.4",
|
"webpack-cli": "^5.1.4",
|
||||||
"webpack-dev-server": "^4.15.1"
|
"webpack-dev-server": "^4.15.1"
|
||||||
}
|
}
|
||||||
|
@@ -1,8 +1,6 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: [
|
plugins: [
|
||||||
require('postcss-import')(),
|
require('postcss-import')(),
|
||||||
require('postcss-preset-env')(),
|
require('postcss-preset-env')()
|
||||||
require('postcss-apply')(),
|
|
||||||
require('postcss-responsive-type')()
|
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
@@ -38,7 +38,7 @@ parseCliInput();
|
|||||||
function parseCliInput() {
|
function parseCliInput() {
|
||||||
program
|
program
|
||||||
.option('-d, --output-dir <output-dir>', 'output dir when running script on all language files', projectRoot(LANGUAGE_FILES_LOCATION))
|
.option('-d, --output-dir <output-dir>', 'output dir when running script on all language files', projectRoot(LANGUAGE_FILES_LOCATION))
|
||||||
.option('-s, --source-dir <source-dir>', 'source dir of transalations to be merged')
|
.option('-s, --source-dir <source-dir>', 'source dir of translations to be merged')
|
||||||
.usage('(-s <source-dir> [-d <output-dir>])')
|
.usage('(-s <source-dir> [-d <output-dir>])')
|
||||||
.parse(process.argv);
|
.parse(process.argv);
|
||||||
|
|
||||||
|
@@ -27,7 +27,7 @@ import * as expressStaticGzip from 'express-static-gzip';
|
|||||||
/* eslint-enable import/no-namespace */
|
/* eslint-enable import/no-namespace */
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import LRU from 'lru-cache';
|
import LRU from 'lru-cache';
|
||||||
import isbot from 'isbot';
|
import { isbot } from 'isbot';
|
||||||
import { createCertificate } from 'pem';
|
import { createCertificate } from 'pem';
|
||||||
import { createServer } from 'https';
|
import { createServer } from 'https';
|
||||||
import { json } from 'body-parser';
|
import { json } from 'body-parser';
|
||||||
@@ -99,7 +99,7 @@ export function app() {
|
|||||||
* If production mode is enabled in the environment file:
|
* If production mode is enabled in the environment file:
|
||||||
* - Enable Angular's production mode
|
* - Enable Angular's production mode
|
||||||
* - Initialize caching of SSR rendered pages (if enabled in config.yml)
|
* - Initialize caching of SSR rendered pages (if enabled in config.yml)
|
||||||
* - Enable compression for SSR reponses. See [compression](https://github.com/expressjs/compression)
|
* - Enable compression for SSR responses. See [compression](https://github.com/expressjs/compression)
|
||||||
*/
|
*/
|
||||||
if (environment.production) {
|
if (environment.production) {
|
||||||
enableProdMode();
|
enableProdMode();
|
||||||
@@ -428,7 +428,7 @@ function checkCacheForRequest(cacheName: string, cache: LRU<string, any>, req, r
|
|||||||
if (environment.cache.serverSide.debug) { console.log(`CACHE EXPIRED FOR ${key} in ${cacheName} cache. Re-rendering...`); }
|
if (environment.cache.serverSide.debug) { console.log(`CACHE EXPIRED FOR ${key} in ${cacheName} cache. Re-rendering...`); }
|
||||||
// Update cached copy by rerendering server-side
|
// Update cached copy by rerendering server-side
|
||||||
// NOTE: In this scenario the currently cached copy will be returned to the current user.
|
// NOTE: In this scenario the currently cached copy will be returned to the current user.
|
||||||
// This re-render is peformed behind the scenes to update cached copy for next user.
|
// This re-render is performed behind the scenes to update cached copy for next user.
|
||||||
serverSideRender(req, res, next, false);
|
serverSideRender(req, res, next, false);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@@ -61,7 +61,7 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let epersonDto of (ePeopleDto$ | async)?.page"
|
<tr *ngFor="let epersonDto of (ePeopleDto$ | async)?.page"
|
||||||
[ngClass]="{'table-primary' : isActive(epersonDto.eperson) | async}">
|
[ngClass]="{'table-primary' : (activeEPerson$ | async) === epersonDto.eperson}">
|
||||||
<td>{{epersonDto.eperson.id}}</td>
|
<td>{{epersonDto.eperson.id}}</td>
|
||||||
<td>{{ dsoNameService.getName(epersonDto.eperson) }}</td>
|
<td>{{ dsoNameService.getName(epersonDto.eperson) }}</td>
|
||||||
<td>{{epersonDto.eperson.email}}</td>
|
<td>{{epersonDto.eperson.email}}</td>
|
||||||
|
@@ -100,6 +100,8 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
ePeopleDto$: BehaviorSubject<PaginatedList<EpersonDtoModel>> = new BehaviorSubject<PaginatedList<EpersonDtoModel>>({} as any);
|
ePeopleDto$: BehaviorSubject<PaginatedList<EpersonDtoModel>> = new BehaviorSubject<PaginatedList<EpersonDtoModel>>({} as any);
|
||||||
|
|
||||||
|
activeEPerson$: Observable<EPerson>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An observable for the pageInfo, needed to pass to the pagination component
|
* An observable for the pageInfo, needed to pass to the pagination component
|
||||||
*/
|
*/
|
||||||
@@ -165,6 +167,7 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
|||||||
initialisePage() {
|
initialisePage() {
|
||||||
this.searching$.next(true);
|
this.searching$.next(true);
|
||||||
this.search({ scope: this.currentSearchScope, query: this.currentSearchQuery });
|
this.search({ scope: this.currentSearchScope, query: this.currentSearchQuery });
|
||||||
|
this.activeEPerson$ = this.epersonService.getActiveEPerson();
|
||||||
this.subs.push(this.ePeople$.pipe(
|
this.subs.push(this.ePeople$.pipe(
|
||||||
switchMap((epeople: PaginatedList<EPerson>) => {
|
switchMap((epeople: PaginatedList<EPerson>) => {
|
||||||
if (epeople.pageInfo.totalElements > 0) {
|
if (epeople.pageInfo.totalElements > 0) {
|
||||||
@@ -232,23 +235,6 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether the given EPerson is active (being edited)
|
|
||||||
* @param eperson
|
|
||||||
*/
|
|
||||||
isActive(eperson: EPerson): Observable<boolean> {
|
|
||||||
return this.getActiveEPerson().pipe(
|
|
||||||
map((activeEPerson) => eperson === activeEPerson),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the active eperson (being edited)
|
|
||||||
*/
|
|
||||||
getActiveEPerson(): Observable<EPerson> {
|
|
||||||
return this.epersonService.getActiveEPerson();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes EPerson, show notification on success/failure & updates EPeople list
|
* Deletes EPerson, show notification on success/failure & updates EPeople list
|
||||||
*/
|
*/
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
<div class="group-form row">
|
<div class="group-form row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
|
|
||||||
<div *ngIf="epersonService.getActiveEPerson() | async; then editHeader; else createHeader"></div>
|
<div *ngIf="activeEPerson$ | async; then editHeader; else createHeader"></div>
|
||||||
|
|
||||||
<ng-template #createHeader>
|
<ng-template #createHeader>
|
||||||
<h1 class="border-bottom pb-2">{{messagePrefix + '.create' | translate}}</h1>
|
<h1 class="border-bottom pb-2">{{messagePrefix + '.create' | translate}}</h1>
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
|
|
||||||
<ds-loading [showMessage]="false" *ngIf="!formGroup"></ds-loading>
|
<ds-loading [showMessage]="false" *ngIf="!formGroup"></ds-loading>
|
||||||
|
|
||||||
<div *ngIf="epersonService.getActiveEPerson() | async">
|
<div *ngIf="activeEPerson$ | async">
|
||||||
<h2>{{messagePrefix + '.groupsEPersonIsMemberOf' | translate}}</h2>
|
<h2>{{messagePrefix + '.groupsEPersonIsMemberOf' | translate}}</h2>
|
||||||
|
|
||||||
<ds-loading [showMessage]="false" *ngIf="groups$ | async | dsHasNoValue"></ds-loading>
|
<ds-loading [showMessage]="false" *ngIf="groups$ | async | dsHasNoValue"></ds-loading>
|
||||||
@@ -75,7 +75,9 @@
|
|||||||
{{ dsoNameService.getName(group) }}
|
{{ dsoNameService.getName(group) }}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="align-middle">{{ dsoNameService.getName(undefined) }}</td>
|
<td class="align-middle">
|
||||||
|
{{ dsoNameService.getName((group.object | async)?.payload) }}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
import {
|
import {
|
||||||
ComponentFixture,
|
ComponentFixture,
|
||||||
TestBed,
|
TestBed,
|
||||||
@@ -19,12 +19,10 @@ import {
|
|||||||
import {
|
import {
|
||||||
ActivatedRoute,
|
ActivatedRoute,
|
||||||
Router,
|
Router,
|
||||||
|
RouterModule,
|
||||||
} from '@angular/router';
|
} from '@angular/router';
|
||||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import {
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
TranslateLoader,
|
|
||||||
TranslateModule,
|
|
||||||
} from '@ngx-translate/core';
|
|
||||||
import {
|
import {
|
||||||
Observable,
|
Observable,
|
||||||
of as observableOf,
|
of as observableOf,
|
||||||
@@ -49,7 +47,6 @@ import { FormBuilderService } from '../../../shared/form/builder/form-builder.se
|
|||||||
import { FormComponent } from '../../../shared/form/form.component';
|
import { FormComponent } from '../../../shared/form/form.component';
|
||||||
import { ThemedLoadingComponent } from '../../../shared/loading/themed-loading.component';
|
import { ThemedLoadingComponent } from '../../../shared/loading/themed-loading.component';
|
||||||
import { getMockFormBuilderService } from '../../../shared/mocks/form-builder-service.mock';
|
import { getMockFormBuilderService } from '../../../shared/mocks/form-builder-service.mock';
|
||||||
import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock';
|
|
||||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
import { PaginationComponent } from '../../../shared/pagination/pagination.component';
|
import { PaginationComponent } from '../../../shared/pagination/pagination.component';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||||
@@ -92,9 +89,6 @@ describe('EPersonFormComponent', () => {
|
|||||||
ePersonDataServiceStub = {
|
ePersonDataServiceStub = {
|
||||||
activeEPerson: null,
|
activeEPerson: null,
|
||||||
allEpeople: mockEPeople,
|
allEpeople: mockEPeople,
|
||||||
getEPeople(): Observable<RemoteData<PaginatedList<EPerson>>> {
|
|
||||||
return createSuccessfulRemoteDataObject$(buildPaginatedList(null, this.allEpeople));
|
|
||||||
},
|
|
||||||
getActiveEPerson(): Observable<EPerson> {
|
getActiveEPerson(): Observable<EPerson> {
|
||||||
return observableOf(this.activeEPerson);
|
return observableOf(this.activeEPerson);
|
||||||
},
|
},
|
||||||
@@ -228,12 +222,8 @@ describe('EPersonFormComponent', () => {
|
|||||||
router = new RouterStub();
|
router = new RouterStub();
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
||||||
TranslateModule.forRoot({
|
RouterModule.forRoot([]),
|
||||||
loader: {
|
TranslateModule.forRoot(),
|
||||||
provide: TranslateLoader,
|
|
||||||
useClass: TranslateLoaderMock,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
EPersonFormComponent,
|
EPersonFormComponent,
|
||||||
HasNoValuePipe,
|
HasNoValuePipe,
|
||||||
],
|
],
|
||||||
@@ -251,7 +241,7 @@ describe('EPersonFormComponent', () => {
|
|||||||
{ provide: Router, useValue: router },
|
{ provide: Router, useValue: router },
|
||||||
EPeopleRegistryComponent,
|
EPeopleRegistryComponent,
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA],
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||||
})
|
})
|
||||||
.overrideComponent(EPersonFormComponent, {
|
.overrideComponent(EPersonFormComponent, {
|
||||||
remove: { imports: [ ThemedLoadingComponent, PaginationComponent,FormComponent] },
|
remove: { imports: [ ThemedLoadingComponent, PaginationComponent,FormComponent] },
|
||||||
@@ -274,37 +264,13 @@ describe('EPersonFormComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('check form validation', () => {
|
describe('check form validation', () => {
|
||||||
let firstName;
|
let canLogIn: boolean;
|
||||||
let lastName;
|
let requireCertificate: boolean;
|
||||||
let email;
|
|
||||||
let canLogIn;
|
|
||||||
let requireCertificate;
|
|
||||||
|
|
||||||
let expected;
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
firstName = 'testName';
|
|
||||||
lastName = 'testLastName';
|
|
||||||
email = 'testEmail@test.com';
|
|
||||||
canLogIn = false;
|
canLogIn = false;
|
||||||
requireCertificate = false;
|
requireCertificate = false;
|
||||||
|
|
||||||
expected = Object.assign(new EPerson(), {
|
|
||||||
metadata: {
|
|
||||||
'eperson.firstname': [
|
|
||||||
{
|
|
||||||
value: firstName,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'eperson.lastname': [
|
|
||||||
{
|
|
||||||
value: lastName,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
email: email,
|
|
||||||
canLogIn: canLogIn,
|
|
||||||
requireCertificate: requireCertificate,
|
|
||||||
});
|
|
||||||
spyOn(component.submitForm, 'emit');
|
spyOn(component.submitForm, 'emit');
|
||||||
component.canLogIn.value = canLogIn;
|
component.canLogIn.value = canLogIn;
|
||||||
component.requireCertificate.value = requireCertificate;
|
component.requireCertificate.value = requireCertificate;
|
||||||
@@ -378,15 +344,13 @@ describe('EPersonFormComponent', () => {
|
|||||||
expect(component.formGroup.controls.email.errors.emailTaken).toBeTruthy();
|
expect(component.formGroup.controls.email.errors.emailTaken).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when submitting the form', () => {
|
describe('when submitting the form', () => {
|
||||||
let firstName;
|
let firstName;
|
||||||
let lastName;
|
let lastName;
|
||||||
let email;
|
let email;
|
||||||
let canLogIn;
|
let canLogIn: boolean;
|
||||||
let requireCertificate;
|
let requireCertificate;
|
||||||
|
|
||||||
let expected;
|
let expected;
|
||||||
@@ -415,6 +379,7 @@ describe('EPersonFormComponent', () => {
|
|||||||
requireCertificate: requireCertificate,
|
requireCertificate: requireCertificate,
|
||||||
});
|
});
|
||||||
spyOn(component.submitForm, 'emit');
|
spyOn(component.submitForm, 'emit');
|
||||||
|
component.ngOnInit();
|
||||||
component.firstName.value = firstName;
|
component.firstName.value = firstName;
|
||||||
component.lastName.value = lastName;
|
component.lastName.value = lastName;
|
||||||
component.email.value = email;
|
component.email.value = email;
|
||||||
@@ -454,9 +419,17 @@ describe('EPersonFormComponent', () => {
|
|||||||
email: email,
|
email: email,
|
||||||
canLogIn: canLogIn,
|
canLogIn: canLogIn,
|
||||||
requireCertificate: requireCertificate,
|
requireCertificate: requireCertificate,
|
||||||
_links: undefined,
|
_links: {
|
||||||
|
groups: {
|
||||||
|
href: '',
|
||||||
|
},
|
||||||
|
self: {
|
||||||
|
href: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
spyOn(ePersonDataServiceStub, 'getActiveEPerson').and.returnValue(observableOf(expectedWithId));
|
spyOn(ePersonDataServiceStub, 'getActiveEPerson').and.returnValue(observableOf(expectedWithId));
|
||||||
|
component.ngOnInit();
|
||||||
component.onSubmit();
|
component.onSubmit();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
@@ -504,22 +477,19 @@ describe('EPersonFormComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('delete', () => {
|
describe('delete', () => {
|
||||||
|
|
||||||
let ePersonId;
|
|
||||||
let eperson: EPerson;
|
let eperson: EPerson;
|
||||||
let modalService;
|
let modalService;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(authService, 'impersonate').and.callThrough();
|
spyOn(authService, 'impersonate').and.callThrough();
|
||||||
ePersonId = 'testEPersonId';
|
|
||||||
eperson = EPersonMock;
|
eperson = EPersonMock;
|
||||||
component.epersonInitial = eperson;
|
component.epersonInitial = eperson;
|
||||||
component.canDelete$ = observableOf(true);
|
component.canDelete$ = observableOf(true);
|
||||||
spyOn(component.epersonService, 'getActiveEPerson').and.returnValue(observableOf(eperson));
|
spyOn(component.epersonService, 'getActiveEPerson').and.returnValue(observableOf(eperson));
|
||||||
modalService = (component as any).modalService;
|
modalService = (component as any).modalService;
|
||||||
spyOn(modalService, 'open').and.returnValue(Object.assign({ componentInstance: Object.assign({ response: observableOf(true) }) }));
|
spyOn(modalService, 'open').and.returnValue(Object.assign({ componentInstance: Object.assign({ response: observableOf(true) }) }));
|
||||||
|
component.ngOnInit();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('the delete button should be visible if the ePerson can be deleted', () => {
|
it('the delete button should be visible if the ePerson can be deleted', () => {
|
||||||
|
@@ -189,6 +189,11 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
canImpersonate$: Observable<boolean>;
|
canImpersonate$: Observable<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current {@link EPerson}
|
||||||
|
*/
|
||||||
|
activeEPerson$: Observable<EPerson>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of subscriptions
|
* List of subscriptions
|
||||||
*/
|
*/
|
||||||
@@ -254,7 +259,11 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
protected route: ActivatedRoute,
|
protected route: ActivatedRoute,
|
||||||
protected router: Router,
|
protected router: Router,
|
||||||
) {
|
) {
|
||||||
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.activeEPerson$ = this.epersonService.getActiveEPerson();
|
||||||
|
this.subs.push(this.activeEPerson$.subscribe((eperson: EPerson) => {
|
||||||
this.epersonInitial = eperson;
|
this.epersonInitial = eperson;
|
||||||
if (hasValue(eperson)) {
|
if (hasValue(eperson)) {
|
||||||
this.isImpersonated = this.authService.isImpersonatingUser(eperson.id);
|
this.isImpersonated = this.authService.isImpersonatingUser(eperson.id);
|
||||||
@@ -262,9 +271,6 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
this.submitLabel = 'form.submit';
|
this.submitLabel = 'form.submit';
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit() {
|
|
||||||
this.initialisePage();
|
this.initialisePage();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,130 +278,121 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
* This method will initialise the page
|
* This method will initialise the page
|
||||||
*/
|
*/
|
||||||
initialisePage() {
|
initialisePage() {
|
||||||
this.subs.push(this.epersonService.findById(this.route.snapshot.params.id).subscribe((ePersonRD: RemoteData<EPerson>) => {
|
if (this.route.snapshot.params.id) {
|
||||||
this.epersonService.editEPerson(ePersonRD.payload);
|
this.subs.push(this.epersonService.findById(this.route.snapshot.params.id).subscribe((ePersonRD: RemoteData<EPerson>) => {
|
||||||
}));
|
this.epersonService.editEPerson(ePersonRD.payload);
|
||||||
observableCombineLatest([
|
|
||||||
this.translateService.get(`${this.messagePrefix}.firstName`),
|
|
||||||
this.translateService.get(`${this.messagePrefix}.lastName`),
|
|
||||||
this.translateService.get(`${this.messagePrefix}.email`),
|
|
||||||
this.translateService.get(`${this.messagePrefix}.canLogIn`),
|
|
||||||
this.translateService.get(`${this.messagePrefix}.requireCertificate`),
|
|
||||||
this.translateService.get(`${this.messagePrefix}.emailHint`),
|
|
||||||
]).subscribe(([firstName, lastName, email, canLogIn, requireCertificate, emailHint]) => {
|
|
||||||
this.firstName = new DynamicInputModel({
|
|
||||||
id: 'firstName',
|
|
||||||
label: firstName,
|
|
||||||
name: 'firstName',
|
|
||||||
validators: {
|
|
||||||
required: null,
|
|
||||||
},
|
|
||||||
required: true,
|
|
||||||
});
|
|
||||||
this.lastName = new DynamicInputModel({
|
|
||||||
id: 'lastName',
|
|
||||||
label: lastName,
|
|
||||||
name: 'lastName',
|
|
||||||
validators: {
|
|
||||||
required: null,
|
|
||||||
},
|
|
||||||
required: true,
|
|
||||||
});
|
|
||||||
this.email = new DynamicInputModel({
|
|
||||||
id: 'email',
|
|
||||||
label: email,
|
|
||||||
name: 'email',
|
|
||||||
validators: {
|
|
||||||
required: null,
|
|
||||||
pattern: '^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,4}$',
|
|
||||||
},
|
|
||||||
required: true,
|
|
||||||
errorMessages: {
|
|
||||||
emailTaken: 'error.validation.emailTaken',
|
|
||||||
pattern: 'error.validation.NotValidEmail',
|
|
||||||
},
|
|
||||||
hint: emailHint,
|
|
||||||
});
|
|
||||||
this.canLogIn = new DynamicCheckboxModel(
|
|
||||||
{
|
|
||||||
id: 'canLogIn',
|
|
||||||
label: canLogIn,
|
|
||||||
name: 'canLogIn',
|
|
||||||
value: (this.epersonInitial != null ? this.epersonInitial.canLogIn : true),
|
|
||||||
});
|
|
||||||
this.requireCertificate = new DynamicCheckboxModel(
|
|
||||||
{
|
|
||||||
id: 'requireCertificate',
|
|
||||||
label: requireCertificate,
|
|
||||||
name: 'requireCertificate',
|
|
||||||
value: (this.epersonInitial != null ? this.epersonInitial.requireCertificate : false),
|
|
||||||
});
|
|
||||||
this.formModel = [
|
|
||||||
this.firstName,
|
|
||||||
this.lastName,
|
|
||||||
this.email,
|
|
||||||
this.canLogIn,
|
|
||||||
this.requireCertificate,
|
|
||||||
];
|
|
||||||
this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
|
|
||||||
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
|
|
||||||
if (eperson != null) {
|
|
||||||
this.groups$ = this.groupsDataService.findListByHref(eperson._links.groups.href, {
|
|
||||||
currentPage: 1,
|
|
||||||
elementsPerPage: this.config.pageSize,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.formGroup.patchValue({
|
|
||||||
firstName: eperson != null ? eperson.firstMetadataValue('eperson.firstname') : '',
|
|
||||||
lastName: eperson != null ? eperson.firstMetadataValue('eperson.lastname') : '',
|
|
||||||
email: eperson != null ? eperson.email : '',
|
|
||||||
canLogIn: eperson != null ? eperson.canLogIn : true,
|
|
||||||
requireCertificate: eperson != null ? eperson.requireCertificate : false,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (eperson === null && !!this.formGroup.controls.email) {
|
|
||||||
this.formGroup.controls.email.setAsyncValidators(ValidateEmailNotTaken.createValidator(this.epersonService));
|
|
||||||
this.emailValueChangeSubscribe = this.email.valueChanges.pipe(debounceTime(300)).subscribe(() => {
|
|
||||||
this.changeDetectorRef.detectChanges();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}));
|
}));
|
||||||
|
}
|
||||||
const activeEPerson$ = this.epersonService.getActiveEPerson();
|
this.firstName = new DynamicInputModel({
|
||||||
|
id: 'firstName',
|
||||||
this.groups$ = activeEPerson$.pipe(
|
label: this.translateService.instant(`${this.messagePrefix}.firstName`),
|
||||||
switchMap((eperson) => {
|
name: 'firstName',
|
||||||
return observableCombineLatest([observableOf(eperson), this.paginationService.getFindListOptions(this.config.id, {
|
validators: {
|
||||||
currentPage: 1,
|
required: null,
|
||||||
elementsPerPage: this.config.pageSize,
|
},
|
||||||
})]);
|
required: true,
|
||||||
}),
|
|
||||||
switchMap(([eperson, findListOptions]) => {
|
|
||||||
if (eperson != null) {
|
|
||||||
return this.groupsDataService.findListByHref(eperson._links.groups.href, findListOptions, true, true, followLink('object'));
|
|
||||||
}
|
|
||||||
return observableOf(undefined);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
this.groupsPageInfoState$ = this.groups$.pipe(
|
|
||||||
map(groupsRD => groupsRD.payload.pageInfo),
|
|
||||||
);
|
|
||||||
|
|
||||||
this.canImpersonate$ = activeEPerson$.pipe(
|
|
||||||
switchMap((eperson) => {
|
|
||||||
if (hasValue(eperson)) {
|
|
||||||
return this.authorizationService.isAuthorized(FeatureID.LoginOnBehalfOf, eperson.self);
|
|
||||||
} else {
|
|
||||||
return observableOf(false);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
this.canDelete$ = activeEPerson$.pipe(
|
|
||||||
switchMap((eperson) => this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(eperson) ? eperson.self : undefined)),
|
|
||||||
);
|
|
||||||
this.canReset$ = observableOf(true);
|
|
||||||
});
|
});
|
||||||
|
this.lastName = new DynamicInputModel({
|
||||||
|
id: 'lastName',
|
||||||
|
label: this.translateService.instant(`${this.messagePrefix}.lastName`),
|
||||||
|
name: 'lastName',
|
||||||
|
validators: {
|
||||||
|
required: null,
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
this.email = new DynamicInputModel({
|
||||||
|
id: 'email',
|
||||||
|
label: this.translateService.instant(`${this.messagePrefix}.email`),
|
||||||
|
name: 'email',
|
||||||
|
validators: {
|
||||||
|
required: null,
|
||||||
|
pattern: '^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,4}$',
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
errorMessages: {
|
||||||
|
emailTaken: 'error.validation.emailTaken',
|
||||||
|
pattern: 'error.validation.NotValidEmail',
|
||||||
|
},
|
||||||
|
hint: this.translateService.instant(`${this.messagePrefix}.emailHint`),
|
||||||
|
});
|
||||||
|
this.canLogIn = new DynamicCheckboxModel(
|
||||||
|
{
|
||||||
|
id: 'canLogIn',
|
||||||
|
label: this.translateService.instant(`${this.messagePrefix}.canLogIn`),
|
||||||
|
name: 'canLogIn',
|
||||||
|
value: (this.epersonInitial != null ? this.epersonInitial.canLogIn : true),
|
||||||
|
});
|
||||||
|
this.requireCertificate = new DynamicCheckboxModel(
|
||||||
|
{
|
||||||
|
id: 'requireCertificate',
|
||||||
|
label: this.translateService.instant(`${this.messagePrefix}.requireCertificate`),
|
||||||
|
name: 'requireCertificate',
|
||||||
|
value: (this.epersonInitial != null ? this.epersonInitial.requireCertificate : false),
|
||||||
|
});
|
||||||
|
this.formModel = [
|
||||||
|
this.firstName,
|
||||||
|
this.lastName,
|
||||||
|
this.email,
|
||||||
|
this.canLogIn,
|
||||||
|
this.requireCertificate,
|
||||||
|
];
|
||||||
|
this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
|
||||||
|
this.subs.push(this.activeEPerson$.subscribe((eperson: EPerson) => {
|
||||||
|
if (eperson != null) {
|
||||||
|
this.groups$ = this.groupsDataService.findListByHref(eperson._links.groups.href, {
|
||||||
|
currentPage: 1,
|
||||||
|
elementsPerPage: this.config.pageSize,
|
||||||
|
}, undefined, undefined, followLink('object'));
|
||||||
|
}
|
||||||
|
this.formGroup.patchValue({
|
||||||
|
firstName: eperson != null ? eperson.firstMetadataValue('eperson.firstname') : '',
|
||||||
|
lastName: eperson != null ? eperson.firstMetadataValue('eperson.lastname') : '',
|
||||||
|
email: eperson != null ? eperson.email : '',
|
||||||
|
canLogIn: eperson != null ? eperson.canLogIn : true,
|
||||||
|
requireCertificate: eperson != null ? eperson.requireCertificate : false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (eperson === null && !!this.formGroup.controls.email) {
|
||||||
|
this.formGroup.controls.email.setAsyncValidators(ValidateEmailNotTaken.createValidator(this.epersonService));
|
||||||
|
this.emailValueChangeSubscribe = this.email.valueChanges.pipe(debounceTime(300)).subscribe(() => {
|
||||||
|
this.changeDetectorRef.detectChanges();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.groups$ = this.activeEPerson$.pipe(
|
||||||
|
switchMap((eperson) => {
|
||||||
|
return observableCombineLatest([observableOf(eperson), this.paginationService.getFindListOptions(this.config.id, {
|
||||||
|
currentPage: 1,
|
||||||
|
elementsPerPage: this.config.pageSize,
|
||||||
|
})]);
|
||||||
|
}),
|
||||||
|
switchMap(([eperson, findListOptions]) => {
|
||||||
|
if (eperson != null) {
|
||||||
|
return this.groupsDataService.findListByHref(eperson._links.groups.href, findListOptions, true, true, followLink('object'));
|
||||||
|
}
|
||||||
|
return observableOf(undefined);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.groupsPageInfoState$ = this.groups$.pipe(
|
||||||
|
map(groupsRD => groupsRD.payload.pageInfo),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.canImpersonate$ = this.activeEPerson$.pipe(
|
||||||
|
switchMap((eperson) => {
|
||||||
|
if (hasValue(eperson)) {
|
||||||
|
return this.authorizationService.isAuthorized(FeatureID.LoginOnBehalfOf, eperson.self);
|
||||||
|
} else {
|
||||||
|
return observableOf(false);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
this.canDelete$ = this.activeEPerson$.pipe(
|
||||||
|
switchMap((eperson) => this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(eperson) ? eperson.self : undefined)),
|
||||||
|
);
|
||||||
|
this.canReset$ = observableOf(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -414,7 +411,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
* Emit the updated/created eperson using the EventEmitter submitForm
|
* Emit the updated/created eperson using the EventEmitter submitForm
|
||||||
*/
|
*/
|
||||||
onSubmit() {
|
onSubmit() {
|
||||||
this.epersonService.getActiveEPerson().pipe(take(1)).subscribe(
|
this.activeEPerson$.pipe(take(1)).subscribe(
|
||||||
(ePerson: EPerson) => {
|
(ePerson: EPerson) => {
|
||||||
const values = {
|
const values = {
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -533,7 +530,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
* It'll either show a success or error message depending on whether the delete was successful or not.
|
* It'll either show a success or error message depending on whether the delete was successful or not.
|
||||||
*/
|
*/
|
||||||
delete(): void {
|
delete(): void {
|
||||||
this.epersonService.getActiveEPerson().pipe(
|
this.activeEPerson$.pipe(
|
||||||
take(1),
|
take(1),
|
||||||
switchMap((eperson: EPerson) => {
|
switchMap((eperson: EPerson) => {
|
||||||
const modalRef = this.modalService.open(ConfirmationModalComponent);
|
const modalRef = this.modalService.open(ConfirmationModalComponent);
|
||||||
@@ -637,7 +634,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
* Update the list of groups by fetching it from the rest api or cache
|
* Update the list of groups by fetching it from the rest api or cache
|
||||||
*/
|
*/
|
||||||
private updateGroups(options) {
|
private updateGroups(options) {
|
||||||
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
|
this.subs.push(this.activeEPerson$.subscribe((eperson: EPerson) => {
|
||||||
this.groups$ = this.groupsDataService.findListByHref(eperson._links.groups.href, options);
|
this.groups$ = this.groupsDataService.findListByHref(eperson._links.groups.href, options);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
<div class="group-form row">
|
<div class="group-form row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
|
|
||||||
<div *ngIf="groupDataService.getActiveGroup() | async; then editHeader; else createHeader"></div>
|
<div *ngIf="activeGroup$ | async; then editHeader; else createHeader"></div>
|
||||||
|
|
||||||
<ng-template #createHeader>
|
<ng-template #createHeader>
|
||||||
<h1 class="border-bottom pb-2">{{messagePrefix + '.head.create' | translate}}</h1>
|
<h1 class="border-bottom pb-2">{{messagePrefix + '.head.create' | translate}}</h1>
|
||||||
@@ -23,11 +23,15 @@
|
|||||||
</h1>
|
</h1>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ds-alert *ngIf="groupBeingEdited?.permanent" [type]="AlertTypeEnum.Warning"
|
<ng-container *ngIf="(activeGroup$ | async) as groupBeingEdited">
|
||||||
[content]="messagePrefix + '.alert.permanent'"></ds-alert>
|
<ds-alert *ngIf="groupBeingEdited?.permanent" [type]="AlertType.Warning"
|
||||||
<ds-alert *ngIf="(canEdit$ | async) !== true && (groupDataService.getActiveGroup() | async)" [type]="AlertTypeEnum.Warning"
|
[content]="messagePrefix + '.alert.permanent'"></ds-alert>
|
||||||
[content]="(messagePrefix + '.alert.workflowGroup' | translate:{ name: dsoNameService.getName((getLinkedDSO(groupBeingEdited) | async)?.payload), comcol: (getLinkedDSO(groupBeingEdited) | async)?.payload?.type, comcolEditRolesRoute: (getLinkedEditRolesRoute(groupBeingEdited) | async) })">
|
<ng-container *ngIf="(activeGroupLinkedDSO$ | async) as activeGroupLinkedDSO">
|
||||||
</ds-alert>
|
<ds-alert *ngIf="(canEdit$ | async) !== true" [type]="AlertType.Warning"
|
||||||
|
[content]="(messagePrefix + '.alert.workflowGroup' | translate:{ name: dsoNameService.getName(activeGroupLinkedDSO), comcol: activeGroupLinkedDSO.type, comcolEditRolesRoute: (linkedEditRolesRoute$ | async) })">
|
||||||
|
</ds-alert>
|
||||||
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
<ds-form [formId]="formId"
|
<ds-form [formId]="formId"
|
||||||
[formModel]="formModel"
|
[formModel]="formModel"
|
||||||
@@ -39,22 +43,21 @@
|
|||||||
<button (click)="onCancel()" type="button"
|
<button (click)="onCancel()" type="button"
|
||||||
class="btn btn-outline-secondary"><i class="fas fa-arrow-left"></i> {{messagePrefix + '.return' | translate}}</button>
|
class="btn btn-outline-secondary"><i class="fas fa-arrow-left"></i> {{messagePrefix + '.return' | translate}}</button>
|
||||||
</div>
|
</div>
|
||||||
<div after *ngIf="(canEdit$ | async) && !groupBeingEdited?.permanent" class="btn-group">
|
<div after *ngIf="(canEdit$ | async) && !(activeGroup$ | async)?.permanent" class="btn-group">
|
||||||
<button (click)="delete()" class="btn btn-danger delete-button" type="button">
|
<button (click)="delete()" class="btn btn-danger delete-button" type="button">
|
||||||
<i class="fa fa-trash"></i> {{ messagePrefix + '.actions.delete' | translate}}
|
<i class="fa fa-trash"></i> {{ messagePrefix + '.actions.delete' | translate}}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</ds-form>
|
</ds-form>
|
||||||
|
|
||||||
<div class="mb-5">
|
<ng-container *ngIf="(activeGroup$ | async) as groupBeingEdited">
|
||||||
<ds-members-list *ngIf="groupBeingEdited !== undefined"
|
<div class="mb-5">
|
||||||
[messagePrefix]="messagePrefix + '.members-list'"></ds-members-list>
|
<ds-members-list *ngIf="groupBeingEdited !== undefined"
|
||||||
</div>
|
[messagePrefix]="messagePrefix + '.members-list'"></ds-members-list>
|
||||||
<ds-subgroups-list *ngIf="groupBeingEdited !== undefined"
|
</div>
|
||||||
[messagePrefix]="messagePrefix + '.subgroups-list'"></ds-subgroups-list>
|
<ds-subgroups-list *ngIf="groupBeingEdited !== undefined"
|
||||||
|
[messagePrefix]="messagePrefix + '.subgroups-list'"></ds-subgroups-list>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
import {
|
import {
|
||||||
ComponentFixture,
|
ComponentFixture,
|
||||||
TestBed,
|
TestBed,
|
||||||
@@ -23,11 +23,7 @@ import {
|
|||||||
} from '@angular/router';
|
} from '@angular/router';
|
||||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import {
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
TranslateLoader,
|
|
||||||
TranslateModule,
|
|
||||||
TranslateService,
|
|
||||||
} from '@ngx-translate/core';
|
|
||||||
import { Operation } from 'fast-json-patch';
|
import { Operation } from 'fast-json-patch';
|
||||||
import {
|
import {
|
||||||
Observable,
|
Observable,
|
||||||
@@ -61,15 +57,14 @@ import { FormComponent } from '../../../shared/form/form.component';
|
|||||||
import { DSONameServiceMock } from '../../../shared/mocks/dso-name.service.mock';
|
import { DSONameServiceMock } from '../../../shared/mocks/dso-name.service.mock';
|
||||||
import { getMockFormBuilderService } from '../../../shared/mocks/form-builder-service.mock';
|
import { getMockFormBuilderService } from '../../../shared/mocks/form-builder-service.mock';
|
||||||
import { RouterMock } from '../../../shared/mocks/router.mock';
|
import { RouterMock } from '../../../shared/mocks/router.mock';
|
||||||
import { getMockTranslateService } from '../../../shared/mocks/translate.service.mock';
|
|
||||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||||
|
import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub';
|
||||||
import {
|
import {
|
||||||
GroupMock,
|
GroupMock,
|
||||||
GroupMock2,
|
GroupMock2,
|
||||||
} from '../../../shared/testing/group-mock';
|
} from '../../../shared/testing/group-mock';
|
||||||
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
|
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
|
||||||
import { TranslateLoaderMock } from '../../../shared/testing/translate-loader.mock';
|
|
||||||
import { GroupFormComponent } from './group-form.component';
|
import { GroupFormComponent } from './group-form.component';
|
||||||
import { MembersListComponent } from './members-list/members-list.component';
|
import { MembersListComponent } from './members-list/members-list.component';
|
||||||
import { SubgroupsListComponent } from './subgroup-list/subgroups-list.component';
|
import { SubgroupsListComponent } from './subgroup-list/subgroups-list.component';
|
||||||
@@ -78,19 +73,19 @@ import { ValidateGroupExists } from './validators/group-exists.validator';
|
|||||||
describe('GroupFormComponent', () => {
|
describe('GroupFormComponent', () => {
|
||||||
let component: GroupFormComponent;
|
let component: GroupFormComponent;
|
||||||
let fixture: ComponentFixture<GroupFormComponent>;
|
let fixture: ComponentFixture<GroupFormComponent>;
|
||||||
let translateService: TranslateService;
|
|
||||||
let builderService: FormBuilderService;
|
let builderService: FormBuilderService;
|
||||||
let ePersonDataServiceStub: any;
|
let ePersonDataServiceStub: any;
|
||||||
let groupsDataServiceStub: any;
|
let groupsDataServiceStub: any;
|
||||||
let dsoDataServiceStub: any;
|
let dsoDataServiceStub: any;
|
||||||
let authorizationService: AuthorizationDataService;
|
let authorizationService: AuthorizationDataService;
|
||||||
let notificationService: NotificationsServiceStub;
|
let notificationService: NotificationsServiceStub;
|
||||||
let router;
|
let router: RouterMock;
|
||||||
|
let route: ActivatedRouteStub;
|
||||||
|
|
||||||
let groups;
|
let groups: Group[];
|
||||||
let groupName;
|
let groupName: string;
|
||||||
let groupDescription;
|
let groupDescription: string;
|
||||||
let expected;
|
let expected: Group;
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
groups = [GroupMock, GroupMock2];
|
groups = [GroupMock, GroupMock2];
|
||||||
@@ -105,6 +100,15 @@ describe('GroupFormComponent', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
object: createSuccessfulRemoteDataObject$(undefined),
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: 'group-selflink',
|
||||||
|
},
|
||||||
|
object: {
|
||||||
|
href: 'group-objectlink',
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
ePersonDataServiceStub = {};
|
ePersonDataServiceStub = {};
|
||||||
groupsDataServiceStub = {
|
groupsDataServiceStub = {
|
||||||
@@ -141,7 +145,14 @@ describe('GroupFormComponent', () => {
|
|||||||
create(group: Group): Observable<RemoteData<Group>> {
|
create(group: Group): Observable<RemoteData<Group>> {
|
||||||
this.allGroups = [...this.allGroups, group];
|
this.allGroups = [...this.allGroups, group];
|
||||||
this.createdGroup = Object.assign({}, group, {
|
this.createdGroup = Object.assign({}, group, {
|
||||||
_links: { self: { href: 'group-selflink' } },
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: 'group-selflink',
|
||||||
|
},
|
||||||
|
object: {
|
||||||
|
href: 'group-objectlink',
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
return createSuccessfulRemoteDataObject$(this.createdGroup);
|
return createSuccessfulRemoteDataObject$(this.createdGroup);
|
||||||
},
|
},
|
||||||
@@ -223,17 +234,15 @@ describe('GroupFormComponent', () => {
|
|||||||
return typeof value === 'object' && value !== null;
|
return typeof value === 'object' && value !== null;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
translateService = getMockTranslateService();
|
|
||||||
router = new RouterMock();
|
router = new RouterMock();
|
||||||
|
route = new ActivatedRouteStub();
|
||||||
notificationService = new NotificationsServiceStub();
|
notificationService = new NotificationsServiceStub();
|
||||||
|
|
||||||
return TestBed.configureTestingModule({
|
return TestBed.configureTestingModule({
|
||||||
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
||||||
TranslateModule.forRoot({
|
TranslateModule.forRoot(),
|
||||||
loader: {
|
GroupFormComponent,
|
||||||
provide: TranslateLoader,
|
],
|
||||||
useClass: TranslateLoaderMock,
|
|
||||||
},
|
|
||||||
}), GroupFormComponent],
|
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: DSONameService, useValue: new DSONameServiceMock() },
|
{ provide: DSONameService, useValue: new DSONameServiceMock() },
|
||||||
{ provide: EPersonDataService, useValue: ePersonDataServiceStub },
|
{ provide: EPersonDataService, useValue: ePersonDataServiceStub },
|
||||||
@@ -249,14 +258,11 @@ describe('GroupFormComponent', () => {
|
|||||||
{ provide: Store, useValue: {} },
|
{ provide: Store, useValue: {} },
|
||||||
{ provide: RemoteDataBuildService, useValue: {} },
|
{ provide: RemoteDataBuildService, useValue: {} },
|
||||||
{ provide: HALEndpointService, useValue: {} },
|
{ provide: HALEndpointService, useValue: {} },
|
||||||
{
|
{ provide: ActivatedRoute, useValue: route },
|
||||||
provide: ActivatedRoute,
|
|
||||||
useValue: { data: observableOf({ dso: { payload: {} } }), params: observableOf({}) },
|
|
||||||
},
|
|
||||||
{ provide: Router, useValue: router },
|
{ provide: Router, useValue: router },
|
||||||
{ provide: AuthorizationDataService, useValue: authorizationService },
|
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA],
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||||
})
|
})
|
||||||
.overrideComponent(GroupFormComponent, {
|
.overrideComponent(GroupFormComponent, {
|
||||||
remove: { imports: [
|
remove: { imports: [
|
||||||
@@ -279,8 +285,8 @@ describe('GroupFormComponent', () => {
|
|||||||
describe('when submitting the form', () => {
|
describe('when submitting the form', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(component.submitForm, 'emit');
|
spyOn(component.submitForm, 'emit');
|
||||||
component.groupName.value = groupName;
|
component.groupName.setValue(groupName);
|
||||||
component.groupDescription.value = groupDescription;
|
component.groupDescription.setValue(groupDescription);
|
||||||
});
|
});
|
||||||
describe('without active Group', () => {
|
describe('without active Group', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -288,14 +294,22 @@ describe('GroupFormComponent', () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should emit a new group using the correct values', (async () => {
|
it('should emit a new group using the correct values', (() => {
|
||||||
await fixture.whenStable().then(() => {
|
expect(component.submitForm.emit).toHaveBeenCalledWith(jasmine.objectContaining({
|
||||||
expect(component.submitForm.emit).toHaveBeenCalledWith(expected);
|
name: groupName,
|
||||||
});
|
metadata: {
|
||||||
|
'dc.description': [
|
||||||
|
{
|
||||||
|
value: groupDescription,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}));
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('with active Group', () => {
|
describe('with active Group', () => {
|
||||||
let expected2;
|
let expected2: Group;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
expected2 = Object.assign(new Group(), {
|
expected2 = Object.assign(new Group(), {
|
||||||
name: 'newGroupName',
|
name: 'newGroupName',
|
||||||
@@ -306,15 +320,24 @@ describe('GroupFormComponent', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
object: createSuccessfulRemoteDataObject$(undefined),
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: 'group-selflink',
|
||||||
|
},
|
||||||
|
object: {
|
||||||
|
href: 'group-objectlink',
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
spyOn(groupsDataServiceStub, 'getActiveGroup').and.returnValue(observableOf(expected));
|
spyOn(groupsDataServiceStub, 'getActiveGroup').and.returnValue(observableOf(expected));
|
||||||
spyOn(groupsDataServiceStub, 'patch').and.returnValue(createSuccessfulRemoteDataObject$(expected2));
|
spyOn(groupsDataServiceStub, 'patch').and.returnValue(createSuccessfulRemoteDataObject$(expected2));
|
||||||
component.groupName.value = 'newGroupName';
|
component.ngOnInit();
|
||||||
component.onSubmit();
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should edit with name and description operations', () => {
|
it('should edit with name and description operations', () => {
|
||||||
|
component.groupName.setValue('newGroupName');
|
||||||
|
component.onSubmit();
|
||||||
const operations = [{
|
const operations = [{
|
||||||
op: 'add',
|
op: 'add',
|
||||||
path: '/metadata/dc.description',
|
path: '/metadata/dc.description',
|
||||||
@@ -328,9 +351,8 @@ describe('GroupFormComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should edit with description operations', () => {
|
it('should edit with description operations', () => {
|
||||||
component.groupName.value = null;
|
component.groupName.setValue(null);
|
||||||
component.onSubmit();
|
component.onSubmit();
|
||||||
fixture.detectChanges();
|
|
||||||
const operations = [{
|
const operations = [{
|
||||||
op: 'add',
|
op: 'add',
|
||||||
path: '/metadata/dc.description',
|
path: '/metadata/dc.description',
|
||||||
@@ -340,9 +362,9 @@ describe('GroupFormComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should edit with name operations', () => {
|
it('should edit with name operations', () => {
|
||||||
component.groupDescription.value = null;
|
component.groupName.setValue('newGroupName');
|
||||||
|
component.groupDescription.setValue(null);
|
||||||
component.onSubmit();
|
component.onSubmit();
|
||||||
fixture.detectChanges();
|
|
||||||
const operations = [{
|
const operations = [{
|
||||||
op: 'replace',
|
op: 'replace',
|
||||||
path: '/name',
|
path: '/name',
|
||||||
@@ -351,12 +373,13 @@ describe('GroupFormComponent', () => {
|
|||||||
expect(groupsDataServiceStub.patch).toHaveBeenCalledWith(expected, operations);
|
expect(groupsDataServiceStub.patch).toHaveBeenCalledWith(expected, operations);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should emit the existing group using the correct new values', (async () => {
|
it('should emit the existing group using the correct new values', () => {
|
||||||
await fixture.whenStable().then(() => {
|
component.onSubmit();
|
||||||
expect(component.submitForm.emit).toHaveBeenCalledWith(expected2);
|
expect(component.submitForm.emit).toHaveBeenCalledWith(expected2);
|
||||||
});
|
});
|
||||||
}));
|
|
||||||
it('should emit success notification', () => {
|
it('should emit success notification', () => {
|
||||||
|
component.onSubmit();
|
||||||
expect(notificationService.success).toHaveBeenCalled();
|
expect(notificationService.success).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -371,11 +394,8 @@ describe('GroupFormComponent', () => {
|
|||||||
|
|
||||||
|
|
||||||
describe('check form validation', () => {
|
describe('check form validation', () => {
|
||||||
let groupCommunity;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
groupName = 'testName';
|
groupName = 'testName';
|
||||||
groupCommunity = 'testgroupCommunity';
|
|
||||||
groupDescription = 'testgroupDescription';
|
groupDescription = 'testgroupDescription';
|
||||||
|
|
||||||
expected = Object.assign(new Group(), {
|
expected = Object.assign(new Group(), {
|
||||||
@@ -387,8 +407,17 @@ describe('GroupFormComponent', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: 'group-selflink',
|
||||||
|
},
|
||||||
|
object: {
|
||||||
|
href: 'group-objectlink',
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
spyOn(component.submitForm, 'emit');
|
spyOn(component.submitForm, 'emit');
|
||||||
|
spyOn(dsoDataServiceStub, 'findByHref').and.returnValue(observableOf(expected));
|
||||||
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
component.initialisePage();
|
component.initialisePage();
|
||||||
@@ -438,21 +467,20 @@ describe('GroupFormComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('delete', () => {
|
describe('delete', () => {
|
||||||
let deleteButton;
|
let deleteButton: HTMLButtonElement;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
component.initialisePage();
|
spyOn(groupsDataServiceStub, 'delete').and.callThrough();
|
||||||
|
component.activeGroup$ = observableOf({
|
||||||
component.canEdit$ = observableOf(true);
|
id: 'active-group',
|
||||||
component.groupBeingEdited = {
|
|
||||||
permanent: false,
|
permanent: false,
|
||||||
} as Group;
|
} as Group);
|
||||||
|
component.canEdit$ = observableOf(true);
|
||||||
|
|
||||||
|
component.initialisePage();
|
||||||
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
deleteButton = fixture.debugElement.query(By.css('.delete-button')).nativeElement;
|
deleteButton = fixture.debugElement.query(By.css('.delete-button')).nativeElement;
|
||||||
|
|
||||||
spyOn(groupsDataServiceStub, 'delete').and.callThrough();
|
|
||||||
spyOn(groupsDataServiceStub, 'getActiveGroup').and.returnValue(observableOf({ id: 'active-group' }));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('if confirmed via modal', () => {
|
describe('if confirmed via modal', () => {
|
||||||
|
@@ -11,7 +11,10 @@ import {
|
|||||||
OnInit,
|
OnInit,
|
||||||
Output,
|
Output,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { UntypedFormGroup } from '@angular/forms';
|
import {
|
||||||
|
AbstractControl,
|
||||||
|
UntypedFormGroup,
|
||||||
|
} from '@angular/forms';
|
||||||
import {
|
import {
|
||||||
ActivatedRoute,
|
ActivatedRoute,
|
||||||
Router,
|
Router,
|
||||||
@@ -31,13 +34,10 @@ import { Operation } from 'fast-json-patch';
|
|||||||
import {
|
import {
|
||||||
combineLatest as observableCombineLatest,
|
combineLatest as observableCombineLatest,
|
||||||
Observable,
|
Observable,
|
||||||
of as observableOf,
|
|
||||||
Subscription,
|
Subscription,
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import {
|
import {
|
||||||
catchError,
|
|
||||||
debounceTime,
|
debounceTime,
|
||||||
filter,
|
|
||||||
map,
|
map,
|
||||||
switchMap,
|
switchMap,
|
||||||
take,
|
take,
|
||||||
@@ -53,7 +53,6 @@ import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
|
|||||||
import { PaginatedList } from '../../../core/data/paginated-list.model';
|
import { PaginatedList } from '../../../core/data/paginated-list.model';
|
||||||
import { RemoteData } from '../../../core/data/remote-data';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
import { RequestService } from '../../../core/data/request.service';
|
import { RequestService } from '../../../core/data/request.service';
|
||||||
import { EPersonDataService } from '../../../core/eperson/eperson-data.service';
|
|
||||||
import { GroupDataService } from '../../../core/eperson/group-data.service';
|
import { GroupDataService } from '../../../core/eperson/group-data.service';
|
||||||
import { Group } from '../../../core/eperson/models/group.model';
|
import { Group } from '../../../core/eperson/models/group.model';
|
||||||
import { Collection } from '../../../core/shared/collection.model';
|
import { Collection } from '../../../core/shared/collection.model';
|
||||||
@@ -61,9 +60,9 @@ import { Community } from '../../../core/shared/community.model';
|
|||||||
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||||
import { NoContent } from '../../../core/shared/NoContent.model';
|
import { NoContent } from '../../../core/shared/NoContent.model';
|
||||||
import {
|
import {
|
||||||
|
getAllCompletedRemoteData,
|
||||||
getFirstCompletedRemoteData,
|
getFirstCompletedRemoteData,
|
||||||
getFirstSucceededRemoteData,
|
getFirstSucceededRemoteData,
|
||||||
getFirstSucceededRemoteDataPayload,
|
|
||||||
getRemoteDataPayload,
|
getRemoteDataPayload,
|
||||||
} from '../../../core/shared/operators';
|
} from '../../../core/shared/operators';
|
||||||
import { AlertComponent } from '../../../shared/alert/alert.component';
|
import { AlertComponent } from '../../../shared/alert/alert.component';
|
||||||
@@ -117,9 +116,9 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
/**
|
/**
|
||||||
* Dynamic models for the inputs of form
|
* Dynamic models for the inputs of form
|
||||||
*/
|
*/
|
||||||
groupName: DynamicInputModel;
|
groupName: AbstractControl;
|
||||||
groupCommunity: DynamicInputModel;
|
groupCommunity: AbstractControl;
|
||||||
groupDescription: DynamicTextAreaModel;
|
groupDescription: AbstractControl;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A list of all dynamic input models
|
* A list of all dynamic input models
|
||||||
@@ -162,21 +161,30 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
subs: Subscription[] = [];
|
subs: Subscription[] = [];
|
||||||
|
|
||||||
/**
|
|
||||||
* Group currently being edited
|
|
||||||
*/
|
|
||||||
groupBeingEdited: Group;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Observable whether or not the logged in user is allowed to delete the Group & doesn't have a linked object (community / collection linked to workspace group
|
* Observable whether or not the logged in user is allowed to delete the Group & doesn't have a linked object (community / collection linked to workspace group
|
||||||
*/
|
*/
|
||||||
canEdit$: Observable<boolean>;
|
canEdit$: Observable<boolean>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The AlertType enumeration
|
* The current {@link Group}
|
||||||
* @type {AlertType}
|
|
||||||
*/
|
*/
|
||||||
public AlertTypeEnum = AlertType;
|
activeGroup$: Observable<Group>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current {@link Group}'s linked {@link Community}/{@link Collection}
|
||||||
|
*/
|
||||||
|
activeGroupLinkedDSO$: Observable<DSpaceObject>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Link to the current {@link Group}'s {@link Community}/{@link Collection} edit role tab
|
||||||
|
*/
|
||||||
|
linkedEditRolesRoute$: Observable<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The AlertType enumeration
|
||||||
|
*/
|
||||||
|
public readonly AlertType = AlertType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscription to email field value change
|
* Subscription to email field value change
|
||||||
@@ -186,126 +194,121 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public groupDataService: GroupDataService,
|
public groupDataService: GroupDataService,
|
||||||
private ePersonDataService: EPersonDataService,
|
protected dSpaceObjectDataService: DSpaceObjectDataService,
|
||||||
private dSpaceObjectDataService: DSpaceObjectDataService,
|
protected formBuilderService: FormBuilderService,
|
||||||
private formBuilderService: FormBuilderService,
|
protected translateService: TranslateService,
|
||||||
private translateService: TranslateService,
|
protected notificationsService: NotificationsService,
|
||||||
private notificationsService: NotificationsService,
|
protected route: ActivatedRoute,
|
||||||
private route: ActivatedRoute,
|
|
||||||
protected router: Router,
|
protected router: Router,
|
||||||
private authorizationService: AuthorizationDataService,
|
protected authorizationService: AuthorizationDataService,
|
||||||
private modalService: NgbModal,
|
protected modalService: NgbModal,
|
||||||
public requestService: RequestService,
|
public requestService: RequestService,
|
||||||
protected changeDetectorRef: ChangeDetectorRef,
|
protected changeDetectorRef: ChangeDetectorRef,
|
||||||
public dsoNameService: DSONameService,
|
public dsoNameService: DSONameService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit(): void {
|
||||||
|
if (this.route.snapshot.params.groupId !== 'newGroup') {
|
||||||
|
this.setActiveGroup(this.route.snapshot.params.groupId);
|
||||||
|
}
|
||||||
|
this.activeGroup$ = this.groupDataService.getActiveGroup();
|
||||||
|
this.activeGroupLinkedDSO$ = this.getActiveGroupLinkedDSO();
|
||||||
|
this.linkedEditRolesRoute$ = this.getLinkedEditRolesRoute();
|
||||||
|
this.canEdit$ = this.activeGroupLinkedDSO$.pipe(
|
||||||
|
switchMap((dso: DSpaceObject) => {
|
||||||
|
if (hasValue(dso)) {
|
||||||
|
return [false];
|
||||||
|
} else {
|
||||||
|
return this.activeGroup$.pipe(
|
||||||
|
hasValueOperator(),
|
||||||
|
switchMap((group: Group) => this.authorizationService.isAuthorized(FeatureID.CanDelete, group.self)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
this.initialisePage();
|
this.initialisePage();
|
||||||
}
|
}
|
||||||
|
|
||||||
initialisePage() {
|
initialisePage() {
|
||||||
this.subs.push(this.route.params.subscribe((params) => {
|
const groupNameModel = new DynamicInputModel({
|
||||||
if (params.groupId !== 'newGroup') {
|
id: 'groupName',
|
||||||
this.setActiveGroup(params.groupId);
|
label: this.translateService.instant(`${this.messagePrefix}.groupName`),
|
||||||
}
|
name: 'groupName',
|
||||||
}));
|
validators: {
|
||||||
this.canEdit$ = this.groupDataService.getActiveGroup().pipe(
|
required: null,
|
||||||
hasValueOperator(),
|
},
|
||||||
switchMap((group: Group) => {
|
required: true,
|
||||||
return observableCombineLatest([
|
});
|
||||||
this.authorizationService.isAuthorized(FeatureID.CanDelete, isNotEmpty(group) ? group.self : undefined),
|
const groupCommunityModel = new DynamicInputModel({
|
||||||
this.hasLinkedDSO(group),
|
id: 'groupCommunity',
|
||||||
]).pipe(
|
label: this.translateService.instant(`${this.messagePrefix}.groupCommunity`),
|
||||||
map(([isAuthorized, hasLinkedDSO]: [boolean, boolean]) => isAuthorized && !hasLinkedDSO),
|
name: 'groupCommunity',
|
||||||
);
|
required: false,
|
||||||
|
readOnly: true,
|
||||||
|
});
|
||||||
|
const groupDescriptionModel = new DynamicTextAreaModel({
|
||||||
|
id: 'groupDescription',
|
||||||
|
label: this.translateService.instant(`${this.messagePrefix}.groupDescription`),
|
||||||
|
name: 'groupDescription',
|
||||||
|
required: false,
|
||||||
|
spellCheck: environment.form.spellCheck,
|
||||||
|
});
|
||||||
|
this.formModel = [
|
||||||
|
groupNameModel,
|
||||||
|
groupDescriptionModel,
|
||||||
|
];
|
||||||
|
this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
|
||||||
|
this.groupName = this.formGroup.get('groupName');
|
||||||
|
this.groupDescription = this.formGroup.get('groupDescription');
|
||||||
|
|
||||||
|
if (hasValue(this.groupName)) {
|
||||||
|
this.groupName.setAsyncValidators(ValidateGroupExists.createValidator(this.groupDataService));
|
||||||
|
this.groupNameValueChangeSubscribe = this.groupName.valueChanges.pipe(debounceTime(300)).subscribe(() => {
|
||||||
|
this.changeDetectorRef.detectChanges();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.subs.push(
|
||||||
|
observableCombineLatest([
|
||||||
|
this.activeGroup$,
|
||||||
|
this.canEdit$,
|
||||||
|
this.activeGroupLinkedDSO$,
|
||||||
|
]).subscribe(([activeGroup, canEdit, linkedObject]) => {
|
||||||
|
|
||||||
|
if (activeGroup != null) {
|
||||||
|
|
||||||
|
// Disable group name exists validator
|
||||||
|
this.formGroup.controls.groupName.clearAsyncValidators();
|
||||||
|
|
||||||
|
if (isNotEmpty(linkedObject?.name)) {
|
||||||
|
if (!this.formGroup.controls.groupCommunity) {
|
||||||
|
this.formBuilderService.insertFormGroupControl(1, this.formGroup, this.formModel, groupCommunityModel);
|
||||||
|
this.groupDescription = this.formGroup.get('groupCommunity');
|
||||||
|
}
|
||||||
|
this.formGroup.patchValue({
|
||||||
|
groupName: activeGroup.name,
|
||||||
|
groupCommunity: linkedObject?.name ?? '',
|
||||||
|
groupDescription: activeGroup.firstMetadataValue('dc.description'),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.formModel = [
|
||||||
|
groupNameModel,
|
||||||
|
groupDescriptionModel,
|
||||||
|
];
|
||||||
|
this.formGroup.patchValue({
|
||||||
|
groupName: activeGroup.name,
|
||||||
|
groupDescription: activeGroup.firstMetadataValue('dc.description'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!canEdit || activeGroup.permanent) {
|
||||||
|
this.formGroup.disable();
|
||||||
|
} else {
|
||||||
|
this.formGroup.enable();
|
||||||
|
}
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
observableCombineLatest([
|
|
||||||
this.translateService.get(`${this.messagePrefix}.groupName`),
|
|
||||||
this.translateService.get(`${this.messagePrefix}.groupCommunity`),
|
|
||||||
this.translateService.get(`${this.messagePrefix}.groupDescription`),
|
|
||||||
]).subscribe(([groupName, groupCommunity, groupDescription]) => {
|
|
||||||
this.groupName = new DynamicInputModel({
|
|
||||||
id: 'groupName',
|
|
||||||
label: groupName,
|
|
||||||
name: 'groupName',
|
|
||||||
validators: {
|
|
||||||
required: null,
|
|
||||||
},
|
|
||||||
required: true,
|
|
||||||
});
|
|
||||||
this.groupCommunity = new DynamicInputModel({
|
|
||||||
id: 'groupCommunity',
|
|
||||||
label: groupCommunity,
|
|
||||||
name: 'groupCommunity',
|
|
||||||
required: false,
|
|
||||||
readOnly: true,
|
|
||||||
});
|
|
||||||
this.groupDescription = new DynamicTextAreaModel({
|
|
||||||
id: 'groupDescription',
|
|
||||||
label: groupDescription,
|
|
||||||
name: 'groupDescription',
|
|
||||||
required: false,
|
|
||||||
spellCheck: environment.form.spellCheck,
|
|
||||||
});
|
|
||||||
this.formModel = [
|
|
||||||
this.groupName,
|
|
||||||
this.groupDescription,
|
|
||||||
];
|
|
||||||
this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
|
|
||||||
|
|
||||||
if (this.formGroup.controls.groupName) {
|
|
||||||
this.formGroup.controls.groupName.setAsyncValidators(ValidateGroupExists.createValidator(this.groupDataService));
|
|
||||||
this.groupNameValueChangeSubscribe = this.groupName.valueChanges.pipe(debounceTime(300)).subscribe(() => {
|
|
||||||
this.changeDetectorRef.detectChanges();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.subs.push(
|
|
||||||
observableCombineLatest([
|
|
||||||
this.groupDataService.getActiveGroup(),
|
|
||||||
this.canEdit$,
|
|
||||||
this.groupDataService.getActiveGroup()
|
|
||||||
.pipe(filter((activeGroup) => hasValue(activeGroup)),switchMap((activeGroup) => this.getLinkedDSO(activeGroup).pipe(getFirstSucceededRemoteDataPayload()))),
|
|
||||||
]).subscribe(([activeGroup, canEdit, linkedObject]) => {
|
|
||||||
|
|
||||||
if (activeGroup != null) {
|
|
||||||
|
|
||||||
// Disable group name exists validator
|
|
||||||
this.formGroup.controls.groupName.clearAsyncValidators();
|
|
||||||
|
|
||||||
this.groupBeingEdited = activeGroup;
|
|
||||||
|
|
||||||
if (linkedObject?.name) {
|
|
||||||
if (!this.formGroup.controls.groupCommunity) {
|
|
||||||
this.formBuilderService.insertFormGroupControl(1, this.formGroup, this.formModel, this.groupCommunity);
|
|
||||||
this.formGroup.patchValue({
|
|
||||||
groupName: activeGroup.name,
|
|
||||||
groupCommunity: linkedObject?.name ?? '',
|
|
||||||
groupDescription: activeGroup.firstMetadataValue('dc.description'),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.formModel = [
|
|
||||||
this.groupName,
|
|
||||||
this.groupDescription,
|
|
||||||
];
|
|
||||||
this.formGroup.patchValue({
|
|
||||||
groupName: activeGroup.name,
|
|
||||||
groupDescription: activeGroup.firstMetadataValue('dc.description'),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
setTimeout(() => {
|
|
||||||
if (!canEdit || activeGroup.permanent) {
|
|
||||||
this.formGroup.disable();
|
|
||||||
}
|
|
||||||
}, 200);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -324,9 +327,9 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
* Emit the updated/created eperson using the EventEmitter submitForm
|
* Emit the updated/created eperson using the EventEmitter submitForm
|
||||||
*/
|
*/
|
||||||
onSubmit() {
|
onSubmit() {
|
||||||
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe(
|
this.activeGroup$.pipe(take(1)).subscribe((group: Group) => {
|
||||||
(group: Group) => {
|
if (group === null) {
|
||||||
const values = {
|
this.createNewGroup({
|
||||||
name: this.groupName.value,
|
name: this.groupName.value,
|
||||||
metadata: {
|
metadata: {
|
||||||
'dc.description': [
|
'dc.description': [
|
||||||
@@ -335,14 +338,11 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
if (group === null) {
|
} else {
|
||||||
this.createNewGroup(values);
|
this.editGroup(group);
|
||||||
} else {
|
}
|
||||||
this.editGroup(group);
|
});
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -448,7 +448,7 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
* @param groupSelfLink SelfLink of group to set as active
|
* @param groupSelfLink SelfLink of group to set as active
|
||||||
*/
|
*/
|
||||||
setActiveGroupWithLink(groupSelfLink: string) {
|
setActiveGroupWithLink(groupSelfLink: string) {
|
||||||
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => {
|
this.activeGroup$.pipe(take(1)).subscribe((activeGroup: Group) => {
|
||||||
if (activeGroup === null) {
|
if (activeGroup === null) {
|
||||||
this.groupDataService.cancelEditGroup();
|
this.groupDataService.cancelEditGroup();
|
||||||
this.groupDataService.findByHref(groupSelfLink, false, false, followLink('subgroups'), followLink('epersons'), followLink('object'))
|
this.groupDataService.findByHref(groupSelfLink, false, false, followLink('subgroups'), followLink('epersons'), followLink('object'))
|
||||||
@@ -467,7 +467,7 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
* It'll either show a success or error message depending on whether the delete was successful or not.
|
* It'll either show a success or error message depending on whether the delete was successful or not.
|
||||||
*/
|
*/
|
||||||
delete() {
|
delete() {
|
||||||
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((group: Group) => {
|
this.activeGroup$.pipe(take(1)).subscribe((group: Group) => {
|
||||||
const modalRef = this.modalService.open(ConfirmationModalComponent);
|
const modalRef = this.modalService.open(ConfirmationModalComponent);
|
||||||
modalRef.componentInstance.name = this.dsoNameService.getName(group);
|
modalRef.componentInstance.name = this.dsoNameService.getName(group);
|
||||||
modalRef.componentInstance.headerLabel = this.messagePrefix + '.delete-group.modal.header';
|
modalRef.componentInstance.headerLabel = this.messagePrefix + '.delete-group.modal.header';
|
||||||
@@ -511,52 +511,38 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if group has a linked object (community or collection linked to a workflow group)
|
* Get the active {@link Group}'s linked object if it has one ({@link Community} or {@link Collection} linked to a
|
||||||
* @param group
|
* workflow group)
|
||||||
*/
|
*/
|
||||||
hasLinkedDSO(group: Group): Observable<boolean> {
|
getActiveGroupLinkedDSO(): Observable<DSpaceObject> {
|
||||||
if (hasValue(group) && hasValue(group._links.object.href)) {
|
return this.activeGroup$.pipe(
|
||||||
return this.getLinkedDSO(group).pipe(
|
hasValueOperator(),
|
||||||
map((rd: RemoteData<DSpaceObject>) => {
|
switchMap((group: Group) => {
|
||||||
return hasValue(rd) && hasValue(rd.payload);
|
if (group.object === undefined) {
|
||||||
}),
|
return this.dSpaceObjectDataService.findByHref(group._links.object.href);
|
||||||
catchError(() => observableOf(false)),
|
}
|
||||||
);
|
return group.object;
|
||||||
}
|
}),
|
||||||
|
getAllCompletedRemoteData(),
|
||||||
|
getRemoteDataPayload(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get group's linked object if it has one (community or collection linked to a workflow group)
|
* Get the route to the edit roles tab of the active {@link Group}'s linked object (community or collection linked
|
||||||
* @param group
|
* to a workflow group) if it has one
|
||||||
*/
|
*/
|
||||||
getLinkedDSO(group: Group): Observable<RemoteData<DSpaceObject>> {
|
getLinkedEditRolesRoute(): Observable<string> {
|
||||||
if (hasValue(group) && hasValue(group._links.object.href)) {
|
return this.activeGroupLinkedDSO$.pipe(
|
||||||
if (group.object === undefined) {
|
hasValueOperator(),
|
||||||
return this.dSpaceObjectDataService.findByHref(group._links.object.href);
|
map((dso: DSpaceObject) => {
|
||||||
}
|
switch ((dso as any).type) {
|
||||||
return group.object;
|
case Community.type.value:
|
||||||
}
|
return getCommunityEditRolesRoute(dso.id);
|
||||||
}
|
case Collection.type.value:
|
||||||
|
return getCollectionEditRolesRoute(dso.id);
|
||||||
/**
|
}
|
||||||
* Get the route to the edit roles tab of the group's linked object (community or collection linked to a workflow group) if it has one
|
}),
|
||||||
* @param group
|
);
|
||||||
*/
|
|
||||||
getLinkedEditRolesRoute(group: Group): Observable<string> {
|
|
||||||
if (hasValue(group) && hasValue(group._links.object.href)) {
|
|
||||||
return this.getLinkedDSO(group).pipe(
|
|
||||||
map((rd: RemoteData<DSpaceObject>) => {
|
|
||||||
if (hasValue(rd) && hasValue(rd.payload)) {
|
|
||||||
const dso = rd.payload;
|
|
||||||
switch ((dso as any).type) {
|
|
||||||
case Community.type.value:
|
|
||||||
return getCommunityEditRolesRoute(rd.payload.id);
|
|
||||||
case Collection.type.value:
|
|
||||||
return getCollectionEditRolesRoute(rd.payload.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
* List of services statuses
|
* List of services statuses
|
||||||
*/
|
*/
|
||||||
export enum LdnServiceStatus {
|
export enum LdnServiceStatus {
|
||||||
UNKOWN,
|
UNKNOWN,
|
||||||
DISABLED,
|
DISABLED,
|
||||||
ENABLED,
|
ENABLED,
|
||||||
}
|
}
|
||||||
|
@@ -9,9 +9,9 @@
|
|||||||
|
|
||||||
|
|
||||||
<ds-pagination
|
<ds-pagination
|
||||||
*ngIf="(bitstreamFormats | async)?.payload?.totalElements > 0"
|
*ngIf="(bitstreamFormats$ | async)?.payload?.totalElements > 0"
|
||||||
[paginationOptions]="pageConfig"
|
[paginationOptions]="pageConfig"
|
||||||
[collectionSize]="(bitstreamFormats | async)?.payload?.totalElements"
|
[collectionSize]="(bitstreamFormats$ | async)?.payload?.totalElements"
|
||||||
[hideGear]="false"
|
[hideGear]="false"
|
||||||
[hidePagerWhenSinglePage]="true">
|
[hidePagerWhenSinglePage]="true">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
@@ -26,12 +26,12 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let bitstreamFormat of (bitstreamFormats | async)?.payload?.page">
|
<tr *ngFor="let bitstreamFormat of (bitstreamFormats$ | async)?.payload?.page">
|
||||||
<td>
|
<td>
|
||||||
<label class="mb-0">
|
<label class="mb-0">
|
||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
[attr.aria-label]="'admin.registries.bitstream-formats.select' | translate"
|
[attr.aria-label]="'admin.registries.bitstream-formats.select' | translate"
|
||||||
[checked]="isSelected(bitstreamFormat) | async"
|
[checked]="(selectedBitstreamFormatIDs$ | async)?.includes(bitstreamFormat.id)"
|
||||||
(change)="selectBitStreamFormat(bitstreamFormat, $event)"
|
(change)="selectBitStreamFormat(bitstreamFormat, $event)"
|
||||||
>
|
>
|
||||||
<span class="sr-only">{{'admin.registries.bitstream-formats.select' | translate}}}</span>
|
<span class="sr-only">{{'admin.registries.bitstream-formats.select' | translate}}}</span>
|
||||||
@@ -46,13 +46,13 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</ds-pagination>
|
</ds-pagination>
|
||||||
<div *ngIf="(bitstreamFormats | async)?.payload?.totalElements === 0" class="alert alert-info" role="alert">
|
<div *ngIf="(bitstreamFormats$ | async)?.payload?.totalElements === 0" class="alert alert-info" role="alert">
|
||||||
{{'admin.registries.bitstream-formats.no-items' | translate}}
|
{{'admin.registries.bitstream-formats.no-items' | translate}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<button *ngIf="(bitstreamFormats | async)?.payload?.page?.length > 0" class="btn btn-primary deselect" (click)="deselectAll()">{{'admin.registries.bitstream-formats.table.deselect-all' | translate}}</button>
|
<button *ngIf="(bitstreamFormats$ | async)?.payload?.page?.length > 0" class="btn btn-primary deselect" (click)="deselectAll()">{{'admin.registries.bitstream-formats.table.deselect-all' | translate}}</button>
|
||||||
<button *ngIf="(bitstreamFormats | async)?.payload?.page?.length > 0" type="submit" class="btn btn-danger float-right" (click)="deleteFormats()">{{'admin.registries.bitstream-formats.table.delete' | translate}}</button>
|
<button *ngIf="(bitstreamFormats$ | async)?.payload?.page?.length > 0" type="submit" class="btn btn-danger float-right" (click)="deleteFormats()">{{'admin.registries.bitstream-formats.table.delete' | translate}}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -8,10 +8,7 @@ import { By } from '@angular/platform-browser';
|
|||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { provideMockStore } from '@ngrx/store/testing';
|
import { provideMockStore } from '@ngrx/store/testing';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import {
|
import { hot } from 'jasmine-marbles';
|
||||||
cold,
|
|
||||||
hot,
|
|
||||||
} from 'jasmine-marbles';
|
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
import { BitstreamFormatDataService } from '../../../core/data/bitstream-format-data.service';
|
import { BitstreamFormatDataService } from '../../../core/data/bitstream-format-data.service';
|
||||||
@@ -190,18 +187,18 @@ describe('BitstreamFormatsComponent', () => {
|
|||||||
describe('isSelected', () => {
|
describe('isSelected', () => {
|
||||||
beforeEach(waitForAsync(initAsync));
|
beforeEach(waitForAsync(initAsync));
|
||||||
beforeEach(initBeforeEach);
|
beforeEach(initBeforeEach);
|
||||||
it('should return an observable of true if the provided bistream is in the list returned by the service', () => {
|
it('should return an observable of true if the provided bitstream is in the list returned by the service', () => {
|
||||||
const result = comp.isSelected(bitstreamFormat1);
|
comp.selectedBitstreamFormatIDs().subscribe((selectedBitstreamFormatIDs: string[]) => {
|
||||||
|
expect(selectedBitstreamFormatIDs).toContain(bitstreamFormat1.id);
|
||||||
expect(result).toBeObservable(cold('b', { b: true }));
|
});
|
||||||
});
|
});
|
||||||
it('should return an observable of false if the provided bistream is not in the list returned by the service', () => {
|
it('should return an observable of false if the provided bitstream is not in the list returned by the service', () => {
|
||||||
const format = new BitstreamFormat();
|
const format = new BitstreamFormat();
|
||||||
format.uuid = 'new';
|
format.uuid = 'new';
|
||||||
|
|
||||||
const result = comp.isSelected(format);
|
comp.selectedBitstreamFormatIDs().subscribe((selectedBitstreamFormatIDs: string[]) => {
|
||||||
|
expect(selectedBitstreamFormatIDs).not.toContain(format.id);
|
||||||
expect(result).toBeObservable(cold('b', { b: false }));
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -13,10 +13,7 @@ import {
|
|||||||
TranslateModule,
|
TranslateModule,
|
||||||
TranslateService,
|
TranslateService,
|
||||||
} from '@ngx-translate/core';
|
} from '@ngx-translate/core';
|
||||||
import {
|
import { Observable } from 'rxjs';
|
||||||
combineLatest as observableCombineLatest,
|
|
||||||
Observable,
|
|
||||||
} from 'rxjs';
|
|
||||||
import {
|
import {
|
||||||
map,
|
map,
|
||||||
mergeMap,
|
mergeMap,
|
||||||
@@ -58,7 +55,12 @@ export class BitstreamFormatsComponent implements OnInit, OnDestroy {
|
|||||||
/**
|
/**
|
||||||
* A paginated list of bitstream formats to be shown on the page
|
* A paginated list of bitstream formats to be shown on the page
|
||||||
*/
|
*/
|
||||||
bitstreamFormats: Observable<RemoteData<PaginatedList<BitstreamFormat>>>;
|
bitstreamFormats$: Observable<RemoteData<PaginatedList<BitstreamFormat>>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The currently selected {@link BitstreamFormat} IDs
|
||||||
|
*/
|
||||||
|
selectedBitstreamFormatIDs$: Observable<string[]>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current pagination configuration for the page
|
* The current pagination configuration for the page
|
||||||
@@ -118,21 +120,18 @@ export class BitstreamFormatsComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deselects all selecetd bitstream formats
|
* Deselects all selected bitstream formats
|
||||||
*/
|
*/
|
||||||
deselectAll() {
|
deselectAll() {
|
||||||
this.bitstreamFormatService.deselectAllBitstreamFormats();
|
this.bitstreamFormatService.deselectAllBitstreamFormats();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether a given bitstream format is selected in the list (checkbox)
|
* Returns the list of all the bitstream formats that are selected in the list (checkbox)
|
||||||
* @param bitstreamFormat
|
|
||||||
*/
|
*/
|
||||||
isSelected(bitstreamFormat: BitstreamFormat): Observable<boolean> {
|
selectedBitstreamFormatIDs(): Observable<string[]> {
|
||||||
return this.bitstreamFormatService.getSelectedBitstreamFormats().pipe(
|
return this.bitstreamFormatService.getSelectedBitstreamFormats().pipe(
|
||||||
map((bitstreamFormats: BitstreamFormat[]) => {
|
map((bitstreamFormats: BitstreamFormat[]) => bitstreamFormats.map((selectedFormat) => selectedFormat.id)),
|
||||||
return bitstreamFormats.find((selectedFormat) => selectedFormat.id === bitstreamFormat.id) != null;
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,27 +155,23 @@ export class BitstreamFormatsComponent implements OnInit, OnDestroy {
|
|||||||
const prefix = 'admin.registries.bitstream-formats.delete';
|
const prefix = 'admin.registries.bitstream-formats.delete';
|
||||||
const suffix = success ? 'success' : 'failure';
|
const suffix = success ? 'success' : 'failure';
|
||||||
|
|
||||||
const messages = observableCombineLatest(
|
const head: string = this.translateService.instant(`${prefix}.${suffix}.head`);
|
||||||
this.translateService.get(`${prefix}.${suffix}.head`),
|
const content: string = this.translateService.instant(`${prefix}.${suffix}.amount`, { amount: amount });
|
||||||
this.translateService.get(`${prefix}.${suffix}.amount`, { amount: amount }),
|
|
||||||
);
|
|
||||||
messages.subscribe(([head, content]) => {
|
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
this.notificationsService.success(head, content);
|
this.notificationsService.success(head, content);
|
||||||
} else {
|
} else {
|
||||||
this.notificationsService.error(head, content);
|
this.notificationsService.error(head, content);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
this.bitstreamFormats$ = this.paginationService.getFindListOptions(this.pageConfig.id, this.pageConfig).pipe(
|
||||||
this.bitstreamFormats = this.paginationService.getFindListOptions(this.pageConfig.id, this.pageConfig).pipe(
|
|
||||||
switchMap((findListOptions: FindListOptions) => {
|
switchMap((findListOptions: FindListOptions) => {
|
||||||
return this.bitstreamFormatService.findAll(findListOptions);
|
return this.bitstreamFormatService.findAll(findListOptions);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
this.selectedBitstreamFormatIDs$ = this.selectedBitstreamFormatIDs();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@@ -154,12 +154,12 @@ export class FormatFormComponent implements OnInit {
|
|||||||
(fieldModel: DynamicFormControlModel) => {
|
(fieldModel: DynamicFormControlModel) => {
|
||||||
if (fieldModel.name === 'extensions') {
|
if (fieldModel.name === 'extensions') {
|
||||||
if (hasValue(this.bitstreamFormat.extensions)) {
|
if (hasValue(this.bitstreamFormat.extensions)) {
|
||||||
const extenstions = this.bitstreamFormat.extensions;
|
const extensions = this.bitstreamFormat.extensions;
|
||||||
const formArray = (fieldModel as DynamicFormArrayModel);
|
const formArray = (fieldModel as DynamicFormArrayModel);
|
||||||
for (let i = 0; i < extenstions.length; i++) {
|
for (let i = 0; i < extensions.length; i++) {
|
||||||
formArray.insertGroup(i).group[0] = new DynamicInputModel({
|
formArray.insertGroup(i).group[0] = new DynamicInputModel({
|
||||||
id: `extension-${i}`,
|
id: `extension-${i}`,
|
||||||
value: extenstions[i],
|
value: extensions[i],
|
||||||
}, this.arrayInputElementLayout);
|
}, this.arrayInputElementLayout);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -172,7 +172,7 @@ export class FormatFormComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an updated bistream format based on the current values in the form
|
* Creates an updated bitstream format based on the current values in the form
|
||||||
* Emits the updated bitstream format trouhg the updatedFormat emitter
|
* Emits the updated bitstream format trouhg the updatedFormat emitter
|
||||||
*/
|
*/
|
||||||
onSubmit() {
|
onSubmit() {
|
||||||
|
@@ -27,14 +27,14 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let schema of (metadataSchemas | async)?.payload?.page"
|
<tr *ngFor="let schema of (metadataSchemas | async)?.payload?.page"
|
||||||
[ngClass]="{'table-primary' : isActive(schema) | async}">
|
[ngClass]="{'table-primary' : (activeMetadataSchema$ | async)?.id === schema.id}">
|
||||||
<td>
|
<td>
|
||||||
<label class="mb-0">
|
<label class="mb-0">
|
||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
[checked]="isSelected(schema) | async"
|
[checked]="(selectedMetadataSchemaIDs$ | async)?.includes(schema.id)"
|
||||||
(change)="selectMetadataSchema(schema, $event)"
|
(change)="selectMetadataSchema(schema, $event)"
|
||||||
>
|
>
|
||||||
<span class="sr-only">{{((isSelected(schema) | async) ? 'admin.registries.metadata.schemas.deselect' : 'admin.registries.metadata.schemas.select') | translate}}</span>
|
<span class="sr-only">{{(((selectedMetadataSchemaIDs$ | async)?.includes(schema.id)) ? 'admin.registries.metadata.schemas.deselect' : 'admin.registries.metadata.schemas.select') | translate}}</span>
|
||||||
</label>
|
</label>
|
||||||
</td>
|
</td>
|
||||||
<td class="selectable-row" (click)="editSchema(schema)"><a [routerLink]="[schema.prefix]">{{schema.id}}</a></td>
|
<td class="selectable-row" (click)="editSchema(schema)"><a [routerLink]="[schema.prefix]">{{schema.id}}</a></td>
|
||||||
|
@@ -17,9 +17,7 @@ import { TranslateModule } from '@ngx-translate/core';
|
|||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
import { FormBuilderService } from 'src/app/shared/form/builder/form-builder.service';
|
import { FormBuilderService } from 'src/app/shared/form/builder/form-builder.service';
|
||||||
|
|
||||||
import { RestResponse } from '../../../core/cache/response.models';
|
|
||||||
import { ConfigurationDataService } from '../../../core/data/configuration-data.service';
|
import { ConfigurationDataService } from '../../../core/data/configuration-data.service';
|
||||||
import { buildPaginatedList } from '../../../core/data/paginated-list.model';
|
|
||||||
import { GroupDataService } from '../../../core/eperson/group-data.service';
|
import { GroupDataService } from '../../../core/eperson/group-data.service';
|
||||||
import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
|
import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
|
||||||
import { PaginationService } from '../../../core/pagination/pagination.service';
|
import { PaginationService } from '../../../core/pagination/pagination.service';
|
||||||
@@ -36,7 +34,9 @@ import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.u
|
|||||||
import { HostWindowServiceStub } from '../../../shared/testing/host-window-service.stub';
|
import { HostWindowServiceStub } from '../../../shared/testing/host-window-service.stub';
|
||||||
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
|
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
|
||||||
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
|
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
|
||||||
|
import { RegistryServiceStub } from '../../../shared/testing/registry.service.stub';
|
||||||
import { SearchConfigurationServiceStub } from '../../../shared/testing/search-configuration-service.stub';
|
import { SearchConfigurationServiceStub } from '../../../shared/testing/search-configuration-service.stub';
|
||||||
|
import { createPaginatedList } from '../../../shared/testing/utils.test';
|
||||||
import { EnumKeysPipe } from '../../../shared/utils/enum-keys-pipe';
|
import { EnumKeysPipe } from '../../../shared/utils/enum-keys-pipe';
|
||||||
import { MetadataRegistryComponent } from './metadata-registry.component';
|
import { MetadataRegistryComponent } from './metadata-registry.component';
|
||||||
import { MetadataSchemaFormComponent } from './metadata-schema-form/metadata-schema-form.component';
|
import { MetadataSchemaFormComponent } from './metadata-schema-form/metadata-schema-form.component';
|
||||||
@@ -44,9 +44,11 @@ import { MetadataSchemaFormComponent } from './metadata-schema-form/metadata-sch
|
|||||||
describe('MetadataRegistryComponent', () => {
|
describe('MetadataRegistryComponent', () => {
|
||||||
let comp: MetadataRegistryComponent;
|
let comp: MetadataRegistryComponent;
|
||||||
let fixture: ComponentFixture<MetadataRegistryComponent>;
|
let fixture: ComponentFixture<MetadataRegistryComponent>;
|
||||||
let registryService: RegistryService;
|
|
||||||
let paginationService;
|
let paginationService: PaginationServiceStub;
|
||||||
const mockSchemasList = [
|
let registryService: RegistryServiceStub;
|
||||||
|
|
||||||
|
const mockSchemasList: MetadataSchema[] = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
_links: {
|
_links: {
|
||||||
@@ -67,25 +69,7 @@ describe('MetadataRegistryComponent', () => {
|
|||||||
prefix: 'mock',
|
prefix: 'mock',
|
||||||
namespace: 'http://dspace.org/mockschema',
|
namespace: 'http://dspace.org/mockschema',
|
||||||
},
|
},
|
||||||
];
|
] as MetadataSchema[];
|
||||||
const mockSchemas = createSuccessfulRemoteDataObject$(buildPaginatedList(null, mockSchemasList));
|
|
||||||
/* eslint-disable no-empty,@typescript-eslint/no-empty-function */
|
|
||||||
const registryServiceStub = {
|
|
||||||
getMetadataSchemas: () => mockSchemas,
|
|
||||||
getActiveMetadataSchema: () => observableOf(undefined),
|
|
||||||
getSelectedMetadataSchemas: () => observableOf([]),
|
|
||||||
editMetadataSchema: (schema) => {
|
|
||||||
},
|
|
||||||
cancelEditMetadataSchema: () => {
|
|
||||||
},
|
|
||||||
deleteMetadataSchema: () => observableOf(new RestResponse(true, 200, 'OK')),
|
|
||||||
deselectAllMetadataSchema: () => {
|
|
||||||
},
|
|
||||||
clearMetadataSchemaRequests: () => observableOf(undefined),
|
|
||||||
};
|
|
||||||
/* eslint-enable no-empty, @typescript-eslint/no-empty-function */
|
|
||||||
|
|
||||||
paginationService = new PaginationServiceStub();
|
|
||||||
|
|
||||||
const configurationDataService = jasmine.createSpyObj('configurationDataService', {
|
const configurationDataService = jasmine.createSpyObj('configurationDataService', {
|
||||||
findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), {
|
findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), {
|
||||||
@@ -109,6 +93,10 @@ describe('MetadataRegistryComponent', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
|
paginationService = new PaginationServiceStub();
|
||||||
|
registryService = new RegistryServiceStub();
|
||||||
|
spyOn(registryService, 'getMetadataSchemas').and.returnValue(createSuccessfulRemoteDataObject$(createPaginatedList(mockSchemasList)));
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
@@ -120,7 +108,7 @@ describe('MetadataRegistryComponent', () => {
|
|||||||
EnumKeysPipe,
|
EnumKeysPipe,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: RegistryService, useValue: registryServiceStub },
|
{ provide: RegistryService, useValue: registryService },
|
||||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
|
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
|
||||||
{ provide: PaginationService, useValue: paginationService },
|
{ provide: PaginationService, useValue: paginationService },
|
||||||
{
|
{
|
||||||
@@ -190,7 +178,7 @@ describe('MetadataRegistryComponent', () => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
it('should cancel editing the selected schema when clicked again', waitForAsync(() => {
|
it('should cancel editing the selected schema when clicked again', waitForAsync(() => {
|
||||||
spyOn(registryService, 'getActiveMetadataSchema').and.returnValue(observableOf(mockSchemasList[0] as MetadataSchema));
|
comp.activeMetadataSchema$ = observableOf(mockSchemasList[0] as MetadataSchema);
|
||||||
spyOn(registryService, 'cancelEditMetadataSchema');
|
spyOn(registryService, 'cancelEditMetadataSchema');
|
||||||
row.click();
|
row.click();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
@@ -205,7 +193,7 @@ describe('MetadataRegistryComponent', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(registryService, 'deleteMetadataSchema').and.callThrough();
|
spyOn(registryService, 'deleteMetadataSchema').and.callThrough();
|
||||||
spyOn(registryService, 'getSelectedMetadataSchemas').and.returnValue(observableOf(selectedSchemas as MetadataSchema[]));
|
comp.selectedMetadataSchemaIDs$ = observableOf(selectedSchemas.map((selectedSchema: MetadataSchema) => selectedSchema.id));
|
||||||
comp.deleteSchemas();
|
comp.deleteSchemas();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
@@ -4,19 +4,20 @@ import {
|
|||||||
NgForOf,
|
NgForOf,
|
||||||
NgIf,
|
NgIf,
|
||||||
} from '@angular/common';
|
} from '@angular/common';
|
||||||
import { Component } from '@angular/core';
|
|
||||||
import {
|
import {
|
||||||
Router,
|
Component,
|
||||||
RouterLink,
|
OnDestroy,
|
||||||
} from '@angular/router';
|
OnInit,
|
||||||
|
} from '@angular/core';
|
||||||
|
import { RouterLink } from '@angular/router';
|
||||||
import {
|
import {
|
||||||
TranslateModule,
|
TranslateModule,
|
||||||
TranslateService,
|
TranslateService,
|
||||||
} from '@ngx-translate/core';
|
} from '@ngx-translate/core';
|
||||||
import {
|
import {
|
||||||
BehaviorSubject,
|
BehaviorSubject,
|
||||||
combineLatest as observableCombineLatest,
|
|
||||||
Observable,
|
Observable,
|
||||||
|
Subscription,
|
||||||
zip,
|
zip,
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import {
|
import {
|
||||||
@@ -33,7 +34,6 @@ import { PaginationService } from '../../../core/pagination/pagination.service';
|
|||||||
import { RegistryService } from '../../../core/registry/registry.service';
|
import { RegistryService } from '../../../core/registry/registry.service';
|
||||||
import { NoContent } from '../../../core/shared/NoContent.model';
|
import { NoContent } from '../../../core/shared/NoContent.model';
|
||||||
import { getFirstCompletedRemoteData } from '../../../core/shared/operators';
|
import { getFirstCompletedRemoteData } from '../../../core/shared/operators';
|
||||||
import { hasValue } from '../../../shared/empty.util';
|
|
||||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
import { PaginationComponent } from '../../../shared/pagination/pagination.component';
|
import { PaginationComponent } from '../../../shared/pagination/pagination.component';
|
||||||
import { toFindListOptions } from '../../../shared/pagination/pagination.utils';
|
import { toFindListOptions } from '../../../shared/pagination/pagination.utils';
|
||||||
@@ -60,13 +60,23 @@ import { MetadataSchemaFormComponent } from './metadata-schema-form/metadata-sch
|
|||||||
* A component used for managing all existing metadata schemas within the repository.
|
* A component used for managing all existing metadata schemas within the repository.
|
||||||
* The admin can create, edit or delete metadata schemas here.
|
* The admin can create, edit or delete metadata schemas here.
|
||||||
*/
|
*/
|
||||||
export class MetadataRegistryComponent {
|
export class MetadataRegistryComponent implements OnDestroy, OnInit {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A list of all the current metadata schemas within the repository
|
* A list of all the current metadata schemas within the repository
|
||||||
*/
|
*/
|
||||||
metadataSchemas: Observable<RemoteData<PaginatedList<MetadataSchema>>>;
|
metadataSchemas: Observable<RemoteData<PaginatedList<MetadataSchema>>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link MetadataSchema}that is being edited
|
||||||
|
*/
|
||||||
|
activeMetadataSchema$: Observable<MetadataSchema>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The selected {@link MetadataSchema} IDs
|
||||||
|
*/
|
||||||
|
selectedMetadataSchemaIDs$: Observable<number[]>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pagination config used to display the list of metadata schemas
|
* Pagination config used to display the list of metadata schemas
|
||||||
*/
|
*/
|
||||||
@@ -76,15 +86,25 @@ export class MetadataRegistryComponent {
|
|||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether or not the list of MetadataSchemas needs an update
|
* Whether the list of MetadataSchemas needs an update
|
||||||
*/
|
*/
|
||||||
needsUpdate$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
|
needsUpdate$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
|
||||||
|
|
||||||
constructor(private registryService: RegistryService,
|
subscriptions: Subscription[] = [];
|
||||||
private notificationsService: NotificationsService,
|
|
||||||
private router: Router,
|
constructor(
|
||||||
private paginationService: PaginationService,
|
protected registryService: RegistryService,
|
||||||
private translateService: TranslateService) {
|
protected notificationsService: NotificationsService,
|
||||||
|
protected paginationService: PaginationService,
|
||||||
|
protected translateService: TranslateService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.activeMetadataSchema$ = this.registryService.getActiveMetadataSchema();
|
||||||
|
this.selectedMetadataSchemaIDs$ = this.registryService.getSelectedMetadataSchemas().pipe(
|
||||||
|
map((schemas: MetadataSchema[]) => schemas.map((schema: MetadataSchema) => schema.id)),
|
||||||
|
);
|
||||||
this.updateSchemas();
|
this.updateSchemas();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,30 +133,13 @@ export class MetadataRegistryComponent {
|
|||||||
* @param schema
|
* @param schema
|
||||||
*/
|
*/
|
||||||
editSchema(schema: MetadataSchema) {
|
editSchema(schema: MetadataSchema) {
|
||||||
this.getActiveSchema().pipe(take(1)).subscribe((activeSchema) => {
|
this.subscriptions.push(this.activeMetadataSchema$.pipe(take(1)).subscribe((activeSchema: MetadataSchema) => {
|
||||||
if (schema === activeSchema) {
|
if (schema === activeSchema) {
|
||||||
this.registryService.cancelEditMetadataSchema();
|
this.registryService.cancelEditMetadataSchema();
|
||||||
} else {
|
} else {
|
||||||
this.registryService.editMetadataSchema(schema);
|
this.registryService.editMetadataSchema(schema);
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether the given metadata schema is active (being edited)
|
|
||||||
* @param schema
|
|
||||||
*/
|
|
||||||
isActive(schema: MetadataSchema): Observable<boolean> {
|
|
||||||
return this.getActiveSchema().pipe(
|
|
||||||
map((activeSchema) => schema === activeSchema),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the active metadata schema (being edited)
|
|
||||||
*/
|
|
||||||
getActiveSchema(): Observable<MetadataSchema> {
|
|
||||||
return this.registryService.getActiveMetadataSchema();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -150,42 +153,25 @@ export class MetadataRegistryComponent {
|
|||||||
this.registryService.deselectMetadataSchema(schema);
|
this.registryService.deselectMetadataSchema(schema);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether a given metadata schema is selected in the list (checkbox)
|
|
||||||
* @param schema
|
|
||||||
*/
|
|
||||||
isSelected(schema: MetadataSchema): Observable<boolean> {
|
|
||||||
return this.registryService.getSelectedMetadataSchemas().pipe(
|
|
||||||
map((schemas) => schemas.find((selectedSchema) => selectedSchema === schema) != null),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete all the selected metadata schemas
|
* Delete all the selected metadata schemas
|
||||||
*/
|
*/
|
||||||
deleteSchemas() {
|
deleteSchemas() {
|
||||||
this.registryService.getSelectedMetadataSchemas().pipe(take(1)).subscribe(
|
this.subscriptions.push(this.selectedMetadataSchemaIDs$.pipe(
|
||||||
(schemas) => {
|
take(1),
|
||||||
const tasks$ = [];
|
switchMap((schemaIDs: number[]) => zip(schemaIDs.map((schemaID: number) => this.registryService.deleteMetadataSchema(schemaID).pipe(getFirstCompletedRemoteData())))),
|
||||||
for (const schema of schemas) {
|
).subscribe((responses: RemoteData<NoContent>[]) => {
|
||||||
if (hasValue(schema.id)) {
|
const successResponses: RemoteData<NoContent>[] = responses.filter((response: RemoteData<NoContent>) => response.hasSucceeded);
|
||||||
tasks$.push(this.registryService.deleteMetadataSchema(schema.id).pipe(getFirstCompletedRemoteData()));
|
const failedResponses: RemoteData<NoContent>[] = responses.filter((response: RemoteData<NoContent>) => response.hasFailed);
|
||||||
}
|
if (successResponses.length > 0) {
|
||||||
}
|
this.showNotification(true, successResponses.length);
|
||||||
zip(...tasks$).subscribe((responses: RemoteData<NoContent>[]) => {
|
}
|
||||||
const successResponses = responses.filter((response: RemoteData<NoContent>) => response.hasSucceeded);
|
if (failedResponses.length > 0) {
|
||||||
const failedResponses = responses.filter((response: RemoteData<NoContent>) => response.hasFailed);
|
this.showNotification(false, failedResponses.length);
|
||||||
if (successResponses.length > 0) {
|
}
|
||||||
this.showNotification(true, successResponses.length);
|
this.registryService.deselectAllMetadataSchema();
|
||||||
}
|
this.registryService.cancelEditMetadataSchema();
|
||||||
if (failedResponses.length > 0) {
|
}));
|
||||||
this.showNotification(false, failedResponses.length);
|
|
||||||
}
|
|
||||||
this.registryService.deselectAllMetadataSchema();
|
|
||||||
this.registryService.cancelEditMetadataSchema();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -196,20 +182,20 @@ export class MetadataRegistryComponent {
|
|||||||
showNotification(success: boolean, amount: number) {
|
showNotification(success: boolean, amount: number) {
|
||||||
const prefix = 'admin.registries.schema.notification';
|
const prefix = 'admin.registries.schema.notification';
|
||||||
const suffix = success ? 'success' : 'failure';
|
const suffix = success ? 'success' : 'failure';
|
||||||
const messages = observableCombineLatest(
|
|
||||||
this.translateService.get(success ? `${prefix}.${suffix}` : `${prefix}.${suffix}`),
|
const head: string = this.translateService.instant(success ? `${prefix}.${suffix}` : `${prefix}.${suffix}`);
|
||||||
this.translateService.get(`${prefix}.deleted.${suffix}`, { amount: amount }),
|
const content: string = this.translateService.instant(`${prefix}.deleted.${suffix}`, { amount: amount });
|
||||||
);
|
|
||||||
messages.subscribe(([head, content]) => {
|
if (success) {
|
||||||
if (success) {
|
this.notificationsService.success(head, content);
|
||||||
this.notificationsService.success(head, content);
|
} else {
|
||||||
} else {
|
this.notificationsService.error(head, content);
|
||||||
this.notificationsService.error(head, content);
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.paginationService.clearPagination(this.config.id);
|
this.paginationService.clearPagination(this.config.id);
|
||||||
|
this.subscriptions.map((subscription: Subscription) => subscription.unsubscribe());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
<div *ngIf="registryService.getActiveMetadataSchema() | async; then editheader; else createHeader"></div>
|
<div *ngIf="activeMetadataSchema$ | async; then editheader; else createHeader"></div>
|
||||||
|
|
||||||
<ng-template #createHeader>
|
<ng-template #createHeader>
|
||||||
<h2>{{messagePrefix + '.create' | translate}}</h2>
|
<h2>{{messagePrefix + '.create' | translate}}</h2>
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
import {
|
import {
|
||||||
ComponentFixture,
|
ComponentFixture,
|
||||||
inject,
|
inject,
|
||||||
@@ -16,42 +16,26 @@ import { RegistryService } from '../../../../core/registry/registry.service';
|
|||||||
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
|
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
|
||||||
import { FormComponent } from '../../../../shared/form/form.component';
|
import { FormComponent } from '../../../../shared/form/form.component';
|
||||||
import { getMockFormBuilderService } from '../../../../shared/mocks/form-builder-service.mock';
|
import { getMockFormBuilderService } from '../../../../shared/mocks/form-builder-service.mock';
|
||||||
|
import { RegistryServiceStub } from '../../../../shared/testing/registry.service.stub';
|
||||||
import { EnumKeysPipe } from '../../../../shared/utils/enum-keys-pipe';
|
import { EnumKeysPipe } from '../../../../shared/utils/enum-keys-pipe';
|
||||||
import { MetadataSchemaFormComponent } from './metadata-schema-form.component';
|
import { MetadataSchemaFormComponent } from './metadata-schema-form.component';
|
||||||
|
|
||||||
describe('MetadataSchemaFormComponent', () => {
|
describe('MetadataSchemaFormComponent', () => {
|
||||||
let component: MetadataSchemaFormComponent;
|
let component: MetadataSchemaFormComponent;
|
||||||
let fixture: ComponentFixture<MetadataSchemaFormComponent>;
|
let fixture: ComponentFixture<MetadataSchemaFormComponent>;
|
||||||
let registryService: RegistryService;
|
|
||||||
|
|
||||||
/* eslint-disable no-empty,@typescript-eslint/no-empty-function */
|
let registryService: RegistryServiceStub;
|
||||||
const registryServiceStub = {
|
|
||||||
getActiveMetadataSchema: () => observableOf(undefined),
|
|
||||||
createOrUpdateMetadataSchema: (schema: MetadataSchema) => observableOf(schema),
|
|
||||||
cancelEditMetadataSchema: () => {
|
|
||||||
},
|
|
||||||
clearMetadataSchemaRequests: () => observableOf(undefined),
|
|
||||||
};
|
|
||||||
const formBuilderServiceStub = {
|
|
||||||
createFormGroup: () => {
|
|
||||||
return {
|
|
||||||
patchValue: () => {
|
|
||||||
},
|
|
||||||
reset(_value?: any, _options?: { onlySelf?: boolean; emitEvent?: boolean; }): void {
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
/* eslint-enable no-empty, @typescript-eslint/no-empty-function */
|
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
|
registryService = new RegistryServiceStub();
|
||||||
|
|
||||||
return TestBed.configureTestingModule({
|
return TestBed.configureTestingModule({
|
||||||
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule, MetadataSchemaFormComponent, EnumKeysPipe],
|
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule, MetadataSchemaFormComponent, EnumKeysPipe],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: RegistryService, useValue: registryServiceStub },
|
{ provide: RegistryService, useValue: registryService },
|
||||||
{ provide: FormBuilderService, useValue: getMockFormBuilderService() },
|
{ provide: FormBuilderService, useValue: getMockFormBuilderService() },
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA],
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||||
})
|
})
|
||||||
.overrideComponent(MetadataSchemaFormComponent, {
|
.overrideComponent(MetadataSchemaFormComponent, {
|
||||||
remove: {
|
remove: {
|
||||||
@@ -88,7 +72,7 @@ describe('MetadataSchemaFormComponent', () => {
|
|||||||
|
|
||||||
describe('without an active schema', () => {
|
describe('without an active schema', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(registryService, 'getActiveMetadataSchema').and.returnValue(observableOf(undefined));
|
component.activeMetadataSchema$ = observableOf(undefined);
|
||||||
component.onSubmit();
|
component.onSubmit();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
@@ -107,7 +91,7 @@ describe('MetadataSchemaFormComponent', () => {
|
|||||||
} as MetadataSchema);
|
} as MetadataSchema);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(registryService, 'getActiveMetadataSchema').and.returnValue(observableOf(expectedWithId));
|
component.activeMetadataSchema$ = observableOf(expectedWithId);
|
||||||
component.onSubmit();
|
component.onSubmit();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
@@ -21,13 +21,13 @@ import {
|
|||||||
TranslateService,
|
TranslateService,
|
||||||
} from '@ngx-translate/core';
|
} from '@ngx-translate/core';
|
||||||
import {
|
import {
|
||||||
combineLatest,
|
|
||||||
Observable,
|
Observable,
|
||||||
|
Subscription,
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import {
|
import {
|
||||||
|
map,
|
||||||
switchMap,
|
switchMap,
|
||||||
take,
|
take,
|
||||||
tap,
|
|
||||||
} from 'rxjs/operators';
|
} from 'rxjs/operators';
|
||||||
|
|
||||||
import { MetadataSchema } from '../../../../core/metadata/metadata-schema.model';
|
import { MetadataSchema } from '../../../../core/metadata/metadata-schema.model';
|
||||||
@@ -102,64 +102,71 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
@Output() submitForm: EventEmitter<any> = new EventEmitter();
|
@Output() submitForm: EventEmitter<any> = new EventEmitter();
|
||||||
|
|
||||||
constructor(public registryService: RegistryService, private formBuilderService: FormBuilderService, private translateService: TranslateService) {
|
/**
|
||||||
|
* The {@link MetadataSchema} that is currently being edited
|
||||||
|
*/
|
||||||
|
activeMetadataSchema$: Observable<MetadataSchema>;
|
||||||
|
|
||||||
|
subscriptions: Subscription[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected registryService: RegistryService,
|
||||||
|
protected formBuilderService: FormBuilderService,
|
||||||
|
protected translateService: TranslateService,
|
||||||
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
combineLatest([
|
this.name = new DynamicInputModel({
|
||||||
this.translateService.get(`${this.messagePrefix}.name`),
|
id: 'name',
|
||||||
this.translateService.get(`${this.messagePrefix}.namespace`),
|
label: this.translateService.instant(`${this.messagePrefix}.name`),
|
||||||
]).subscribe(([name, namespace]) => {
|
name: 'name',
|
||||||
this.name = new DynamicInputModel({
|
validators: {
|
||||||
id: 'name',
|
required: null,
|
||||||
label: name,
|
pattern: '^[^. ,]*$',
|
||||||
name: 'name',
|
maxLength: 32,
|
||||||
validators: {
|
},
|
||||||
required: null,
|
required: true,
|
||||||
pattern: '^[^. ,]*$',
|
errorMessages: {
|
||||||
maxLength: 32,
|
pattern: 'error.validation.metadata.name.invalid-pattern',
|
||||||
},
|
maxLength: 'error.validation.metadata.name.max-length',
|
||||||
required: true,
|
},
|
||||||
errorMessages: {
|
|
||||||
pattern: 'error.validation.metadata.name.invalid-pattern',
|
|
||||||
maxLength: 'error.validation.metadata.name.max-length',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.namespace = new DynamicInputModel({
|
|
||||||
id: 'namespace',
|
|
||||||
label: namespace,
|
|
||||||
name: 'namespace',
|
|
||||||
validators: {
|
|
||||||
required: null,
|
|
||||||
maxLength: 256,
|
|
||||||
},
|
|
||||||
required: true,
|
|
||||||
errorMessages: {
|
|
||||||
maxLength: 'error.validation.metadata.namespace.max-length',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.formModel = [
|
|
||||||
new DynamicFormGroupModel(
|
|
||||||
{
|
|
||||||
id: 'metadatadataschemagroup',
|
|
||||||
group:[this.namespace, this.name],
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
|
|
||||||
this.registryService.getActiveMetadataSchema().subscribe((schema: MetadataSchema) => {
|
|
||||||
if (schema == null) {
|
|
||||||
this.clearFields();
|
|
||||||
} else {
|
|
||||||
this.formGroup.patchValue({
|
|
||||||
metadatadataschemagroup: {
|
|
||||||
name: schema.prefix,
|
|
||||||
namespace: schema.namespace,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.name.disabled = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
this.namespace = new DynamicInputModel({
|
||||||
|
id: 'namespace',
|
||||||
|
label: this.translateService.instant(`${this.messagePrefix}.namespace`),
|
||||||
|
name: 'namespace',
|
||||||
|
validators: {
|
||||||
|
required: null,
|
||||||
|
maxLength: 256,
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
errorMessages: {
|
||||||
|
maxLength: 'error.validation.metadata.namespace.max-length',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.formModel = [
|
||||||
|
new DynamicFormGroupModel(
|
||||||
|
{
|
||||||
|
id: 'metadatadataschemagroup',
|
||||||
|
group:[this.namespace, this.name],
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
|
||||||
|
this.activeMetadataSchema$ = this.registryService.getActiveMetadataSchema();
|
||||||
|
this.subscriptions.push(this.activeMetadataSchema$.subscribe((schema: MetadataSchema) => {
|
||||||
|
if (schema == null) {
|
||||||
|
this.clearFields();
|
||||||
|
} else {
|
||||||
|
this.formGroup.patchValue({
|
||||||
|
metadatadataschemagroup: {
|
||||||
|
name: schema.prefix,
|
||||||
|
namespace: schema.namespace,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.name.disabled = true;
|
||||||
|
}
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -176,48 +183,29 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
|
|||||||
* Emit the updated/created schema using the EventEmitter submitForm
|
* Emit the updated/created schema using the EventEmitter submitForm
|
||||||
*/
|
*/
|
||||||
onSubmit(): void {
|
onSubmit(): void {
|
||||||
this.registryService
|
this.activeMetadataSchema$.pipe(
|
||||||
.getActiveMetadataSchema()
|
take(1),
|
||||||
.pipe(
|
switchMap((schema: MetadataSchema) => {
|
||||||
take(1),
|
const metadataValues = {
|
||||||
switchMap((schema: MetadataSchema) => {
|
prefix: this.name.value,
|
||||||
const metadataValues = {
|
namespace: this.namespace.value,
|
||||||
prefix: this.name.value,
|
};
|
||||||
namespace: this.namespace.value,
|
if (schema == null) {
|
||||||
};
|
return this.registryService.createOrUpdateMetadataSchema(Object.assign(new MetadataSchema(), metadataValues));
|
||||||
|
} else {
|
||||||
let createOrUpdate$: Observable<MetadataSchema>;
|
return this.registryService.createOrUpdateMetadataSchema(Object.assign(new MetadataSchema(), schema, {
|
||||||
|
namespace: metadataValues.namespace,
|
||||||
if (schema == null) {
|
}));
|
||||||
createOrUpdate$ =
|
}
|
||||||
this.registryService.createOrUpdateMetadataSchema(
|
}),
|
||||||
Object.assign(new MetadataSchema(), metadataValues),
|
switchMap((updatedOrCreatedSchema: MetadataSchema) => this.registryService.clearMetadataSchemaRequests().pipe(
|
||||||
);
|
map(() => updatedOrCreatedSchema),
|
||||||
} else {
|
)),
|
||||||
const updatedSchema = Object.assign(
|
).subscribe((updatedOrCreatedSchema: MetadataSchema) => {
|
||||||
new MetadataSchema(),
|
this.submitForm.emit(updatedOrCreatedSchema);
|
||||||
schema,
|
this.clearFields();
|
||||||
{
|
this.registryService.cancelEditMetadataSchema();
|
||||||
namespace: metadataValues.namespace,
|
});
|
||||||
},
|
|
||||||
);
|
|
||||||
createOrUpdate$ =
|
|
||||||
this.registryService.createOrUpdateMetadataSchema(
|
|
||||||
updatedSchema,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return createOrUpdate$;
|
|
||||||
}),
|
|
||||||
tap(() => {
|
|
||||||
this.registryService.clearMetadataSchemaRequests().subscribe();
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.subscribe((updatedOrCreatedSchema: MetadataSchema) => {
|
|
||||||
this.submitForm.emit(updatedOrCreatedSchema);
|
|
||||||
this.clearFields();
|
|
||||||
this.registryService.cancelEditMetadataSchema();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -233,5 +221,6 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.onCancel();
|
this.onCancel();
|
||||||
|
this.subscriptions.forEach((subscription: Subscription) => subscription.unsubscribe());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
import {
|
import {
|
||||||
ComponentFixture,
|
ComponentFixture,
|
||||||
inject,
|
inject,
|
||||||
@@ -17,13 +17,15 @@ import { RegistryService } from '../../../../core/registry/registry.service';
|
|||||||
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
|
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
|
||||||
import { FormComponent } from '../../../../shared/form/form.component';
|
import { FormComponent } from '../../../../shared/form/form.component';
|
||||||
import { getMockFormBuilderService } from '../../../../shared/mocks/form-builder-service.mock';
|
import { getMockFormBuilderService } from '../../../../shared/mocks/form-builder-service.mock';
|
||||||
|
import { RegistryServiceStub } from '../../../../shared/testing/registry.service.stub';
|
||||||
import { EnumKeysPipe } from '../../../../shared/utils/enum-keys-pipe';
|
import { EnumKeysPipe } from '../../../../shared/utils/enum-keys-pipe';
|
||||||
import { MetadataFieldFormComponent } from './metadata-field-form.component';
|
import { MetadataFieldFormComponent } from './metadata-field-form.component';
|
||||||
|
|
||||||
describe('MetadataFieldFormComponent', () => {
|
describe('MetadataFieldFormComponent', () => {
|
||||||
let component: MetadataFieldFormComponent;
|
let component: MetadataFieldFormComponent;
|
||||||
let fixture: ComponentFixture<MetadataFieldFormComponent>;
|
let fixture: ComponentFixture<MetadataFieldFormComponent>;
|
||||||
let registryService: RegistryService;
|
|
||||||
|
let registryService: RegistryServiceStub;
|
||||||
|
|
||||||
const metadataSchema = Object.assign(new MetadataSchema(), {
|
const metadataSchema = Object.assign(new MetadataSchema(), {
|
||||||
id: 1,
|
id: 1,
|
||||||
@@ -31,37 +33,16 @@ describe('MetadataFieldFormComponent', () => {
|
|||||||
prefix: 'fake',
|
prefix: 'fake',
|
||||||
});
|
});
|
||||||
|
|
||||||
/* eslint-disable no-empty,@typescript-eslint/no-empty-function */
|
|
||||||
const registryServiceStub = {
|
|
||||||
getActiveMetadataField: () => observableOf(undefined),
|
|
||||||
createMetadataField: (field: MetadataField) => observableOf(field),
|
|
||||||
updateMetadataField: (field: MetadataField) => observableOf(field),
|
|
||||||
cancelEditMetadataField: () => {
|
|
||||||
},
|
|
||||||
cancelEditMetadataSchema: () => {
|
|
||||||
},
|
|
||||||
clearMetadataFieldRequests: () => observableOf(undefined),
|
|
||||||
};
|
|
||||||
const formBuilderServiceStub = {
|
|
||||||
createFormGroup: () => {
|
|
||||||
return {
|
|
||||||
patchValue: () => {
|
|
||||||
},
|
|
||||||
reset(_value?: any, _options?: { onlySelf?: boolean; emitEvent?: boolean; }): void {
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
/* eslint-enable no-empty, @typescript-eslint/no-empty-function */
|
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
|
registryService = new RegistryServiceStub();
|
||||||
|
|
||||||
return TestBed.configureTestingModule({
|
return TestBed.configureTestingModule({
|
||||||
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule, MetadataFieldFormComponent, EnumKeysPipe],
|
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule, MetadataFieldFormComponent, EnumKeysPipe],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: RegistryService, useValue: registryServiceStub },
|
{ provide: RegistryService, useValue: registryService },
|
||||||
{ provide: FormBuilderService, useValue: getMockFormBuilderService() },
|
{ provide: FormBuilderService, useValue: getMockFormBuilderService() },
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA],
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||||
})
|
})
|
||||||
.overrideComponent(MetadataFieldFormComponent, {
|
.overrideComponent(MetadataFieldFormComponent, {
|
||||||
remove: { imports: [FormComponent] },
|
remove: { imports: [FormComponent] },
|
||||||
|
@@ -31,8 +31,8 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let field of fields?.page"
|
<tr *ngFor="let field of fields?.page"
|
||||||
[ngClass]="{'table-primary' : isActive(field) | async}">
|
[ngClass]="{'table-primary' : (activeField$ | async)?.id === field.id}">
|
||||||
<td *ngVar="(isSelected(field) | async) as selected">
|
<td *ngVar="(selectedMetadataFieldIDs$ | async)?.includes(field.id) as selected">
|
||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
[attr.aria-label]="(selected ? 'admin.registries.schema.fields.deselect' : 'admin.registries.schema.fields.select') | translate"
|
[attr.aria-label]="(selected ? 'admin.registries.schema.fields.deselect' : 'admin.registries.schema.fields.select') | translate"
|
||||||
[checked]="selected"
|
[checked]="selected"
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
import {
|
import {
|
||||||
ComponentFixture,
|
ComponentFixture,
|
||||||
inject,
|
inject,
|
||||||
@@ -7,16 +7,12 @@ import {
|
|||||||
waitForAsync,
|
waitForAsync,
|
||||||
} from '@angular/core/testing';
|
} from '@angular/core/testing';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import {
|
import { ActivatedRoute } from '@angular/router';
|
||||||
ActivatedRoute,
|
|
||||||
Router,
|
|
||||||
} from '@angular/router';
|
|
||||||
import { RouterTestingModule } from '@angular/router/testing';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
import { RestResponse } from '../../../core/cache/response.models';
|
|
||||||
import { ConfigurationDataService } from '../../../core/data/configuration-data.service';
|
import { ConfigurationDataService } from '../../../core/data/configuration-data.service';
|
||||||
import { buildPaginatedList } from '../../../core/data/paginated-list.model';
|
import { buildPaginatedList } from '../../../core/data/paginated-list.model';
|
||||||
import { GroupDataService } from '../../../core/eperson/group-data.service';
|
import { GroupDataService } from '../../../core/eperson/group-data.service';
|
||||||
@@ -34,7 +30,7 @@ import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub';
|
|||||||
import { HostWindowServiceStub } from '../../../shared/testing/host-window-service.stub';
|
import { HostWindowServiceStub } from '../../../shared/testing/host-window-service.stub';
|
||||||
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
|
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
|
||||||
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
|
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
|
||||||
import { RouterStub } from '../../../shared/testing/router.stub';
|
import { RegistryServiceStub } from '../../../shared/testing/registry.service.stub';
|
||||||
import { SearchConfigurationServiceStub } from '../../../shared/testing/search-configuration-service.stub';
|
import { SearchConfigurationServiceStub } from '../../../shared/testing/search-configuration-service.stub';
|
||||||
import { createPaginatedList } from '../../../shared/testing/utils.test';
|
import { createPaginatedList } from '../../../shared/testing/utils.test';
|
||||||
import { EnumKeysPipe } from '../../../shared/utils/enum-keys-pipe';
|
import { EnumKeysPipe } from '../../../shared/utils/enum-keys-pipe';
|
||||||
@@ -45,8 +41,12 @@ import { MetadataSchemaComponent } from './metadata-schema.component';
|
|||||||
describe('MetadataSchemaComponent', () => {
|
describe('MetadataSchemaComponent', () => {
|
||||||
let comp: MetadataSchemaComponent;
|
let comp: MetadataSchemaComponent;
|
||||||
let fixture: ComponentFixture<MetadataSchemaComponent>;
|
let fixture: ComponentFixture<MetadataSchemaComponent>;
|
||||||
let registryService: RegistryService;
|
|
||||||
const mockSchemasList = [
|
let registryService: RegistryServiceStub;
|
||||||
|
let activatedRoute: ActivatedRouteStub;
|
||||||
|
let paginationService: PaginationServiceStub;
|
||||||
|
|
||||||
|
const mockSchemasList: MetadataSchema[] = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
_links: {
|
_links: {
|
||||||
@@ -67,8 +67,8 @@ describe('MetadataSchemaComponent', () => {
|
|||||||
prefix: 'mock',
|
prefix: 'mock',
|
||||||
namespace: 'http://dspace.org/mockschema',
|
namespace: 'http://dspace.org/mockschema',
|
||||||
},
|
},
|
||||||
];
|
] as MetadataSchema[];
|
||||||
const mockFieldsList = [
|
const mockFieldsList: MetadataField[] = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
_links: {
|
_links: {
|
||||||
@@ -117,33 +117,8 @@ describe('MetadataSchemaComponent', () => {
|
|||||||
scopeNote: null,
|
scopeNote: null,
|
||||||
schema: createSuccessfulRemoteDataObject$(mockSchemasList[1]),
|
schema: createSuccessfulRemoteDataObject$(mockSchemasList[1]),
|
||||||
},
|
},
|
||||||
];
|
] as MetadataField[];
|
||||||
const mockSchemas = createSuccessfulRemoteDataObject$(buildPaginatedList(null, mockSchemasList));
|
|
||||||
/* eslint-disable no-empty,@typescript-eslint/no-empty-function */
|
|
||||||
const registryServiceStub = {
|
|
||||||
getMetadataSchemas: () => mockSchemas,
|
|
||||||
getMetadataFieldsBySchema: (schema: MetadataSchema) => createSuccessfulRemoteDataObject$(buildPaginatedList(null, mockFieldsList.filter((value) => value.id === 3 || value.id === 4))),
|
|
||||||
getMetadataSchemaByPrefix: (schemaName: string) => createSuccessfulRemoteDataObject$(mockSchemasList.filter((value) => value.prefix === schemaName)[0]),
|
|
||||||
getActiveMetadataField: () => observableOf(undefined),
|
|
||||||
getSelectedMetadataFields: () => observableOf([]),
|
|
||||||
editMetadataField: (schema) => {
|
|
||||||
},
|
|
||||||
cancelEditMetadataField: () => {
|
|
||||||
},
|
|
||||||
deleteMetadataField: () => observableOf(new RestResponse(true, 200, 'OK')),
|
|
||||||
deselectAllMetadataField: () => {
|
|
||||||
},
|
|
||||||
clearMetadataFieldRequests: () => observableOf(undefined),
|
|
||||||
};
|
|
||||||
/* eslint-enable no-empty, @typescript-eslint/no-empty-function */
|
|
||||||
const schemaNameParam = 'mock';
|
const schemaNameParam = 'mock';
|
||||||
const activatedRouteStub = Object.assign(new ActivatedRouteStub(), {
|
|
||||||
params: observableOf({
|
|
||||||
schemaName: schemaNameParam,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const paginationService = new PaginationServiceStub();
|
|
||||||
|
|
||||||
const configurationDataService = jasmine.createSpyObj('configurationDataService', {
|
const configurationDataService = jasmine.createSpyObj('configurationDataService', {
|
||||||
findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), {
|
findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), {
|
||||||
@@ -162,6 +137,14 @@ describe('MetadataSchemaComponent', () => {
|
|||||||
|
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
|
activatedRoute = new ActivatedRouteStub({
|
||||||
|
schemaName: schemaNameParam,
|
||||||
|
});
|
||||||
|
paginationService = new PaginationServiceStub();
|
||||||
|
registryService = new RegistryServiceStub();
|
||||||
|
spyOn(registryService, 'getMetadataFieldsBySchema').and.returnValue(createSuccessfulRemoteDataObject$(buildPaginatedList(null, mockFieldsList.filter((value) => value.id === 3 || value.id === 4))));
|
||||||
|
spyOn(registryService, 'getMetadataSchemaByPrefix').and.callFake((schemaName) => createSuccessfulRemoteDataObject$(mockSchemasList.filter((value) => value.prefix === schemaName)[0]));
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
@@ -174,10 +157,9 @@ describe('MetadataSchemaComponent', () => {
|
|||||||
VarDirective,
|
VarDirective,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: RegistryService, useValue: registryServiceStub },
|
{ provide: RegistryService, useValue: registryService },
|
||||||
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
{ provide: ActivatedRoute, useValue: activatedRoute },
|
||||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
|
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
|
||||||
{ provide: Router, useValue: new RouterStub() },
|
|
||||||
{ provide: PaginationService, useValue: paginationService },
|
{ provide: PaginationService, useValue: paginationService },
|
||||||
{
|
{
|
||||||
provide: NotificationsService,
|
provide: NotificationsService,
|
||||||
@@ -187,7 +169,7 @@ describe('MetadataSchemaComponent', () => {
|
|||||||
{ provide: ConfigurationDataService, useValue: configurationDataService },
|
{ provide: ConfigurationDataService, useValue: configurationDataService },
|
||||||
{ provide: SearchConfigurationService, useValue: new SearchConfigurationServiceStub() },
|
{ provide: SearchConfigurationService, useValue: new SearchConfigurationServiceStub() },
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA],
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||||
})
|
})
|
||||||
.overrideComponent(MetadataSchemaComponent, {
|
.overrideComponent(MetadataSchemaComponent, {
|
||||||
remove: {
|
remove: {
|
||||||
@@ -242,7 +224,7 @@ describe('MetadataSchemaComponent', () => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
it('should cancel editing the selected field when clicked again', waitForAsync(() => {
|
it('should cancel editing the selected field when clicked again', waitForAsync(() => {
|
||||||
spyOn(registryService, 'getActiveMetadataField').and.returnValue(observableOf(mockFieldsList[2] as MetadataField));
|
comp.activeField$ = observableOf(mockFieldsList[2] as MetadataField);
|
||||||
spyOn(registryService, 'cancelEditMetadataField');
|
spyOn(registryService, 'cancelEditMetadataField');
|
||||||
row.click();
|
row.click();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
@@ -257,7 +239,7 @@ describe('MetadataSchemaComponent', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(registryService, 'deleteMetadataField').and.callThrough();
|
spyOn(registryService, 'deleteMetadataField').and.callThrough();
|
||||||
spyOn(registryService, 'getSelectedMetadataFields').and.returnValue(observableOf(selectedFields as MetadataField[]));
|
comp.selectedMetadataFieldIDs$ = observableOf(selectedFields.map((metadataField: MetadataField) => metadataField.id));
|
||||||
comp.deleteFields();
|
comp.deleteFields();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
@@ -20,9 +20,9 @@ import {
|
|||||||
import {
|
import {
|
||||||
BehaviorSubject,
|
BehaviorSubject,
|
||||||
combineLatest,
|
combineLatest,
|
||||||
combineLatest as observableCombineLatest,
|
|
||||||
Observable,
|
Observable,
|
||||||
of as observableOf,
|
of as observableOf,
|
||||||
|
Subscription,
|
||||||
zip,
|
zip,
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import {
|
import {
|
||||||
@@ -42,7 +42,6 @@ import {
|
|||||||
getFirstCompletedRemoteData,
|
getFirstCompletedRemoteData,
|
||||||
getFirstSucceededRemoteDataPayload,
|
getFirstSucceededRemoteDataPayload,
|
||||||
} from '../../../core/shared/operators';
|
} from '../../../core/shared/operators';
|
||||||
import { hasValue } from '../../../shared/empty.util';
|
|
||||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
import { PaginationComponent } from '../../../shared/pagination/pagination.component';
|
import { PaginationComponent } from '../../../shared/pagination/pagination.component';
|
||||||
import { toFindListOptions } from '../../../shared/pagination/pagination.utils';
|
import { toFindListOptions } from '../../../shared/pagination/pagination.utils';
|
||||||
@@ -71,7 +70,7 @@ import { MetadataFieldFormComponent } from './metadata-field-form/metadata-field
|
|||||||
* A component used for managing all existing metadata fields within the current metadata schema.
|
* A component used for managing all existing metadata fields within the current metadata schema.
|
||||||
* The admin can create, edit or delete metadata fields here.
|
* The admin can create, edit or delete metadata fields here.
|
||||||
*/
|
*/
|
||||||
export class MetadataSchemaComponent implements OnInit, OnDestroy {
|
export class MetadataSchemaComponent implements OnDestroy, OnInit {
|
||||||
/**
|
/**
|
||||||
* The metadata schema
|
* The metadata schema
|
||||||
*/
|
*/
|
||||||
@@ -96,26 +95,33 @@ export class MetadataSchemaComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
needsUpdate$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
|
needsUpdate$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
|
||||||
|
|
||||||
constructor(private registryService: RegistryService,
|
/**
|
||||||
private route: ActivatedRoute,
|
* The current {@link MetadataField} that is being edited
|
||||||
private notificationsService: NotificationsService,
|
*/
|
||||||
private paginationService: PaginationService,
|
activeField$: Observable<MetadataField>;
|
||||||
private translateService: TranslateService) {
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The selected {@link MetadataField} IDs
|
||||||
|
*/
|
||||||
|
selectedMetadataFieldIDs$: Observable<number[]>;
|
||||||
|
|
||||||
|
subscriptions: Subscription[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected registryService: RegistryService,
|
||||||
|
protected route: ActivatedRoute,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected paginationService: PaginationService,
|
||||||
|
protected translateService: TranslateService,
|
||||||
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.route.params.subscribe((params) => {
|
this.metadataSchema$ = this.registryService.getMetadataSchemaByPrefix(this.route.snapshot.params.schemaName).pipe(getFirstSucceededRemoteDataPayload());
|
||||||
this.initialize(params);
|
this.activeField$ = this.registryService.getActiveMetadataField();
|
||||||
});
|
this.selectedMetadataFieldIDs$ = this.registryService.getSelectedMetadataFields().pipe(
|
||||||
}
|
map((metadataFields: MetadataField[]) => metadataFields.map((metadataField: MetadataField) => metadataField.id)),
|
||||||
|
);
|
||||||
/**
|
|
||||||
* Initialize the component using the params within the url (schemaName)
|
|
||||||
* @param params
|
|
||||||
*/
|
|
||||||
initialize(params) {
|
|
||||||
this.metadataSchema$ = this.registryService.getMetadataSchemaByPrefix(params.schemaName).pipe(getFirstSucceededRemoteDataPayload());
|
|
||||||
this.updateFields();
|
this.updateFields();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,30 +154,13 @@ export class MetadataSchemaComponent implements OnInit, OnDestroy {
|
|||||||
* @param field
|
* @param field
|
||||||
*/
|
*/
|
||||||
editField(field: MetadataField) {
|
editField(field: MetadataField) {
|
||||||
this.getActiveField().pipe(take(1)).subscribe((activeField) => {
|
this.subscriptions.push(this.activeField$.pipe(take(1)).subscribe((activeField) => {
|
||||||
if (field === activeField) {
|
if (field === activeField) {
|
||||||
this.registryService.cancelEditMetadataField();
|
this.registryService.cancelEditMetadataField();
|
||||||
} else {
|
} else {
|
||||||
this.registryService.editMetadataField(field);
|
this.registryService.editMetadataField(field);
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether the given metadata field is active (being edited)
|
|
||||||
* @param field
|
|
||||||
*/
|
|
||||||
isActive(field: MetadataField): Observable<boolean> {
|
|
||||||
return this.getActiveField().pipe(
|
|
||||||
map((activeField) => field === activeField),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the active metadata field (being edited)
|
|
||||||
*/
|
|
||||||
getActiveField(): Observable<MetadataField> {
|
|
||||||
return this.registryService.getActiveMetadataField();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -185,42 +174,25 @@ export class MetadataSchemaComponent implements OnInit, OnDestroy {
|
|||||||
this.registryService.deselectMetadataField(field);
|
this.registryService.deselectMetadataField(field);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether a given metadata field is selected in the list (checkbox)
|
|
||||||
* @param field
|
|
||||||
*/
|
|
||||||
isSelected(field: MetadataField): Observable<boolean> {
|
|
||||||
return this.registryService.getSelectedMetadataFields().pipe(
|
|
||||||
map((fields) => fields.find((selectedField) => selectedField === field) != null),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete all the selected metadata fields
|
* Delete all the selected metadata fields
|
||||||
*/
|
*/
|
||||||
deleteFields() {
|
deleteFields() {
|
||||||
this.registryService.getSelectedMetadataFields().pipe(take(1)).subscribe(
|
this.subscriptions.push(this.selectedMetadataFieldIDs$.pipe(
|
||||||
(fields) => {
|
take(1),
|
||||||
const tasks$ = [];
|
switchMap((fieldIDs) => zip(fieldIDs.map((fieldID) => this.registryService.deleteMetadataField(fieldID).pipe(getFirstCompletedRemoteData())))),
|
||||||
for (const field of fields) {
|
).subscribe((responses: RemoteData<NoContent>[]) => {
|
||||||
if (hasValue(field.id)) {
|
const successResponses = responses.filter((response: RemoteData<NoContent>) => response.hasSucceeded);
|
||||||
tasks$.push(this.registryService.deleteMetadataField(field.id).pipe(getFirstCompletedRemoteData()));
|
const failedResponses = responses.filter((response: RemoteData<NoContent>) => response.hasFailed);
|
||||||
}
|
if (successResponses.length > 0) {
|
||||||
}
|
this.showNotification(true, successResponses.length);
|
||||||
zip(...tasks$).subscribe((responses: RemoteData<NoContent>[]) => {
|
}
|
||||||
const successResponses = responses.filter((response: RemoteData<NoContent>) => response.hasSucceeded);
|
if (failedResponses.length > 0) {
|
||||||
const failedResponses = responses.filter((response: RemoteData<NoContent>) => response.hasFailed);
|
this.showNotification(false, failedResponses.length);
|
||||||
if (successResponses.length > 0) {
|
}
|
||||||
this.showNotification(true, successResponses.length);
|
this.registryService.deselectAllMetadataField();
|
||||||
}
|
this.registryService.cancelEditMetadataField();
|
||||||
if (failedResponses.length > 0) {
|
}));
|
||||||
this.showNotification(false, failedResponses.length);
|
|
||||||
}
|
|
||||||
this.registryService.deselectAllMetadataField();
|
|
||||||
this.registryService.cancelEditMetadataField();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -231,21 +203,19 @@ export class MetadataSchemaComponent implements OnInit, OnDestroy {
|
|||||||
showNotification(success: boolean, amount: number) {
|
showNotification(success: boolean, amount: number) {
|
||||||
const prefix = 'admin.registries.schema.notification';
|
const prefix = 'admin.registries.schema.notification';
|
||||||
const suffix = success ? 'success' : 'failure';
|
const suffix = success ? 'success' : 'failure';
|
||||||
const messages = observableCombineLatest([
|
const head = this.translateService.instant(success ? `${prefix}.${suffix}` : `${prefix}.${suffix}`);
|
||||||
this.translateService.get(success ? `${prefix}.${suffix}` : `${prefix}.${suffix}`),
|
const content = this.translateService.instant(`${prefix}.field.deleted.${suffix}`, { amount: amount });
|
||||||
this.translateService.get(`${prefix}.field.deleted.${suffix}`, { amount: amount }),
|
if (success) {
|
||||||
]);
|
this.notificationsService.success(head, content);
|
||||||
messages.subscribe(([head, content]) => {
|
} else {
|
||||||
if (success) {
|
this.notificationsService.error(head, content);
|
||||||
this.notificationsService.success(head, content);
|
}
|
||||||
} else {
|
|
||||||
this.notificationsService.error(head, content);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.paginationService.clearPagination(this.config.id);
|
this.paginationService.clearPagination(this.config.id);
|
||||||
this.registryService.deselectAllMetadataField();
|
this.registryService.deselectAllMetadataField();
|
||||||
|
this.subscriptions.forEach((subscription: Subscription) => subscription.unsubscribe());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -4,6 +4,7 @@ import {
|
|||||||
} from '@angular/common';
|
} from '@angular/common';
|
||||||
import {
|
import {
|
||||||
Component,
|
Component,
|
||||||
|
OnInit,
|
||||||
ViewChild,
|
ViewChild,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import {
|
import {
|
||||||
@@ -40,7 +41,7 @@ import { FilteredCollections } from './filtered-collections.model';
|
|||||||
],
|
],
|
||||||
standalone: true,
|
standalone: true,
|
||||||
})
|
})
|
||||||
export class FilteredCollectionsComponent {
|
export class FilteredCollectionsComponent implements OnInit {
|
||||||
|
|
||||||
queryForm: FormGroup;
|
queryForm: FormGroup;
|
||||||
results: FilteredCollections = new FilteredCollections();
|
results: FilteredCollections = new FilteredCollections();
|
||||||
|
@@ -5,6 +5,7 @@ import {
|
|||||||
} from '@angular/common';
|
} from '@angular/common';
|
||||||
import {
|
import {
|
||||||
Component,
|
Component,
|
||||||
|
OnInit,
|
||||||
ViewChild,
|
ViewChild,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import {
|
import {
|
||||||
@@ -66,7 +67,7 @@ import { QueryPredicate } from './query-predicate.model';
|
|||||||
],
|
],
|
||||||
standalone: true,
|
standalone: true,
|
||||||
})
|
})
|
||||||
export class FilteredItemsComponent {
|
export class FilteredItemsComponent implements OnInit {
|
||||||
|
|
||||||
collections: OptionVO[];
|
collections: OptionVO[];
|
||||||
presetQueries: PresetQuery[];
|
presetQueries: PresetQuery[];
|
||||||
@@ -90,7 +91,7 @@ export class FilteredItemsComponent {
|
|||||||
private formBuilder: FormBuilder,
|
private formBuilder: FormBuilder,
|
||||||
private restService: DspaceRestService) {}
|
private restService: DspaceRestService) {}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit(): void {
|
||||||
this.loadCollections();
|
this.loadCollections();
|
||||||
this.loadPresetQueries();
|
this.loadPresetQueries();
|
||||||
this.loadMetadataFields();
|
this.loadMetadataFields();
|
||||||
|
@@ -11,8 +11,8 @@ import {
|
|||||||
REGISTRIES_MODULE_PATH,
|
REGISTRIES_MODULE_PATH,
|
||||||
REPORTS_MODULE_PATH,
|
REPORTS_MODULE_PATH,
|
||||||
} from './admin-routing-paths';
|
} from './admin-routing-paths';
|
||||||
import { AdminSearchPageComponent } from './admin-search-page/admin-search-page.component';
|
import { ThemedAdminSearchPageComponent } from './admin-search-page/themed-admin-search-page.component';
|
||||||
import { AdminWorkflowPageComponent } from './admin-workflow-page/admin-workflow-page.component';
|
import { ThemedAdminWorkflowPageComponent } from './admin-workflow-page/themed-admin-workflow-page.component';
|
||||||
|
|
||||||
export const ROUTES: Route[] = [
|
export const ROUTES: Route[] = [
|
||||||
{
|
{
|
||||||
@@ -28,13 +28,13 @@ export const ROUTES: Route[] = [
|
|||||||
{
|
{
|
||||||
path: 'search',
|
path: 'search',
|
||||||
resolve: { breadcrumb: i18nBreadcrumbResolver },
|
resolve: { breadcrumb: i18nBreadcrumbResolver },
|
||||||
component: AdminSearchPageComponent,
|
component: ThemedAdminSearchPageComponent,
|
||||||
data: { title: 'admin.search.title', breadcrumbKey: 'admin.search' },
|
data: { title: 'admin.search.title', breadcrumbKey: 'admin.search' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'workflow',
|
path: 'workflow',
|
||||||
resolve: { breadcrumb: i18nBreadcrumbResolver },
|
resolve: { breadcrumb: i18nBreadcrumbResolver },
|
||||||
component: AdminWorkflowPageComponent,
|
component: ThemedAdminWorkflowPageComponent,
|
||||||
data: { title: 'admin.workflow.title', breadcrumbKey: 'admin.workflow' },
|
data: { title: 'admin.workflow.title', breadcrumbKey: 'admin.workflow' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@@ -4,7 +4,7 @@ import { Context } from '../../core/shared/context.model';
|
|||||||
import { ThemedConfigurationSearchPageComponent } from '../../search-page/themed-configuration-search-page.component';
|
import { ThemedConfigurationSearchPageComponent } from '../../search-page/themed-configuration-search-page.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-admin-search-page',
|
selector: 'ds-base-admin-search-page',
|
||||||
templateUrl: './admin-search-page.component.html',
|
templateUrl: './admin-search-page.component.html',
|
||||||
styleUrls: ['./admin-search-page.component.scss'],
|
styleUrls: ['./admin-search-page.component.scss'],
|
||||||
standalone: true,
|
standalone: true,
|
||||||
|
@@ -1,4 +1,7 @@
|
|||||||
import { Component } from '@angular/core';
|
import {
|
||||||
|
Component,
|
||||||
|
OnInit,
|
||||||
|
} from '@angular/core';
|
||||||
import { RouterLink } from '@angular/router';
|
import { RouterLink } from '@angular/router';
|
||||||
|
|
||||||
import { getCollectionEditRoute } from '../../../../../collection-page/collection-page-routing-paths';
|
import { getCollectionEditRoute } from '../../../../../collection-page/collection-page-routing-paths';
|
||||||
@@ -21,10 +24,10 @@ import { SearchResultGridElementComponent } from '../../../../../shared/object-g
|
|||||||
/**
|
/**
|
||||||
* The component for displaying a list element for a collection search result on the admin search page
|
* The component for displaying a list element for a collection search result on the admin search page
|
||||||
*/
|
*/
|
||||||
export class CollectionAdminSearchResultGridElementComponent extends SearchResultGridElementComponent<CollectionSearchResult, Collection> {
|
export class CollectionAdminSearchResultGridElementComponent extends SearchResultGridElementComponent<CollectionSearchResult, Collection> implements OnInit {
|
||||||
editPath: string;
|
editPath: string;
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit(): void {
|
||||||
super.ngOnInit();
|
super.ngOnInit();
|
||||||
this.editPath = getCollectionEditRoute(this.dso.uuid);
|
this.editPath = getCollectionEditRoute(this.dso.uuid);
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,7 @@
|
|||||||
import { Component } from '@angular/core';
|
import {
|
||||||
|
Component,
|
||||||
|
OnInit,
|
||||||
|
} from '@angular/core';
|
||||||
import { RouterLink } from '@angular/router';
|
import { RouterLink } from '@angular/router';
|
||||||
|
|
||||||
import { getCommunityEditRoute } from '../../../../../community-page/community-page-routing-paths';
|
import { getCommunityEditRoute } from '../../../../../community-page/community-page-routing-paths';
|
||||||
@@ -21,10 +24,10 @@ import { SearchResultGridElementComponent } from '../../../../../shared/object-g
|
|||||||
/**
|
/**
|
||||||
* The component for displaying a list element for a community search result on the admin search page
|
* The component for displaying a list element for a community search result on the admin search page
|
||||||
*/
|
*/
|
||||||
export class CommunityAdminSearchResultGridElementComponent extends SearchResultGridElementComponent<CommunitySearchResult, Community> {
|
export class CommunityAdminSearchResultGridElementComponent extends SearchResultGridElementComponent<CommunitySearchResult, Community> implements OnInit {
|
||||||
editPath: string;
|
editPath: string;
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit(): void {
|
||||||
super.ngOnInit();
|
super.ngOnInit();
|
||||||
this.editPath = getCommunityEditRoute(this.dso.uuid);
|
this.editPath = getCommunityEditRoute(this.dso.uuid);
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,7 @@
|
|||||||
import { Component } from '@angular/core';
|
import {
|
||||||
|
Component,
|
||||||
|
OnInit,
|
||||||
|
} from '@angular/core';
|
||||||
import { RouterLink } from '@angular/router';
|
import { RouterLink } from '@angular/router';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
@@ -22,10 +25,10 @@ import { SearchResultListElementComponent } from '../../../../../shared/object-l
|
|||||||
/**
|
/**
|
||||||
* The component for displaying a list element for a collection search result on the admin search page
|
* The component for displaying a list element for a collection search result on the admin search page
|
||||||
*/
|
*/
|
||||||
export class CollectionAdminSearchResultListElementComponent extends SearchResultListElementComponent<CollectionSearchResult, Collection> {
|
export class CollectionAdminSearchResultListElementComponent extends SearchResultListElementComponent<CollectionSearchResult, Collection> implements OnInit {
|
||||||
editPath: string;
|
editPath: string;
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit(): void {
|
||||||
super.ngOnInit();
|
super.ngOnInit();
|
||||||
this.editPath = getCollectionEditRoute(this.dso.uuid);
|
this.editPath = getCollectionEditRoute(this.dso.uuid);
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,7 @@
|
|||||||
import { Component } from '@angular/core';
|
import {
|
||||||
|
Component,
|
||||||
|
OnInit,
|
||||||
|
} from '@angular/core';
|
||||||
import { RouterLink } from '@angular/router';
|
import { RouterLink } from '@angular/router';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
@@ -22,10 +25,10 @@ import { SearchResultListElementComponent } from '../../../../../shared/object-l
|
|||||||
/**
|
/**
|
||||||
* The component for displaying a list element for a community search result on the admin search page
|
* The component for displaying a list element for a community search result on the admin search page
|
||||||
*/
|
*/
|
||||||
export class CommunityAdminSearchResultListElementComponent extends SearchResultListElementComponent<CommunitySearchResult, Community> {
|
export class CommunityAdminSearchResultListElementComponent extends SearchResultListElementComponent<CommunitySearchResult, Community> implements OnInit {
|
||||||
editPath: string;
|
editPath: string;
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit(): void {
|
||||||
super.ngOnInit();
|
super.ngOnInit();
|
||||||
this.editPath = getCommunityEditRoute(this.dso.uuid);
|
this.editPath = getCommunityEditRoute(this.dso.uuid);
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user