Merge branch 'coar-notify-7' of bitbucket.org:4Science/dspace-angular into CST-11012-demo

This commit is contained in:
Sondissimo
2023-09-14 17:40:08 +02:00
910 changed files with 42672 additions and 8391 deletions

View File

@@ -1,17 +0,0 @@
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
# For the full list of supported browsers by the Angular framework, please see:
# https://angular.io/guide/browser-support
# You can see what browsers were selected by your queries by running:
# npx browserslist
last 1 Chrome version
last 1 Firefox version
last 2 Edge major versions
last 2 Safari major versions
last 2 iOS major versions
Firefox ESR
not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line.

View File

@@ -1,26 +0,0 @@
# This workflow runs whenever a new pull request is created
# TEMPORARILY DISABLED. Unfortunately this doesn't work for PRs created from forked repositories (which is how we tend to create PRs).
# There is no known workaround yet. See https://github.community/t/how-to-use-github-token-for-prs-from-forks/16818
name: Pull Request opened
# Only run for newly opened PRs against the "main" branch
on:
pull_request:
types: [opened]
branches:
- main
jobs:
automation:
runs-on: ubuntu-latest
steps:
# Assign the PR to whomever created it. This is useful for visualizing assignments on project boards
# See https://github.com/marketplace/actions/pull-request-assigner
- name: Assign PR to creator
uses: thomaseizinger/assign-pr-creator-action@v1.0.0
# Note, this authentication token is created automatically
# See: https://docs.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
# Ignore errors. It is possible the PR was created by someone who cannot be assigned
continue-on-error: true

View File

@@ -31,6 +31,8 @@ jobs:
# When Chrome version is specified, we pin to a specific version of Chrome # When Chrome version is specified, we pin to a specific version of Chrome
# Comment this out to use the latest release # Comment this out to use the latest release
#CHROME_VERSION: "90.0.4430.212-1" #CHROME_VERSION: "90.0.4430.212-1"
# Bump Node heap size (OOM in CI after upgrading to Angular 15)
NODE_OPTIONS: '--max-old-space-size=4096'
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:

View File

@@ -5,12 +5,16 @@
# because CodeQL requires a fresh build with all tests *disabled*. # because CodeQL requires a fresh build with all tests *disabled*.
name: "Code Scanning" name: "Code Scanning"
# Run this code scan for all pushes / PRs to main branch. Also run once a week. # Run this code scan for all pushes / PRs to main or maintenance branches. Also run once a week.
on: on:
push: push:
branches: [ main ] branches:
- main
- 'dspace-**'
pull_request: pull_request:
branches: [ main ] branches:
- main
- 'dspace-**'
# Don't run if PR is only updating static documentation # Don't run if PR is only updating static documentation
paths-ignore: paths-ignore:
- '**/*.md' - '**/*.md'

View File

@@ -15,29 +15,35 @@ on:
permissions: permissions:
contents: read # to fetch code (actions/checkout) contents: read # to fetch code (actions/checkout)
env:
# Define tags to use for Docker images based on Git tags/branches (for docker/metadata-action)
# For a new commit on default branch (main), use the literal tag 'latest' on Docker image.
# For a new commit on other branches, use the branch name as the tag for Docker image.
# For a new tag, copy that tag name as the tag for Docker image.
IMAGE_TAGS: |
type=raw,value=latest,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }}
type=ref,event=branch,enable=${{ !endsWith(github.ref, github.event.repository.default_branch) }}
type=ref,event=tag
# Define default tag "flavor" for docker/metadata-action per
# https://github.com/docker/metadata-action#flavor-input
# We manage the 'latest' tag ourselves to the 'main' branch (see settings above)
TAGS_FLAVOR: |
latest=false
# Architectures / Platforms for which we will build Docker images
# If this is a PR, we ONLY build for AMD64. For PRs we only do a sanity check test to ensure Docker builds work.
# If this is NOT a PR (e.g. a tag or merge commit), also build for ARM64.
PLATFORMS: linux/amd64${{ github.event_name != 'pull_request' && ', linux/arm64' || '' }}
jobs: jobs:
docker: ###############################################
# Build/Push the 'dspace/dspace-angular' image
###############################################
dspace-angular:
# Ensure this job never runs on forked repos. It's only executed for 'dspace/dspace-angular' # Ensure this job never runs on forked repos. It's only executed for 'dspace/dspace-angular'
if: github.repository == 'dspace/dspace-angular' if: github.repository == 'dspace/dspace-angular'
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
# Define tags to use for Docker images based on Git tags/branches (for docker/metadata-action)
# For a new commit on default branch (main), use the literal tag 'dspace-7_x' on Docker image.
# For a new commit on other branches, use the branch name as the tag for Docker image.
# For a new tag, copy that tag name as the tag for Docker image.
IMAGE_TAGS: |
type=raw,value=dspace-7_x,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }}
type=ref,event=branch,enable=${{ !endsWith(github.ref, github.event.repository.default_branch) }}
type=ref,event=tag
# Define default tag "flavor" for docker/metadata-action per
# https://github.com/docker/metadata-action#flavor-input
# We turn off 'latest' tag by default.
TAGS_FLAVOR: |
latest=false
# Architectures / Platforms for which we will build Docker images
# If this is a PR, we ONLY build for AMD64. For PRs we only do a sanity check test to ensure Docker builds work.
# If this is NOT a PR (e.g. a tag or merge commit), also build for ARM64.
PLATFORMS: linux/amd64${{ github.event_name != 'pull_request' && ', linux/arm64' || '' }}
steps: steps:
# https://github.com/actions/checkout # https://github.com/actions/checkout
@@ -61,9 +67,6 @@ jobs:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_ACCESS_TOKEN }} password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
###############################################
# Build/Push the 'dspace/dspace-angular' image
###############################################
# https://github.com/docker/metadata-action # https://github.com/docker/metadata-action
# Get Metadata for docker_build step below # Get Metadata for docker_build step below
- name: Sync metadata (tags, labels) from GitHub to Docker for 'dspace-angular' image - name: Sync metadata (tags, labels) from GitHub to Docker for 'dspace-angular' image
@@ -77,7 +80,7 @@ jobs:
# https://github.com/docker/build-push-action # https://github.com/docker/build-push-action
- name: Build and push 'dspace-angular' image - name: Build and push 'dspace-angular' image
id: docker_build id: docker_build
uses: docker/build-push-action@v3 uses: docker/build-push-action@v4
with: with:
context: . context: .
file: ./Dockerfile file: ./Dockerfile
@@ -89,9 +92,36 @@ jobs:
tags: ${{ steps.meta_build.outputs.tags }} tags: ${{ steps.meta_build.outputs.tags }}
labels: ${{ steps.meta_build.outputs.labels }} labels: ${{ steps.meta_build.outputs.labels }}
##################################################### #############################################################
# Build/Push the 'dspace/dspace-angular' image ('-dist' tag) # Build/Push the 'dspace/dspace-angular' image ('-dist' tag)
##################################################### #############################################################
dspace-angular-dist:
# Ensure this job never runs on forked repos. It's only executed for 'dspace/dspace-angular'
if: github.repository == 'dspace/dspace-angular'
runs-on: ubuntu-latest
steps:
# https://github.com/actions/checkout
- name: Checkout codebase
uses: actions/checkout@v3
# https://github.com/docker/setup-buildx-action
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v2
# https://github.com/docker/setup-qemu-action
- name: Set up QEMU emulation to build for multiple architectures
uses: docker/setup-qemu-action@v2
# https://github.com/docker/login-action
- name: Login to DockerHub
# Only login if not a PR, as PRs only trigger a Docker build and not a push
if: github.event_name != 'pull_request'
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
# https://github.com/docker/metadata-action # https://github.com/docker/metadata-action
# Get Metadata for docker_build_dist step below # Get Metadata for docker_build_dist step below
- name: Sync metadata (tags, labels) from GitHub to Docker for 'dspace-angular-dist' image - name: Sync metadata (tags, labels) from GitHub to Docker for 'dspace-angular-dist' image
@@ -107,7 +137,7 @@ jobs:
- name: Build and push 'dspace-angular-dist' image - name: Build and push 'dspace-angular-dist' image
id: docker_build_dist id: docker_build_dist
uses: docker/build-push-action@v3 uses: docker/build-push-action@v4
with: with:
context: . context: .
file: ./Dockerfile.dist file: ./Dockerfile.dist

View File

@@ -1,11 +1,12 @@
# This workflow checks open PRs for merge conflicts and labels them when conflicts are found # This workflow checks open PRs for merge conflicts and labels them when conflicts are found
name: Check for merge conflicts name: Check for merge conflicts
# Run whenever the "main" branch is updated # Run this for all pushes (i.e. merges) to 'main' or maintenance branches
# NOTE: This means merge conflicts are only checked for when a PR is merged to main.
on: on:
push: push:
branches: [ main ] branches:
- main
- 'dspace-**'
# So that the `conflict_label_name` is removed if conflicts are resolved, # So that the `conflict_label_name` is removed if conflicts are resolved,
# we allow this to run for `pull_request_target` so that github secrets are available. # we allow this to run for `pull_request_target` so that github secrets are available.
pull_request_target: pull_request_target:
@@ -24,6 +25,8 @@ jobs:
# See: https://github.com/prince-chrismc/label-merge-conflicts-action # See: https://github.com/prince-chrismc/label-merge-conflicts-action
- name: Auto-label PRs with merge conflicts - name: Auto-label PRs with merge conflicts
uses: prince-chrismc/label-merge-conflicts-action@v3 uses: prince-chrismc/label-merge-conflicts-action@v3
# Ignore any failures -- may occur (randomly?) for older, outdated PRs.
continue-on-error: true
# Add "merge conflict" label if a merge conflict is detected. Remove it when resolved. # Add "merge conflict" label if a merge conflict is detected. Remove it when resolved.
# Note, the authentication token is created automatically # Note, the authentication token is created automatically
# See: https://docs.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token # See: https://docs.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token

View File

@@ -0,0 +1,44 @@
# This workflow will attempt to port a merged pull request to
# the branch specified in a "port to" label (if exists)
name: Port merged Pull Request
# Only run for merged PRs against the "main" or maintenance branches
# We allow this to run for `pull_request_target` so that github secrets are available
# (This is required when the PR comes from a forked repo)
on:
pull_request_target:
types: [ closed ]
branches:
- main
- 'dspace-**'
permissions:
contents: write # so action can add comments
pull-requests: write # so action can create pull requests
jobs:
port_pr:
runs-on: ubuntu-latest
# Don't run on closed *unmerged* pull requests
if: github.event.pull_request.merged
steps:
# Checkout code
- uses: actions/checkout@v3
# Port PR to other branch (ONLY if labeled with "port to")
# See https://github.com/korthout/backport-action
- name: Create backport pull requests
uses: korthout/backport-action@v1
with:
# Trigger based on a "port to [branch]" label on PR
# (This label must specify the branch name to port to)
label_pattern: '^port to ([^ ]+)$'
# Title to add to the (newly created) port PR
pull_title: '[Port ${target_branch}] ${pull_title}'
# Description to add to the (newly created) port PR
pull_description: 'Port of #${pull_number} by @${pull_author} to `${target_branch}`.'
# Copy all labels from original PR to (newly created) port PR
# NOTE: The labels matching 'label_pattern' are automatically excluded
copy_labels_pattern: '.*'
# Use a personal access token (PAT) to create PR as 'dspace-bot' user.
# A PAT is required in order for the new PR to trigger its own actions (for CI checks)
github_token: ${{ secrets.PR_PORT_TOKEN }}

View File

@@ -0,0 +1,24 @@
# This workflow runs whenever a new pull request is created
name: Pull Request opened
# Only run for newly opened PRs against the "main" or maintenance branches
# We allow this to run for `pull_request_target` so that github secrets are available
# (This is required to assign a PR back to the creator when the PR comes from a forked repo)
on:
pull_request_target:
types: [ opened ]
branches:
- main
- 'dspace-**'
permissions:
pull-requests: write
jobs:
automation:
runs-on: ubuntu-latest
steps:
# Assign the PR to whomever created it. This is useful for visualizing assignments on project boards
# See https://github.com/toshimaru/auto-author-assign
- name: Assign PR to creator
uses: toshimaru/auto-author-assign@v1.6.2

2
.gitignore vendored
View File

@@ -39,3 +39,5 @@ package-lock.json
/nbproject/ /nbproject/
junit.xml junit.xml
/src/mirador-viewer/config.local.js

View File

@@ -2,7 +2,7 @@
# 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
# Test build: # Test build:
# docker build -f Dockerfile.dist -t dspace/dspace-angular:dspace-7_x-dist . # docker build -f Dockerfile.dist -t dspace/dspace-angular:latest-dist .
FROM node:18-alpine as build FROM node:18-alpine as build

View File

@@ -413,8 +413,7 @@ dspace-angular
│ ├── merge-i18n-files.ts * │ ├── merge-i18n-files.ts *
│ ├── serve.ts * │ ├── serve.ts *
│ ├── sync-i18n-files.ts * │ ├── sync-i18n-files.ts *
── test-rest.ts * ── test-rest.ts *
│ └── webpack.js *
├── src * The source of the application ├── src * The source of the application
│ ├── app * The source code of the application, subdivided by module/page. │ ├── app * The source code of the application, subdivided by module/page.
│ ├── assets * Folder for static resources │ ├── assets * Folder for static resources

View File

@@ -274,9 +274,18 @@
} }
} }
}, },
"defaultProject": "dspace-angular",
"cli": { "cli": {
"analytics": false, "analytics": false,
"defaultCollection": "@angular-eslint/schematics" "schematicCollections": [
"@angular-eslint/schematics"
]
},
"schematics": {
"@angular-eslint/schematics:application": {
"setParserOptionsProject": true
},
"@angular-eslint/schematics:library": {
"setParserOptionsProject": true
}
} }
} }

View File

@@ -187,6 +187,9 @@ languages:
- code: gd - code: gd
label: Gàidhlig label: Gàidhlig
active: true active: true
- code: it
label: Italiano
active: true
- code: lv - code: lv
label: Latviešu label: Latviešu
active: true active: true
@@ -214,6 +217,9 @@ languages:
- code: tr - code: tr
label: Türkçe label: Türkçe
active: true active: true
- code: vi
label: Tiếng Việt
active: true
- code: kk - code: kk
label: Қазақ label: Қазақ
active: true active: true

View File

@@ -6,8 +6,8 @@ describe('Homepage', () => {
cy.visit('/'); cy.visit('/');
}); });
it('should display translated title "DSpace Angular :: Home"', () => { it('should display translated title "DSpace Repository :: Home"', () => {
cy.title().should('eq', 'DSpace Angular :: Home'); cy.title().should('eq', 'DSpace Repository :: Home');
}); });
it('should contain a news section', () => { it('should contain a news section', () => {

View File

@@ -23,14 +23,14 @@ the Docker compose scripts in this 'docker' folder.
This Dockerfile is used to build a *development* DSpace 7 Angular UI image, published as 'dspace/dspace-angular' This Dockerfile is used to build a *development* DSpace 7 Angular UI image, published as 'dspace/dspace-angular'
``` ```
docker build -t dspace/dspace-angular:dspace-7_x . docker build -t dspace/dspace-angular:latest .
``` ```
This image is built *automatically* after each commit is made to the `main` branch. This image is built *automatically* after each commit is made to the `main` branch.
Admins to our DockerHub repo can manually publish with the following command. Admins to our DockerHub repo can manually publish with the following command.
``` ```
docker push dspace/dspace-angular:dspace-7_x docker push dspace/dspace-angular:latest
``` ```
### Dockerfile.dist ### Dockerfile.dist
@@ -39,7 +39,7 @@ The `Dockerfile.dist` is used to generate a *production* build and runtime envir
```bash ```bash
# build the latest image # build the latest image
docker build -f Dockerfile.dist -t dspace/dspace-angular:dspace-7_x-dist . docker build -f Dockerfile.dist -t dspace/dspace-angular:latest-dist .
``` ```
A default/demo version of this image is built *automatically*. A default/demo version of this image is built *automatically*.

View File

@@ -16,7 +16,7 @@ version: "3.7"
services: services:
dspace-cli: dspace-cli:
image: "${DOCKER_OWNER:-dspace}/dspace-cli:${DSPACE_VER:-dspace-7_x}" image: "${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.

View File

@@ -35,7 +35,7 @@ services:
solr__D__statistics__P__autoCommit: 'false' solr__D__statistics__P__autoCommit: 'false'
depends_on: depends_on:
- dspacedb - dspacedb
image: dspace/dspace:dspace-7_x-test image: dspace/dspace:latest-test
networks: networks:
dspacenet: dspacenet:
ports: ports:

View File

@@ -27,7 +27,7 @@ services:
DSPACE_REST_HOST: api7.dspace.org DSPACE_REST_HOST: api7.dspace.org
DSPACE_REST_PORT: 443 DSPACE_REST_PORT: 443
DSPACE_REST_NAMESPACE: /server DSPACE_REST_NAMESPACE: /server
image: dspace/dspace-angular:dspace-7_x-dist image: dspace/dspace-angular:${DSPACE_VER:-latest}-dist
build: build:
context: .. context: ..
dockerfile: Dockerfile.dist dockerfile: Dockerfile.dist

View File

@@ -39,7 +39,7 @@ services:
# proxies.trusted.ipranges: This setting is required for a REST API running in Docker to trust requests # proxies.trusted.ipranges: This setting is required for a REST API running in Docker to trust requests
# 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'
image: "${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-dspace-7_x-test}" image: "${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-latest-test}"
depends_on: depends_on:
- dspacedb - dspacedb
networks: networks:
@@ -82,7 +82,7 @@ services:
# DSpace Solr container # DSpace Solr container
dspacesolr: dspacesolr:
container_name: dspacesolr container_name: dspacesolr
image: "${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-dspace-7_x}" image: "${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-latest}"
# Needs main 'dspace' container to start first to guarantee access to solr_configs # Needs main 'dspace' container to start first to guarantee access to solr_configs
depends_on: depends_on:
- dspace - dspace

View File

@@ -24,7 +24,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-7_x image: dspace/dspace-angular:${DSPACE_VER:-latest}
build: build:
context: .. context: ..
dockerfile: Dockerfile dockerfile: Dockerfile

View File

@@ -1,6 +1,6 @@
{ {
"name": "dspace-angular", "name": "dspace-angular",
"version": "7.6.0-next", "version": "8.0.0-next",
"scripts": { "scripts": {
"ng": "ng", "ng": "ng",
"config:watch": "nodemon", "config:watch": "nodemon",
@@ -17,9 +17,9 @@
"build:stats": "ng build --stats-json", "build:stats": "ng build --stats-json",
"build:prod": "yarn run build:ssr", "build:prod": "yarn 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",
"test": "ng test --sourceMap=true --watch=false --configuration test", "test": "ng test --source-map=true --watch=false --configuration test",
"test:watch": "nodemon --exec \"ng test --sourceMap=true --watch=true --configuration test\"", "test:watch": "nodemon --exec \"ng test --source-map=true --watch=true --configuration test\"",
"test:headless": "ng test --sourceMap=true --watch=false --configuration test --browsers=ChromeHeadless --code-coverage", "test:headless": "ng test --source-map=true --watch=false --configuration test --browsers=ChromeHeadless --code-coverage",
"lint": "ng lint", "lint": "ng lint",
"lint-fix": "ng lint --fix=true", "lint-fix": "ng lint --fix=true",
"e2e": "ng e2e", "e2e": "ng e2e",
@@ -55,136 +55,136 @@
"ts-node": "10.2.1" "ts-node": "10.2.1"
}, },
"dependencies": { "dependencies": {
"@angular/animations": "~13.3.12", "@angular/animations": "^15.2.8",
"@angular/cdk": "^13.2.6", "@angular/cdk": "^15.2.8",
"@angular/common": "~13.3.12", "@angular/common": "^15.2.8",
"@angular/compiler": "~13.3.12", "@angular/compiler": "^15.2.8",
"@angular/core": "~13.3.12", "@angular/core": "^15.2.8",
"@angular/forms": "~13.3.12", "@angular/forms": "^15.2.8",
"@angular/localize": "13.3.12", "@angular/localize": "15.2.8",
"@angular/platform-browser": "~13.3.12", "@angular/platform-browser": "^15.2.8",
"@angular/platform-browser-dynamic": "~13.3.12", "@angular/platform-browser-dynamic": "^15.2.8",
"@angular/platform-server": "~13.3.12", "@angular/platform-server": "^15.2.8",
"@angular/router": "~13.3.12", "@angular/router": "^15.2.8",
"@babel/runtime": "7.17.2", "@babel/runtime": "7.21.0",
"@kolkov/ngx-gallery": "^2.0.1", "@kolkov/ngx-gallery": "^2.0.1",
"@material-ui/core": "^4.11.0", "@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1", "@material-ui/icons": "^4.11.3",
"@ng-bootstrap/ng-bootstrap": "^11.0.0", "@ng-bootstrap/ng-bootstrap": "^11.0.0",
"@ng-dynamic-forms/core": "^15.0.0", "@ng-dynamic-forms/core": "^15.0.0",
"@ng-dynamic-forms/ui-ng-bootstrap": "^15.0.0", "@ng-dynamic-forms/ui-ng-bootstrap": "^15.0.0",
"@ngrx/effects": "^13.0.2", "@ngrx/effects": "^15.4.0",
"@ngrx/router-store": "^13.0.2", "@ngrx/router-store": "^15.4.0",
"@ngrx/store": "^13.0.2", "@ngrx/store": "^15.4.0",
"@nguniversal/express-engine": "^13.0.2", "@nguniversal/express-engine": "^15.2.1",
"@ngx-translate/core": "^13.0.0", "@ngx-translate/core": "^14.0.0",
"@nicky-lenaers/ngx-scroll-to": "^13.0.0", "@nicky-lenaers/ngx-scroll-to": "^14.0.0",
"@types/grecaptcha": "^3.0.4", "@types/grecaptcha": "^3.0.4",
"angular-idle-preload": "3.0.0", "angular-idle-preload": "3.0.0",
"angulartics2": "^12.0.0", "angulartics2": "^12.2.0",
"axios": "^0.27.2", "axios": "^0.27.2",
"bootstrap": "^4.6.1", "bootstrap": "^4.6.1",
"cerialize": "0.1.18", "cerialize": "0.1.18",
"cli-progress": "^3.8.0", "cli-progress": "^3.12.0",
"colors": "^1.4.0", "colors": "^1.4.0",
"compression": "^1.7.4", "compression": "^1.7.4",
"cookie-parser": "1.4.5", "cookie-parser": "1.4.6",
"core-js": "^3.7.0", "core-js": "^3.30.1",
"date-fns": "^2.29.3", "date-fns": "^2.29.3",
"date-fns-tz": "^1.3.7", "date-fns-tz": "^1.3.7",
"deepmerge": "^4.2.2", "deepmerge": "^4.3.1",
"ejs": "^3.1.8", "ejs": "^3.1.9",
"express": "^4.17.1", "express": "^4.18.2",
"express-rate-limit": "^5.1.3", "express-rate-limit": "^5.1.3",
"fast-json-patch": "^3.0.0-1", "fast-json-patch": "^3.1.1",
"filesize": "^6.1.0", "filesize": "^6.1.0",
"http-proxy-middleware": "^1.0.5", "http-proxy-middleware": "^1.0.5",
"isbot": "^3.6.5", "isbot": "^3.6.10",
"js-cookie": "2.2.1", "js-cookie": "2.2.1",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"json5": "^2.2.2", "json5": "^2.2.3",
"jsonschema": "1.4.0", "jsonschema": "1.4.1",
"jwt-decode": "^3.1.2", "jwt-decode": "^3.1.2",
"klaro": "^0.7.18", "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",
"markdown-it-mathjax3": "^4.3.1", "markdown-it-mathjax3": "^4.3.2",
"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.11.0",
"morgan": "^1.10.0", "morgan": "^1.10.0",
"ng-mocks": "^13.1.1", "ng-mocks": "^14.10.0",
"ng2-file-upload": "1.4.0", "ng2-file-upload": "1.4.0",
"ng2-nouislider": "^1.8.3", "ng2-nouislider": "^1.8.3",
"ngx-infinite-scroll": "^10.0.1", "ngx-infinite-scroll": "^15.0.0",
"ngx-pagination": "5.0.0", "ngx-pagination": "6.0.3",
"ngx-sortablejs": "^11.1.0", "ngx-sortablejs": "^11.1.0",
"ngx-ui-switch": "^13.0.2", "ngx-ui-switch": "^14.0.3",
"nouislider": "^14.6.3", "nouislider": "^14.6.3",
"pem": "1.14.4", "pem": "1.14.7",
"prop-types": "^15.7.2", "prop-types": "^15.8.1",
"react-copy-to-clipboard": "^5.0.1", "react-copy-to-clipboard": "^5.1.0",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rxjs": "^7.5.5", "rxjs": "^7.8.0",
"sanitize-html": "^2.7.2", "sanitize-html": "^2.10.0",
"sortablejs": "1.13.0", "sortablejs": "1.15.0",
"uuid": "^8.3.2", "uuid": "^8.3.2",
"webfontloader": "1.6.28", "webfontloader": "1.6.28",
"zone.js": "~0.11.5" "zone.js": "~0.11.5"
}, },
"devDependencies": { "devDependencies": {
"@angular-builders/custom-webpack": "~13.1.0", "@angular-builders/custom-webpack": "~15.0.0",
"@angular-devkit/build-angular": "~13.3.10", "@angular-devkit/build-angular": "^15.2.6",
"@angular-eslint/builder": "13.1.0", "@angular-eslint/builder": "15.2.1",
"@angular-eslint/eslint-plugin": "13.1.0", "@angular-eslint/eslint-plugin": "15.2.1",
"@angular-eslint/eslint-plugin-template": "13.1.0", "@angular-eslint/eslint-plugin-template": "15.2.1",
"@angular-eslint/schematics": "13.1.0", "@angular-eslint/schematics": "15.2.1",
"@angular-eslint/template-parser": "13.1.0", "@angular-eslint/template-parser": "15.2.1",
"@angular/cli": "~13.3.10", "@angular/cli": "^15.2.6",
"@angular/compiler-cli": "~13.3.12", "@angular/compiler-cli": "^15.2.8",
"@angular/language-service": "~13.3.12", "@angular/language-service": "^15.2.8",
"@cypress/schematic": "^1.5.0", "@cypress/schematic": "^1.5.0",
"@fortawesome/fontawesome-free": "^6.2.1", "@fortawesome/fontawesome-free": "^6.4.0",
"@ngrx/store-devtools": "^13.0.2", "@ngrx/store-devtools": "^15.4.0",
"@ngtools/webpack": "^13.2.6", "@ngtools/webpack": "^15.2.6",
"@nguniversal/builders": "^13.1.1", "@nguniversal/builders": "^15.2.1",
"@types/deep-freeze": "0.1.2", "@types/deep-freeze": "0.1.2",
"@types/ejs": "^3.1.1", "@types/ejs": "^3.1.2",
"@types/express": "^4.17.9", "@types/express": "^4.17.17",
"@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.165", "@types/lodash": "^4.14.194",
"@types/node": "^14.14.9", "@types/node": "^14.14.9",
"@types/sanitize-html": "^2.6.2", "@types/sanitize-html": "^2.9.0",
"@typescript-eslint/eslint-plugin": "5.11.0", "@typescript-eslint/eslint-plugin": "^5.59.1",
"@typescript-eslint/parser": "5.11.0", "@typescript-eslint/parser": "^5.59.1",
"axe-core": "^4.4.3", "axe-core": "^4.7.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.9.0", "cypress": "12.10.0",
"cypress-axe": "^1.1.0", "cypress-axe": "^1.4.0",
"deep-freeze": "0.0.1", "deep-freeze": "0.0.1",
"eslint": "^8.2.0", "eslint": "^8.39.0",
"eslint-plugin-deprecation": "^1.3.2", "eslint-plugin-deprecation": "^1.4.1",
"eslint-plugin-import": "^2.25.4", "eslint-plugin-import": "^2.27.5",
"eslint-plugin-jsdoc": "^39.6.4", "eslint-plugin-jsdoc": "^39.6.4",
"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-unused-imports": "^2.0.0", "eslint-plugin-unused-imports": "^2.0.0",
"express-static-gzip": "^2.1.5", "express-static-gzip": "^2.1.7",
"jasmine-core": "^3.8.0", "jasmine-core": "^3.8.0",
"jasmine-marbles": "0.9.2", "jasmine-marbles": "0.9.2",
"karma": "^6.3.14", "karma": "^6.4.2",
"karma-chrome-launcher": "~3.1.0", "karma-chrome-launcher": "~3.2.0",
"karma-coverage-istanbul-reporter": "~3.0.2", "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",
"ngx-mask": "^13.1.7", "ngx-mask": "^13.1.7",
"nodemon": "^2.0.20", "nodemon": "^2.0.22",
"postcss": "^8.1", "postcss": "^8.4",
"postcss-apply": "0.12.0", "postcss-apply": "0.12.0",
"postcss-import": "^14.0.0", "postcss-import": "^14.0.0",
"postcss-loader": "^4.0.3", "postcss-loader": "^4.0.3",
@@ -194,14 +194,14 @@
"react-dom": "^16.14.0", "react-dom": "^16.14.0",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"rxjs-spy": "^8.0.2", "rxjs-spy": "^8.0.2",
"sass": "~1.33.0", "sass": "~1.62.0",
"sass-loader": "^12.6.0", "sass-loader": "^12.6.0",
"sass-resources-loader": "^2.1.1", "sass-resources-loader": "^2.2.5",
"ts-node": "^8.10.2", "ts-node": "^8.10.2",
"typescript": "~4.5.5", "typescript": "~4.8.4",
"webpack": "^5.76.0", "webpack": "5.76.1",
"webpack-bundle-analyzer": "^4.4.0", "webpack-bundle-analyzer": "^4.8.0",
"webpack-cli": "^4.2.0", "webpack-cli": "^4.2.0",
"webpack-dev-server": "^4.5.0" "webpack-dev-server": "^4.13.3"
} }
} }

View File

@@ -1,13 +0,0 @@
const path = require('path');
const child_process = require('child_process');
const heapSize = 4096;
const webpackPath = path.join('node_modules', 'webpack', 'bin', 'webpack.js');
const params = [
'--max_old_space_size=' + heapSize,
webpackPath,
...process.argv.slice(2)
];
child_process.spawn('node', params, { stdio:'inherit' });

View File

@@ -26,7 +26,6 @@ import * as ejs from 'ejs';
import * as compression from 'compression'; import * as compression from 'compression';
import * as expressStaticGzip from 'express-static-gzip'; 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';
@@ -34,7 +33,7 @@ import { createCertificate } from 'pem';
import { createServer } from 'https'; import { createServer } from 'https';
import { json } from 'body-parser'; import { json } from 'body-parser';
import { existsSync, readFileSync } from 'fs'; import { readFileSync } from 'fs';
import { join } from 'path'; import { join } from 'path';
import { enableProdMode } from '@angular/core'; import { enableProdMode } from '@angular/core';
@@ -54,7 +53,7 @@ import { buildAppConfig } from './src/config/config.server';
import { APP_CONFIG, AppConfig } from './src/config/app-config.interface'; import { APP_CONFIG, AppConfig } from './src/config/app-config.interface';
import { extendEnvironmentWithAppConfig } from './src/config/config.util'; import { extendEnvironmentWithAppConfig } from './src/config/config.util';
import { logStartupMessage } from './startup-message'; import { logStartupMessage } from './startup-message';
import { TOKENITEM } from 'src/app/core/auth/models/auth-token-info.model'; import { TOKENITEM } from './src/app/core/auth/models/auth-token-info.model';
/* /*
@@ -180,6 +179,15 @@ export function app() {
changeOrigin: true changeOrigin: true
})); }));
/**
* Proxy the linksets
*/
router.use('/signposting**', createProxyMiddleware({
target: `${environment.rest.baseUrl}`,
pathRewrite: path => path.replace(environment.ui.nameSpace, '/'),
changeOrigin: true
}));
/** /**
* Checks if the rateLimiter property is present * Checks if the rateLimiter property is present
* When it is present, the rateLimiter will be enabled. When it is undefined, the rateLimiter will be disabled. * When it is present, the rateLimiter will be enabled. When it is undefined, the rateLimiter will be disabled.
@@ -366,9 +374,19 @@ function cacheCheck(req, res, next) {
} }
// If cached copy exists, return it to the user. // If cached copy exists, return it to the user.
if (cachedCopy) { if (cachedCopy && cachedCopy.page) {
if (cachedCopy.headers) {
Object.keys(cachedCopy.headers).forEach((header) => {
if (cachedCopy.headers[header]) {
if (environment.cache.serverSide.debug) {
console.log(`Restore cached ${header} header`);
}
res.setHeader(header, cachedCopy.headers[header]);
}
});
}
res.locals.ssr = true; // mark response as SSR-generated (enables text compression) res.locals.ssr = true; // mark response as SSR-generated (enables text compression)
res.send(cachedCopy); res.send(cachedCopy.page);
// Tell Express to skip all other handlers for this path // Tell Express to skip all other handlers for this path
// This ensures we don't try to re-render the page since we've already returned the cached copy // This ensures we don't try to re-render the page since we've already returned the cached copy
@@ -443,22 +461,50 @@ function saveToCache(req, page: any) {
const key = getCacheKey(req); const key = getCacheKey(req);
// Avoid caching "/reload/[random]" paths (these are hard refreshes after logout) // Avoid caching "/reload/[random]" paths (these are hard refreshes after logout)
if (key.startsWith('/reload')) { return; } if (key.startsWith('/reload')) { return; }
// Avoid caching not successful responses (status code different from 2XX status)
if (hasNotSucceeded(req.res.statusCode)) { return; }
// Retrieve response headers to save, if any
const headers = retrieveHeaders(req.res);
// If bot cache is enabled, save it to that cache if it doesn't exist or is expired // If bot cache is enabled, save it to that cache if it doesn't exist or is expired
// (NOTE: has() will return false if page is expired in cache) // (NOTE: has() will return false if page is expired in cache)
if (botCacheEnabled() && !botCache.has(key)) { if (botCacheEnabled() && !botCache.has(key)) {
botCache.set(key, page); botCache.set(key, { page, headers });
if (environment.cache.serverSide.debug) { console.log(`CACHE SAVE FOR ${key} in bot cache.`); } if (environment.cache.serverSide.debug) { console.log(`CACHE SAVE FOR ${key} in bot cache.`); }
} }
// If anonymous cache is enabled, save it to that cache if it doesn't exist or is expired // If anonymous cache is enabled, save it to that cache if it doesn't exist or is expired
if (anonymousCacheEnabled() && !anonymousCache.has(key)) { if (anonymousCacheEnabled() && !anonymousCache.has(key)) {
anonymousCache.set(key, page); anonymousCache.set(key, { page, headers });
if (environment.cache.serverSide.debug) { console.log(`CACHE SAVE FOR ${key} in anonymous cache.`); } if (environment.cache.serverSide.debug) { console.log(`CACHE SAVE FOR ${key} in anonymous cache.`); }
} }
} }
} }
/**
* Check if status code is different from 2XX
* @param statusCode
*/
function hasNotSucceeded(statusCode) {
const rgx = new RegExp(/^20+/);
return !rgx.test(statusCode)
}
function retrieveHeaders(response) {
const headers = Object.create({});
if (Array.isArray(environment.cache.serverSide.headers) && environment.cache.serverSide.headers.length > 0) {
environment.cache.serverSide.headers.forEach((header) => {
if (response.hasHeader(header)) {
if (environment.cache.serverSide.debug) {
console.log(`Save ${header} header to cache`);
}
headers[header] = response.getHeader(header);
}
});
}
return headers;
}
/** /**
* Whether a user is authenticated or not * Whether a user is authenticated or not
*/ */

View File

@@ -6,8 +6,13 @@ import { GroupsRegistryComponent } from './group-registry/groups-registry.compon
import { GROUP_EDIT_PATH } from './access-control-routing-paths'; import { GROUP_EDIT_PATH } from './access-control-routing-paths';
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
import { GroupPageGuard } from './group-registry/group-page.guard'; import { GroupPageGuard } from './group-registry/group-page.guard';
import { GroupAdministratorGuard } from '../core/data/feature-authorization/feature-authorization-guard/group-administrator.guard'; import {
import { SiteAdministratorGuard } from '../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard'; GroupAdministratorGuard
} from '../core/data/feature-authorization/feature-authorization-guard/group-administrator.guard';
import {
SiteAdministratorGuard
} from '../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard';
import { BulkAccessComponent } from './bulk-access/bulk-access.component';
@NgModule({ @NgModule({
imports: [ imports: [
@@ -47,7 +52,16 @@ import { SiteAdministratorGuard } from '../core/data/feature-authorization/featu
}, },
data: { title: 'admin.access-control.groups.title.singleGroup', breadcrumbKey: 'admin.access-control.groups.singleGroup' }, data: { title: 'admin.access-control.groups.title.singleGroup', breadcrumbKey: 'admin.access-control.groups.singleGroup' },
canActivate: [GroupPageGuard] canActivate: [GroupPageGuard]
} },
{
path: 'bulk-access',
component: BulkAccessComponent,
resolve: {
breadcrumb: I18nBreadcrumbResolver
},
data: { title: 'admin.access-control.bulk-access.title', breadcrumbKey: 'admin.access-control.bulk-access' },
canActivate: [SiteAdministratorGuard]
},
]) ])
] ]
}) })

View File

@@ -12,6 +12,12 @@ import { GroupsRegistryComponent } from './group-registry/groups-registry.compon
import { FormModule } from '../shared/form/form.module'; import { FormModule } from '../shared/form/form.module';
import { DYNAMIC_ERROR_MESSAGES_MATCHER, DynamicErrorMessagesMatcher } from '@ng-dynamic-forms/core'; import { DYNAMIC_ERROR_MESSAGES_MATCHER, DynamicErrorMessagesMatcher } from '@ng-dynamic-forms/core';
import { AbstractControl } from '@angular/forms'; import { AbstractControl } from '@angular/forms';
import { BulkAccessComponent } from './bulk-access/bulk-access.component';
import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap';
import { BulkAccessBrowseComponent } from './bulk-access/browse/bulk-access-browse.component';
import { BulkAccessSettingsComponent } from './bulk-access/settings/bulk-access-settings.component';
import { SearchModule } from '../shared/search/search.module';
import { AccessControlFormModule } from '../shared/access-control-form-container/access-control-form.module';
/** /**
* Condition for displaying error messages on email form field * Condition for displaying error messages on email form field
@@ -28,6 +34,9 @@ export const ValidateEmailErrorStateMatcher: DynamicErrorMessagesMatcher =
RouterModule, RouterModule,
AccessControlRoutingModule, AccessControlRoutingModule,
FormModule, FormModule,
NgbAccordionModule,
SearchModule,
AccessControlFormModule,
], ],
exports: [ exports: [
MembersListComponent, MembersListComponent,
@@ -39,6 +48,9 @@ export const ValidateEmailErrorStateMatcher: DynamicErrorMessagesMatcher =
GroupFormComponent, GroupFormComponent,
SubgroupsListComponent, SubgroupsListComponent,
MembersListComponent, MembersListComponent,
BulkAccessComponent,
BulkAccessBrowseComponent,
BulkAccessSettingsComponent,
], ],
providers: [ providers: [
{ {

View File

@@ -0,0 +1,67 @@
<ngb-accordion #acc="ngbAccordion" [activeIds]="'browse'">
<ngb-panel [id]="'browse'">
<ng-template ngbPanelHeader>
<div class="w-100 d-flex justify-content-between collapse-toggle" ngbPanelToggle (click)="acc.toggle('browse')"
data-test="browse">
<button type="button" class="btn btn-link p-0" (click)="$event.preventDefault()"
[attr.aria-expanded]="!acc.isExpanded('browse')"
aria-controls="collapsePanels">
{{ 'admin.access-control.bulk-access-browse.header' | translate }}
</button>
<div class="text-right d-flex">
<div class="ml-3 d-inline-block">
<span *ngIf="acc.isExpanded('browse')" class="fas fa-chevron-up fa-fw"></span>
<span *ngIf="!acc.isExpanded('browse')" class="fas fa-chevron-down fa-fw"></span>
</div>
</div>
</div>
</ng-template>
<ng-template ngbPanelContent>
<ul ngbNav #nav="ngbNav" [(activeId)]="activateId" class="nav-pills">
<li [ngbNavItem]="'search'">
<a ngbNavLink>{{'admin.access-control.bulk-access-browse.search.header' | translate}}</a>
<ng-template ngbNavContent>
<div class="mx-n3">
<ds-themed-search [configuration]="'administrativeBulkAccess'"
[selectable]="true"
[selectionConfig]="{ repeatable: true, listId: listId }"
[showThumbnails]="false"></ds-themed-search>
</div>
</ng-template>
</li>
<li [ngbNavItem]="'selected'">
<a ngbNavLink>
{{'admin.access-control.bulk-access-browse.selected.header' | translate: {number: ((objectsSelected$ | async)?.payload?.totalElements) ? (objectsSelected$ | async)?.payload?.totalElements : '0'} }}
</a>
<ng-template ngbNavContent>
<ds-pagination
[paginationOptions]="(paginationOptions$ | async)"
[pageInfoState]="(objectsSelected$|async)?.payload.pageInfo"
[collectionSize]="(objectsSelected$|async)?.payload?.totalElements"
[objects]="(objectsSelected$|async)"
[showPaginator]="false"
(prev)="pagePrev()"
(next)="pageNext()">
<ul *ngIf="(objectsSelected$|async)?.hasSucceeded" class="list-unstyled ml-4">
<li *ngFor='let object of (objectsSelected$|async)?.payload?.page | paginate: { itemsPerPage: (paginationOptions$ | async).pageSize,
currentPage: (paginationOptions$ | async).currentPage, totalItems: (objectsSelected$|async)?.payload?.page.length }; let i = index; let last = last '
class="mt-4 mb-4 d-flex"
[attr.data-test]="'list-object' | dsBrowserOnly">
<ds-selectable-list-item-control [index]="i"
[object]="object"
[selectionConfig]="{ repeatable: true, listId: listId }"></ds-selectable-list-item-control>
<ds-listable-object-component-loader [listID]="listId"
[index]="i"
[object]="object"
[showThumbnails]="false"
[viewMode]="'list'"></ds-listable-object-component-loader>
</li>
</ul>
</ds-pagination>
</ng-template>
</li>
</ul>
<div [ngbNavOutlet]="nav" class="mt-5"></div>
</ng-template>
</ngb-panel>
</ngb-accordion>

View File

@@ -0,0 +1,82 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { of } from 'rxjs';
import { NgbAccordionModule, NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
import { TranslateModule } from '@ngx-translate/core';
import { BulkAccessBrowseComponent } from './bulk-access-browse.component';
import { SelectableListService } from '../../../shared/object-list/selectable-list/selectable-list.service';
import { SelectableObject } from '../../../shared/object-list/selectable-list/selectable-list.service.spec';
import { PageInfo } from '../../../core/shared/page-info.model';
import { buildPaginatedList } from '../../../core/data/paginated-list.model';
import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils';
describe('BulkAccessBrowseComponent', () => {
let component: BulkAccessBrowseComponent;
let fixture: ComponentFixture<BulkAccessBrowseComponent>;
const listID1 = 'id1';
const value1 = 'Selected object';
const value2 = 'Another selected object';
const selected1 = new SelectableObject(value1);
const selected2 = new SelectableObject(value2);
const testSelection = { id: listID1, selection: [selected1, selected2] } ;
const selectableListService = jasmine.createSpyObj('SelectableListService', ['getSelectableList', 'deselectAll']);
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [
NgbAccordionModule,
NgbNavModule,
TranslateModule.forRoot()
],
declarations: [BulkAccessBrowseComponent],
providers: [ { provide: SelectableListService, useValue: selectableListService }, ],
schemas: [
NO_ERRORS_SCHEMA
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(BulkAccessBrowseComponent);
component = fixture.componentInstance;
(component as any).selectableListService.getSelectableList.and.returnValue(of(testSelection));
fixture.detectChanges();
});
afterEach(() => {
fixture.destroy();
component = null;
});
it('should create the component', () => {
expect(component).toBeTruthy();
});
it('should have an initial active nav id of "search"', () => {
expect(component.activateId).toEqual('search');
});
it('should have an initial pagination options object with default values', () => {
expect(component.paginationOptions$.getValue().id).toEqual('bas');
expect(component.paginationOptions$.getValue().pageSize).toEqual(5);
expect(component.paginationOptions$.getValue().currentPage).toEqual(1);
});
it('should have an initial remote data with a paginated list as value', () => {
const list = buildPaginatedList(new PageInfo({
'elementsPerPage': 5,
'totalElements': 2,
'totalPages': 1,
'currentPage': 1
}), [selected1, selected2]) ;
const rd = createSuccessfulRemoteDataObject(list);
expect(component.objectsSelected$.value).toEqual(rd);
});
});

View File

@@ -0,0 +1,119 @@
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { BehaviorSubject, Subscription } from 'rxjs';
import { distinctUntilChanged, map } from 'rxjs/operators';
import { SEARCH_CONFIG_SERVICE } from '../../../my-dspace-page/my-dspace-page.component';
import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service';
import { SelectableListService } from '../../../shared/object-list/selectable-list/selectable-list.service';
import { SelectableListState } from '../../../shared/object-list/selectable-list/selectable-list.reducer';
import { RemoteData } from '../../../core/data/remote-data';
import { buildPaginatedList, PaginatedList } from '../../../core/data/paginated-list.model';
import { ListableObject } from '../../../shared/object-collection/shared/listable-object.model';
import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils';
import { PageInfo } from '../../../core/shared/page-info.model';
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
import { hasValue } from '../../../shared/empty.util';
@Component({
selector: 'ds-bulk-access-browse',
templateUrl: 'bulk-access-browse.component.html',
styleUrls: ['./bulk-access-browse.component.scss'],
providers: [
{
provide: SEARCH_CONFIG_SERVICE,
useClass: SearchConfigurationService
}
]
})
export class BulkAccessBrowseComponent implements OnInit, OnDestroy {
/**
* The selection list id
*/
@Input() listId!: string;
/**
* The active nav id
*/
activateId = 'search';
/**
* The list of the objects already selected
*/
objectsSelected$: BehaviorSubject<RemoteData<PaginatedList<ListableObject>>> = new BehaviorSubject<RemoteData<PaginatedList<ListableObject>>>(null);
/**
* The pagination options object used for the list of selected elements
*/
paginationOptions$: BehaviorSubject<PaginationComponentOptions> = new BehaviorSubject<PaginationComponentOptions>(Object.assign(new PaginationComponentOptions(), {
id: 'bas',
pageSize: 5,
currentPage: 1
}));
/**
* Array to track all subscriptions and unsubscribe them onDestroy
*/
private subs: Subscription[] = [];
constructor(private selectableListService: SelectableListService) {}
/**
* Subscribe to selectable list updates
*/
ngOnInit(): void {
this.subs.push(
this.selectableListService.getSelectableList(this.listId).pipe(
distinctUntilChanged(),
map((list: SelectableListState) => this.generatePaginatedListBySelectedElements(list))
).subscribe(this.objectsSelected$)
);
}
pageNext() {
this.paginationOptions$.next(Object.assign(new PaginationComponentOptions(), this.paginationOptions$.value, {
currentPage: this.paginationOptions$.value.currentPage + 1
}));
}
pagePrev() {
this.paginationOptions$.next(Object.assign(new PaginationComponentOptions(), this.paginationOptions$.value, {
currentPage: this.paginationOptions$.value.currentPage - 1
}));
}
private calculatePageCount(pageSize, totalCount = 0) {
// we suppose that if we have 0 items we want 1 empty page
return totalCount < pageSize ? 1 : Math.ceil(totalCount / pageSize);
}
/**
* Generate The RemoteData object containing the list of the selected elements
* @param list
* @private
*/
private generatePaginatedListBySelectedElements(list: SelectableListState): RemoteData<PaginatedList<ListableObject>> {
const pageInfo = new PageInfo({
elementsPerPage: this.paginationOptions$.value.pageSize,
totalElements: list?.selection.length,
totalPages: this.calculatePageCount(this.paginationOptions$.value.pageSize, list?.selection.length),
currentPage: this.paginationOptions$.value.currentPage
});
if (pageInfo.currentPage > pageInfo.totalPages) {
pageInfo.currentPage = pageInfo.totalPages;
this.paginationOptions$.next(Object.assign(new PaginationComponentOptions(), this.paginationOptions$.value, {
currentPage: pageInfo.currentPage
}));
}
return createSuccessfulRemoteDataObject(buildPaginatedList(pageInfo, list?.selection || []));
}
ngOnDestroy(): void {
this.subs
.filter((sub) => hasValue(sub))
.forEach((sub) => sub.unsubscribe());
this.selectableListService.deselectAll(this.listId);
}
}

View File

@@ -0,0 +1,19 @@
<div class="container">
<ds-bulk-access-browse [listId]="listId"></ds-bulk-access-browse>
<div class="clearfix mb-3"></div>
<ds-bulk-access-settings #dsBulkSettings ></ds-bulk-access-settings>
<hr>
<div class="d-flex justify-content-end">
<button class="btn btn-outline-primary mr-3" (click)="reset()">
{{ 'access-control-cancel' | translate }}
</button>
<button class="btn btn-primary" [disabled]="!canExport()" (click)="submit()">
{{ 'access-control-execute' | translate }}
</button>
</div>
</div>

View File

@@ -0,0 +1,158 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { TranslateModule } from '@ngx-translate/core';
import { of } from 'rxjs';
import { BulkAccessComponent } from './bulk-access.component';
import { BulkAccessControlService } from '../../shared/access-control-form-container/bulk-access-control.service';
import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service';
import { SelectableListState } from '../../shared/object-list/selectable-list/selectable-list.reducer';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { Process } from '../../process-page/processes/process.model';
import { RouterTestingModule } from '@angular/router/testing';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
describe('BulkAccessComponent', () => {
let component: BulkAccessComponent;
let fixture: ComponentFixture<BulkAccessComponent>;
let bulkAccessControlService: any;
let selectableListService: any;
const selectableListServiceMock = jasmine.createSpyObj('SelectableListService', ['getSelectableList', 'deselectAll']);
const bulkAccessControlServiceMock = jasmine.createSpyObj('bulkAccessControlService', ['createPayloadFile', 'executeScript']);
const mockFormState = {
'bitstream': [],
'item': [
{
'name': 'embargo',
'startDate': {
'year': 2026,
'month': 5,
'day': 31
},
'endDate': null
}
],
'state': {
'item': {
'toggleStatus': true,
'accessMode': 'replace'
},
'bitstream': {
'toggleStatus': false,
'accessMode': '',
'changesLimit': '',
'selectedBitstreams': []
}
}
};
const mockFile = {
'uuids': [
'1234', '5678'
],
'file': { }
};
const mockSettings: any = jasmine.createSpyObj('AccessControlFormContainerComponent', {
getValue: jasmine.createSpy('getValue'),
reset: jasmine.createSpy('reset')
});
const selection: any[] = [{ indexableObject: { uuid: '1234' } }, { indexableObject: { uuid: '5678' } }];
const selectableListState: SelectableListState = { id: 'test', selection };
const expectedIdList = ['1234', '5678'];
const selectableListStateEmpty: SelectableListState = { id: 'test', selection: [] };
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
RouterTestingModule,
TranslateModule.forRoot()
],
declarations: [ BulkAccessComponent ],
providers: [
{ provide: BulkAccessControlService, useValue: bulkAccessControlServiceMock },
{ provide: NotificationsService, useValue: NotificationsServiceStub },
{ provide: SelectableListService, useValue: selectableListServiceMock }
],
schemas: [NO_ERRORS_SCHEMA]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(BulkAccessComponent);
component = fixture.componentInstance;
bulkAccessControlService = TestBed.inject(BulkAccessControlService);
selectableListService = TestBed.inject(SelectableListService);
});
afterEach(() => {
fixture.destroy();
});
describe('when there are no elements selected', () => {
beforeEach(() => {
(component as any).selectableListService.getSelectableList.and.returnValue(of(selectableListStateEmpty));
fixture.detectChanges();
component.settings = mockSettings;
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should generate the id list by selected elements', () => {
expect(component.objectsSelected$.value).toEqual([]);
});
it('should disable the execute button when there are no objects selected', () => {
expect(component.canExport()).toBe(false);
});
});
describe('when there are elements selected', () => {
beforeEach(() => {
(component as any).selectableListService.getSelectableList.and.returnValue(of(selectableListState));
fixture.detectChanges();
component.settings = mockSettings;
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should generate the id list by selected elements', () => {
expect(component.objectsSelected$.value).toEqual(expectedIdList);
});
it('should enable the execute button when there are objects selected', () => {
component.objectsSelected$.next(['1234']);
expect(component.canExport()).toBe(true);
});
it('should call the settings reset method when reset is called', () => {
component.reset();
expect(component.settings.reset).toHaveBeenCalled();
});
it('should call the bulkAccessControlService executeScript method when submit is called', () => {
(component.settings as any).getValue.and.returnValue(mockFormState);
bulkAccessControlService.createPayloadFile.and.returnValue(mockFile);
bulkAccessControlService.executeScript.and.returnValue(createSuccessfulRemoteDataObject$(new Process()));
component.objectsSelected$.next(['1234']);
component.submit();
expect(bulkAccessControlService.executeScript).toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,94 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { BehaviorSubject, Subscription } from 'rxjs';
import { distinctUntilChanged, map } from 'rxjs/operators';
import { BulkAccessSettingsComponent } from './settings/bulk-access-settings.component';
import { BulkAccessControlService } from '../../shared/access-control-form-container/bulk-access-control.service';
import { SelectableListState } from '../../shared/object-list/selectable-list/selectable-list.reducer';
import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service';
@Component({
selector: 'ds-bulk-access',
templateUrl: './bulk-access.component.html',
styleUrls: ['./bulk-access.component.scss']
})
export class BulkAccessComponent implements OnInit {
/**
* The selection list id
*/
listId = 'bulk-access-list';
/**
* The list of the objects already selected
*/
objectsSelected$: BehaviorSubject<string[]> = new BehaviorSubject<string[]>([]);
/**
* Array to track all subscriptions and unsubscribe them onDestroy
*/
private subs: Subscription[] = [];
/**
* The SectionsDirective reference
*/
@ViewChild('dsBulkSettings') settings: BulkAccessSettingsComponent;
constructor(
private bulkAccessControlService: BulkAccessControlService,
private selectableListService: SelectableListService
) {
}
ngOnInit(): void {
this.subs.push(
this.selectableListService.getSelectableList(this.listId).pipe(
distinctUntilChanged(),
map((list: SelectableListState) => this.generateIdListBySelectedElements(list))
).subscribe(this.objectsSelected$)
);
}
canExport(): boolean {
return this.objectsSelected$.value?.length > 0;
}
/**
* Reset the form to its initial state
* This will also reset the state of the child components (bitstream and item access)
*/
reset(): void {
this.settings.reset();
}
/**
* Submit the form
* This will create a payload file and execute the script
*/
submit(): void {
const settings = this.settings.getValue();
const bitstreamAccess = settings.bitstream;
const itemAccess = settings.item;
const { file } = this.bulkAccessControlService.createPayloadFile({
bitstreamAccess,
itemAccess,
state: settings.state
});
this.bulkAccessControlService.executeScript(
this.objectsSelected$.value || [],
file
).subscribe();
}
/**
* Generate The RemoteData object containing the list of the selected elements
* @param list
* @private
*/
private generateIdListBySelectedElements(list: SelectableListState): string[] {
return list?.selection?.map((entry: any) => entry.indexableObject.uuid);
}
}

View File

@@ -0,0 +1,21 @@
<ngb-accordion #acc="ngbAccordion" [activeIds]="'settings'">
<ngb-panel [id]="'settings'">
<ng-template ngbPanelHeader>
<div class="w-100 d-flex justify-content-between collapse-toggle" ngbPanelToggle (click)="acc.toggle('settings')" data-test="settings">
<button type="button" class="btn btn-link p-0" (click)="$event.preventDefault()" [attr.aria-expanded]="!acc.isExpanded('browse')"
aria-controls="collapsePanels">
{{ 'admin.access-control.bulk-access-settings.header' | translate }}
</button>
<div class="text-right d-flex">
<div class="ml-3 d-inline-block">
<span *ngIf="acc.isExpanded('settings')" class="fas fa-chevron-up fa-fw"></span>
<span *ngIf="!acc.isExpanded('settings')" class="fas fa-chevron-down fa-fw"></span>
</div>
</div>
</div>
</ng-template>
<ng-template ngbPanelContent>
<ds-access-control-form-container #dsAccessControlForm [showSubmit]="false"></ds-access-control-form-container>
</ng-template>
</ngb-panel>
</ngb-accordion>

View File

@@ -0,0 +1,81 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap';
import { TranslateModule } from '@ngx-translate/core';
import { BulkAccessSettingsComponent } from './bulk-access-settings.component';
import { NO_ERRORS_SCHEMA } from '@angular/core';
describe('BulkAccessSettingsComponent', () => {
let component: BulkAccessSettingsComponent;
let fixture: ComponentFixture<BulkAccessSettingsComponent>;
const mockFormState = {
'bitstream': [],
'item': [
{
'name': 'embargo',
'startDate': {
'year': 2026,
'month': 5,
'day': 31
},
'endDate': null
}
],
'state': {
'item': {
'toggleStatus': true,
'accessMode': 'replace'
},
'bitstream': {
'toggleStatus': false,
'accessMode': '',
'changesLimit': '',
'selectedBitstreams': []
}
}
};
const mockControl: any = jasmine.createSpyObj('AccessControlFormContainerComponent', {
getFormValue: jasmine.createSpy('getFormValue'),
reset: jasmine.createSpy('reset')
});
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [NgbAccordionModule, TranslateModule.forRoot()],
declarations: [BulkAccessSettingsComponent],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(BulkAccessSettingsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
component.controlForm = mockControl;
});
it('should create the component', () => {
expect(component).toBeTruthy();
});
it('should have a method to get the form value', () => {
expect(component.getValue).toBeDefined();
});
it('should have a method to reset the form', () => {
expect(component.reset).toBeDefined();
});
it('should return the correct form value', () => {
const expectedValue = mockFormState;
(component.controlForm as any).getFormValue.and.returnValue(mockFormState);
const actualValue = component.getValue();
// @ts-ignore
expect(actualValue).toEqual(expectedValue);
});
it('should call reset on the control form', () => {
component.reset();
expect(component.controlForm.reset).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,34 @@
import { Component, ViewChild } from '@angular/core';
import {
AccessControlFormContainerComponent
} from '../../../shared/access-control-form-container/access-control-form-container.component';
@Component({
selector: 'ds-bulk-access-settings',
templateUrl: 'bulk-access-settings.component.html',
styleUrls: ['./bulk-access-settings.component.scss'],
exportAs: 'dsBulkSettings'
})
export class BulkAccessSettingsComponent {
/**
* The SectionsDirective reference
*/
@ViewChild('dsAccessControlForm') controlForm: AccessControlFormContainerComponent<any>;
/**
* Will be used from a parent component to read the value of the form
*/
getValue() {
return this.controlForm.getFormValue();
}
/**
* Reset the form to its initial state
* This will also reset the state of the child components (bitstream and item access)
*/
reset() {
this.controlForm.reset();
}
}

View File

@@ -68,18 +68,18 @@
<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' : isActive(epersonDto.eperson) | async}">
<td>{{epersonDto.eperson.id}}</td> <td>{{epersonDto.eperson.id}}</td>
<td>{{epersonDto.eperson.name}}</td> <td>{{ dsoNameService.getName(epersonDto.eperson) }}</td>
<td>{{epersonDto.eperson.email}}</td> <td>{{epersonDto.eperson.email}}</td>
<td> <td>
<div class="btn-group edit-field"> <div class="btn-group edit-field">
<button (click)="toggleEditEPerson(epersonDto.eperson)" <button (click)="toggleEditEPerson(epersonDto.eperson)"
class="btn btn-outline-primary btn-sm access-control-editEPersonButton" class="btn btn-outline-primary btn-sm access-control-editEPersonButton"
title="{{labelPrefix + 'table.edit.buttons.edit' | translate: {name: epersonDto.eperson.name} }}"> title="{{labelPrefix + 'table.edit.buttons.edit' | translate: { name: dsoNameService.getName(epersonDto.eperson) } }}">
<i class="fas fa-edit fa-fw"></i> <i class="fas fa-edit fa-fw"></i>
</button> </button>
<button [disabled]="!epersonDto.ableToDelete" (click)="deleteEPerson(epersonDto.eperson)" <button [disabled]="!epersonDto.ableToDelete" (click)="deleteEPerson(epersonDto.eperson)"
class="delete-button btn btn-outline-danger btn-sm access-control-deleteEPersonButton" class="delete-button btn btn-outline-danger btn-sm access-control-deleteEPersonButton"
title="{{labelPrefix + 'table.edit.buttons.remove' | translate: {name: epersonDto.eperson.name} }}"> title="{{labelPrefix + 'table.edit.buttons.remove' | translate: { name: dsoNameService.getName(epersonDto.eperson) } }}">
<i class="fas fa-trash-alt fa-fw"></i> <i class="fas fa-trash-alt fa-fw"></i>
</button> </button>
</div> </div>

View File

@@ -1,5 +1,5 @@
import { Component, OnDestroy, OnInit } from '@angular/core'; import { Component, OnDestroy, OnInit } from '@angular/core';
import { FormBuilder } from '@angular/forms'; import { UntypedFormBuilder } from '@angular/forms';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { BehaviorSubject, combineLatest, Observable, Subscription } from 'rxjs'; import { BehaviorSubject, combineLatest, Observable, Subscription } from 'rxjs';
@@ -21,6 +21,7 @@ import { RequestService } from '../../core/data/request.service';
import { PageInfo } from '../../core/shared/page-info.model'; import { PageInfo } from '../../core/shared/page-info.model';
import { NoContent } from '../../core/shared/NoContent.model'; import { NoContent } from '../../core/shared/NoContent.model';
import { PaginationService } from '../../core/pagination/pagination.service'; import { PaginationService } from '../../core/pagination/pagination.service';
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
@Component({ @Component({
selector: 'ds-epeople-registry', selector: 'ds-epeople-registry',
@@ -89,11 +90,13 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
private translateService: TranslateService, private translateService: TranslateService,
private notificationsService: NotificationsService, private notificationsService: NotificationsService,
private authorizationService: AuthorizationDataService, private authorizationService: AuthorizationDataService,
private formBuilder: FormBuilder, private formBuilder: UntypedFormBuilder,
private router: Router, private router: Router,
private modalService: NgbModal, private modalService: NgbModal,
private paginationService: PaginationService, private paginationService: PaginationService,
public requestService: RequestService) { public requestService: RequestService,
public dsoNameService: DSONameService,
) {
this.currentSearchQuery = ''; this.currentSearchQuery = '';
this.currentSearchScope = 'metadata'; this.currentSearchScope = 'metadata';
this.searchForm = this.formBuilder.group(({ this.searchForm = this.formBuilder.group(({
@@ -121,7 +124,7 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
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) {
return combineLatest(...epeople.page.map((eperson) => { return combineLatest([...epeople.page.map((eperson: EPerson) => {
return this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(eperson) ? eperson.self : undefined).pipe( return this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(eperson) ? eperson.self : undefined).pipe(
map((authorized) => { map((authorized) => {
const epersonDtoModel: EpersonDtoModel = new EpersonDtoModel(); const epersonDtoModel: EpersonDtoModel = new EpersonDtoModel();
@@ -130,7 +133,7 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
return epersonDtoModel; return epersonDtoModel;
}) })
); );
})).pipe(map((dtos: EpersonDtoModel[]) => { })]).pipe(map((dtos: EpersonDtoModel[]) => {
return buildPaginatedList(epeople.pageInfo, dtos); return buildPaginatedList(epeople.pageInfo, dtos);
})); }));
} else { } else {
@@ -237,7 +240,7 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
if (hasValue(ePerson.id)) { if (hasValue(ePerson.id)) {
this.epersonService.deleteEPerson(ePerson).pipe(getFirstCompletedRemoteData()).subscribe((restResponse: RemoteData<NoContent>) => { this.epersonService.deleteEPerson(ePerson).pipe(getFirstCompletedRemoteData()).subscribe((restResponse: RemoteData<NoContent>) => {
if (restResponse.hasSucceeded) { if (restResponse.hasSucceeded) {
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', {name: ePerson.name})); this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', {name: this.dsoNameService.getName(ePerson)}));
} else { } else {
this.notificationsService.error('Error occured when trying to delete EPerson with id: ' + ePerson.id + ' with code: ' + restResponse.statusCode + ' and message: ' + restResponse.errorMessage); this.notificationsService.error('Error occured when trying to delete EPerson with id: ' + ePerson.id + ' with code: ' + restResponse.statusCode + ' and message: ' + restResponse.errorMessage);
} }
@@ -284,14 +287,17 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
/** /**
* This method will set everything to stale, which will cause the lists on this page to update. * This method will set everything to stale, which will cause the lists on this page to update.
*/ */
reset() { reset(): void {
this.epersonService.getBrowseEndpoint().pipe( this.epersonService.getBrowseEndpoint().pipe(
take(1) take(1),
).subscribe((href: string) => { switchMap((href: string) => {
this.requestService.setStaleByHrefSubstring(href).pipe(take(1)).subscribe(() => { return this.requestService.setStaleByHrefSubstring(href).pipe(
this.epersonService.cancelEditEPerson(); take(1),
this.isEPersonFormShown = false; );
}); })
).subscribe(()=>{
this.epersonService.cancelEditEPerson();
this.isEPersonFormShown = false;
}); });
} }
} }

View File

@@ -65,9 +65,13 @@
<tbody> <tbody>
<tr *ngFor="let group of (groups | async)?.payload?.page"> <tr *ngFor="let group of (groups | async)?.payload?.page">
<td class="align-middle">{{group.id}}</td> <td class="align-middle">{{group.id}}</td>
<td class="align-middle"><a (click)="groupsDataService.startEditingNewGroup(group)" <td class="align-middle">
[routerLink]="[groupsDataService.getGroupEditPageRouterLink(group)]">{{group.name}}</a></td> <a (click)="groupsDataService.startEditingNewGroup(group)"
<td class="align-middle">{{(group.object | async)?.payload?.name}}</td> [routerLink]="[groupsDataService.getGroupEditPageRouterLink(group)]">
{{ dsoNameService.getName(group) }}
</a>
</td>
<td class="align-middle">{{ dsoNameService.getName(undefined) }}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@@ -2,7 +2,7 @@ import { Observable, of as observableOf } from 'rxjs';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { NO_ERRORS_SCHEMA } from '@angular/core'; import { NO_ERRORS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; import { UntypedFormControl, UntypedFormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
import { BrowserModule, By } from '@angular/platform-browser'; import { BrowserModule, By } from '@angular/platform-browser';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
@@ -116,9 +116,9 @@ describe('EPersonFormComponent', () => {
const controlModel = model; const controlModel = model;
const controlState = { value: controlModel.value, disabled: controlModel.disabled }; const controlState = { value: controlModel.value, disabled: controlModel.disabled };
const controlOptions = this.createAbstractControlOptions(controlModel.validators, controlModel.asyncValidators, controlModel.updateOn); const controlOptions = this.createAbstractControlOptions(controlModel.validators, controlModel.asyncValidators, controlModel.updateOn);
controls[model.id] = new FormControl(controlState, controlOptions); controls[model.id] = new UntypedFormControl(controlState, controlOptions);
}); });
return new FormGroup(controls, options); return new UntypedFormGroup(controls, options);
}, },
createAbstractControlOptions(validatorsConfig = null, asyncValidatorsConfig = null, updateOn = null) { createAbstractControlOptions(validatorsConfig = null, asyncValidatorsConfig = null, updateOn = null) {
return { return {

View File

@@ -1,5 +1,5 @@
import { ChangeDetectorRef, Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core'; import { ChangeDetectorRef, Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core';
import { FormGroup } from '@angular/forms'; import { UntypedFormGroup } from '@angular/forms';
import { import {
DynamicCheckboxModel, DynamicCheckboxModel,
DynamicFormControlModel, DynamicFormControlModel,
@@ -8,7 +8,7 @@ import {
} from '@ng-dynamic-forms/core'; } from '@ng-dynamic-forms/core';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { combineLatest as observableCombineLatest, Observable, of as observableOf, Subscription } from 'rxjs'; import { combineLatest as observableCombineLatest, Observable, of as observableOf, Subscription } from 'rxjs';
import { debounceTime, switchMap, take } from 'rxjs/operators'; import { debounceTime, finalize, map, switchMap, take } from 'rxjs/operators';
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 { EPersonDataService } from '../../../core/eperson/eperson-data.service'; import { EPersonDataService } from '../../../core/eperson/eperson-data.service';
@@ -37,6 +37,7 @@ import { ValidateEmailNotTaken } from './validators/email-taken.validator';
import { Registration } from '../../../core/shared/registration.model'; import { Registration } from '../../../core/shared/registration.model';
import { EpersonRegistrationService } from '../../../core/data/eperson-registration.service'; import { EpersonRegistrationService } from '../../../core/data/eperson-registration.service';
import { TYPE_REQUEST_FORGOT } from '../../../register-email-form/register-email-form.component'; import { TYPE_REQUEST_FORGOT } from '../../../register-email-form/register-email-form.component';
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
@Component({ @Component({
selector: 'ds-eperson-form', selector: 'ds-eperson-form',
@@ -108,7 +109,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
/** /**
* A FormGroup that combines all inputs * A FormGroup that combines all inputs
*/ */
formGroup: FormGroup; formGroup: UntypedFormGroup;
/** /**
* An EventEmitter that's fired whenever the form is being submitted * An EventEmitter that's fired whenever the form is being submitted
@@ -192,6 +193,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
private paginationService: PaginationService, private paginationService: PaginationService,
public requestService: RequestService, public requestService: RequestService,
private epersonRegistrationService: EpersonRegistrationService, private epersonRegistrationService: EpersonRegistrationService,
public dsoNameService: DSONameService,
) { ) {
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => { this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
this.epersonInitial = eperson; this.epersonInitial = eperson;
@@ -212,14 +214,14 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
*/ */
initialisePage() { initialisePage() {
observableCombineLatest( observableCombineLatest([
this.translateService.get(`${this.messagePrefix}.firstName`), this.translateService.get(`${this.messagePrefix}.firstName`),
this.translateService.get(`${this.messagePrefix}.lastName`), this.translateService.get(`${this.messagePrefix}.lastName`),
this.translateService.get(`${this.messagePrefix}.email`), this.translateService.get(`${this.messagePrefix}.email`),
this.translateService.get(`${this.messagePrefix}.canLogIn`), this.translateService.get(`${this.messagePrefix}.canLogIn`),
this.translateService.get(`${this.messagePrefix}.requireCertificate`), this.translateService.get(`${this.messagePrefix}.requireCertificate`),
this.translateService.get(`${this.messagePrefix}.emailHint`), this.translateService.get(`${this.messagePrefix}.emailHint`),
).subscribe(([firstName, lastName, email, canLogIn, requireCertificate, emailHint]) => { ]).subscribe(([firstName, lastName, email, canLogIn, requireCertificate, emailHint]) => {
this.firstName = new DynamicInputModel({ this.firstName = new DynamicInputModel({
id: 'firstName', id: 'firstName',
label: firstName, label: firstName,
@@ -386,10 +388,10 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
getFirstCompletedRemoteData() getFirstCompletedRemoteData()
).subscribe((rd: RemoteData<EPerson>) => { ).subscribe((rd: RemoteData<EPerson>) => {
if (rd.hasSucceeded) { if (rd.hasSucceeded) {
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.created.success', { name: ePersonToCreate.name })); this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.created.success', { name: this.dsoNameService.getName(ePersonToCreate) }));
this.submitForm.emit(ePersonToCreate); this.submitForm.emit(ePersonToCreate);
} else { } else {
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.created.failure', { name: ePersonToCreate.name })); this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.created.failure', { name: this.dsoNameService.getName(ePersonToCreate) }));
this.cancelForm.emit(); this.cancelForm.emit();
} }
}); });
@@ -425,10 +427,10 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
const response = this.epersonService.updateEPerson(editedEperson); const response = this.epersonService.updateEPerson(editedEperson);
response.pipe(getFirstCompletedRemoteData()).subscribe((rd: RemoteData<EPerson>) => { response.pipe(getFirstCompletedRemoteData()).subscribe((rd: RemoteData<EPerson>) => {
if (rd.hasSucceeded) { if (rd.hasSucceeded) {
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.edited.success', { name: editedEperson.name })); this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.edited.success', { name: this.dsoNameService.getName(editedEperson) }));
this.submitForm.emit(editedEperson); this.submitForm.emit(editedEperson);
} else { } else {
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.edited.failure', { name: editedEperson.name })); this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.edited.failure', { name: this.dsoNameService.getName(editedEperson) }));
this.cancelForm.emit(); this.cancelForm.emit();
} }
}); });
@@ -461,31 +463,42 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
* Deletes the EPerson from the Repository. The EPerson will be the only that this form is showing. * Deletes the EPerson from the Repository. The EPerson will be the only that this form is showing.
* 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(): void {
this.epersonService.getActiveEPerson().pipe(take(1)).subscribe((eperson: EPerson) => { this.epersonService.getActiveEPerson().pipe(
const modalRef = this.modalService.open(ConfirmationModalComponent); take(1),
modalRef.componentInstance.dso = eperson; switchMap((eperson: EPerson) => {
modalRef.componentInstance.headerLabel = 'confirmation-modal.delete-eperson.header'; const modalRef = this.modalService.open(ConfirmationModalComponent);
modalRef.componentInstance.infoLabel = 'confirmation-modal.delete-eperson.info'; modalRef.componentInstance.dso = eperson;
modalRef.componentInstance.cancelLabel = 'confirmation-modal.delete-eperson.cancel'; modalRef.componentInstance.headerLabel = 'confirmation-modal.delete-eperson.header';
modalRef.componentInstance.confirmLabel = 'confirmation-modal.delete-eperson.confirm'; modalRef.componentInstance.infoLabel = 'confirmation-modal.delete-eperson.info';
modalRef.componentInstance.brandColor = 'danger'; modalRef.componentInstance.cancelLabel = 'confirmation-modal.delete-eperson.cancel';
modalRef.componentInstance.confirmIcon = 'fas fa-trash'; modalRef.componentInstance.confirmLabel = 'confirmation-modal.delete-eperson.confirm';
modalRef.componentInstance.response.pipe(take(1)).subscribe((confirm: boolean) => { modalRef.componentInstance.brandColor = 'danger';
if (confirm) { modalRef.componentInstance.confirmIcon = 'fas fa-trash';
if (hasValue(eperson.id)) {
this.epersonService.deleteEPerson(eperson).pipe(getFirstCompletedRemoteData()).subscribe((restResponse: RemoteData<NoContent>) => { return modalRef.componentInstance.response.pipe(
if (restResponse.hasSucceeded) { take(1),
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', { name: eperson.name })); switchMap((confirm: boolean) => {
this.submitForm.emit(); if (confirm && hasValue(eperson.id)) {
} else { this.canDelete$ = observableOf(false);
this.notificationsService.error('Error occured when trying to delete EPerson with id: ' + eperson.id + ' with code: ' + restResponse.statusCode + ' and message: ' + restResponse.errorMessage); return this.epersonService.deleteEPerson(eperson).pipe(
} getFirstCompletedRemoteData(),
this.cancelForm.emit(); map((restResponse: RemoteData<NoContent>) => ({ restResponse, eperson }))
}); );
} } else {
} return observableOf(null);
}); }
}),
finalize(() => this.canDelete$ = observableOf(true))
);
})
).subscribe(({ restResponse, eperson }: { restResponse: RemoteData<NoContent> | null, eperson: EPerson }) => {
if (restResponse?.hasSucceeded) {
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', { name: this.dsoNameService.getName(eperson) }));
} else {
this.notificationsService.error(`Error occurred when trying to delete EPerson with id: ${eperson?.id} with code: ${restResponse?.statusCode} and message: ${restResponse?.errorMessage}`);
}
this.cancelForm.emit();
}); });
} }
@@ -521,7 +534,6 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
* Cancel the current edit when component is destroyed & unsub all subscriptions * Cancel the current edit when component is destroyed & unsub all subscriptions
*/ */
ngOnDestroy(): void { ngOnDestroy(): void {
this.onCancel();
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe()); this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
this.paginationService.clearPagination(this.config.id); this.paginationService.clearPagination(this.config.id);
if (hasValue(this.emailValueChangeSubscribe)) { if (hasValue(this.emailValueChangeSubscribe)) {
@@ -554,7 +566,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
.subscribe((list: PaginatedList<EPerson>) => { .subscribe((list: PaginatedList<EPerson>) => {
if (list.totalElements > 0) { if (list.totalElements > 0) {
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.' + notificationSection + '.failure.emailInUse', { this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.' + notificationSection + '.failure.emailInUse', {
name: ePerson.name, name: this.dsoNameService.getName(ePerson),
email: ePerson.email email: ePerson.email
})); }));
} }

View File

@@ -26,7 +26,7 @@
<ds-alert *ngIf="groupBeingEdited?.permanent" [type]="AlertTypeEnum.Warning" <ds-alert *ngIf="groupBeingEdited?.permanent" [type]="AlertTypeEnum.Warning"
[content]="messagePrefix + '.alert.permanent'"></ds-alert> [content]="messagePrefix + '.alert.permanent'"></ds-alert>
<ds-alert *ngIf="!(canEdit$ | async) && (groupDataService.getActiveGroup() | async)" [type]="AlertTypeEnum.Warning" <ds-alert *ngIf="!(canEdit$ | async) && (groupDataService.getActiveGroup() | async)" [type]="AlertTypeEnum.Warning"
[content]="(messagePrefix + '.alert.workflowGroup' | translate:{ name: (getLinkedDSO(groupBeingEdited) | async)?.payload?.name, comcol: (getLinkedDSO(groupBeingEdited) | async)?.payload?.type, comcolEditRolesRoute: (getLinkedEditRolesRoute(groupBeingEdited) | async) })"> [content]="(messagePrefix + '.alert.workflowGroup' | translate:{ name: dsoNameService.getName((getLinkedDSO(groupBeingEdited) | async)?.payload), comcol: (getLinkedDSO(groupBeingEdited) | async)?.payload?.type, comcolEditRolesRoute: (getLinkedEditRolesRoute(groupBeingEdited) | async) })">
</ds-alert> </ds-alert>
<ds-form [formId]="formId" <ds-form [formId]="formId"

View File

@@ -2,7 +2,7 @@ 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 { NO_ERRORS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; import { UntypedFormControl, UntypedFormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
import { BrowserModule, By } from '@angular/platform-browser'; import { BrowserModule, By } from '@angular/platform-browser';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
@@ -36,6 +36,8 @@ import { NotificationsServiceStub } from '../../../shared/testing/notifications-
import { Operation } from 'fast-json-patch'; import { Operation } from 'fast-json-patch';
import { ValidateGroupExists } from './validators/group-exists.validator'; import { ValidateGroupExists } from './validators/group-exists.validator';
import { NoContent } from '../../../core/shared/NoContent.model'; import { NoContent } from '../../../core/shared/NoContent.model';
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
import { DSONameServiceMock } from '../../../shared/mocks/dso-name.service.mock';
describe('GroupFormComponent', () => { describe('GroupFormComponent', () => {
let component: GroupFormComponent; let component: GroupFormComponent;
@@ -130,9 +132,9 @@ describe('GroupFormComponent', () => {
const controlModel = model; const controlModel = model;
const controlState = { value: controlModel.value, disabled: controlModel.disabled }; const controlState = { value: controlModel.value, disabled: controlModel.disabled };
const controlOptions = this.createAbstractControlOptions(controlModel.validators, controlModel.asyncValidators, controlModel.updateOn); const controlOptions = this.createAbstractControlOptions(controlModel.validators, controlModel.asyncValidators, controlModel.updateOn);
controls[model.id] = new FormControl(controlState, controlOptions); controls[model.id] = new UntypedFormControl(controlState, controlOptions);
}); });
return new FormGroup(controls, options); return new UntypedFormGroup(controls, options);
}, },
createAbstractControlOptions(validatorsConfig = null, asyncValidatorsConfig = null, updateOn = null) { createAbstractControlOptions(validatorsConfig = null, asyncValidatorsConfig = null, updateOn = null) {
return { return {
@@ -188,7 +190,7 @@ describe('GroupFormComponent', () => {
translateService = getMockTranslateService(); translateService = getMockTranslateService();
router = new RouterMock(); router = new RouterMock();
notificationService = new NotificationsServiceStub(); notificationService = new NotificationsServiceStub();
TestBed.configureTestingModule({ return TestBed.configureTestingModule({
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
TranslateModule.forRoot({ TranslateModule.forRoot({
loader: { loader: {
@@ -198,7 +200,8 @@ describe('GroupFormComponent', () => {
}), }),
], ],
declarations: [GroupFormComponent], declarations: [GroupFormComponent],
providers: [GroupFormComponent, providers: [
{ provide: DSONameService, useValue: new DSONameServiceMock() },
{ provide: EPersonDataService, useValue: ePersonDataServiceStub }, { provide: EPersonDataService, useValue: ePersonDataServiceStub },
{ provide: GroupDataService, useValue: groupsDataServiceStub }, { provide: GroupDataService, useValue: groupsDataServiceStub },
{ provide: DSpaceObjectDataService, useValue: dsoDataServiceStub }, { provide: DSpaceObjectDataService, useValue: dsoDataServiceStub },
@@ -240,8 +243,8 @@ describe('GroupFormComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should emit a new group using the correct values', waitForAsync(() => { it('should emit a new group using the correct values', (async () => {
fixture.whenStable().then(() => { await fixture.whenStable().then(() => {
expect(component.submitForm.emit).toHaveBeenCalledWith(expected); expect(component.submitForm.emit).toHaveBeenCalledWith(expected);
}); });
})); }));
@@ -303,8 +306,8 @@ 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', waitForAsync(() => { it('should emit the existing group using the correct new values', (async () => {
fixture.whenStable().then(() => { await fixture.whenStable().then(() => {
expect(component.submitForm.emit).toHaveBeenCalledWith(expected2); expect(component.submitForm.emit).toHaveBeenCalledWith(expected2);
}); });
})); }));

View File

@@ -1,5 +1,5 @@
import { Component, EventEmitter, HostListener, OnDestroy, OnInit, Output, ChangeDetectorRef } from '@angular/core'; import { Component, EventEmitter, HostListener, OnDestroy, OnInit, Output, ChangeDetectorRef } from '@angular/core';
import { FormGroup } from '@angular/forms'; import { UntypedFormGroup } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { import {
@@ -46,6 +46,7 @@ import { followLink } from '../../../shared/utils/follow-link-config.model';
import { NoContent } from '../../../core/shared/NoContent.model'; import { NoContent } from '../../../core/shared/NoContent.model';
import { Operation } from 'fast-json-patch'; import { Operation } from 'fast-json-patch';
import { ValidateGroupExists } from './validators/group-exists.validator'; import { ValidateGroupExists } from './validators/group-exists.validator';
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
import { environment } from '../../../../environments/environment'; import { environment } from '../../../../environments/environment';
@Component({ @Component({
@@ -95,7 +96,7 @@ export class GroupFormComponent implements OnInit, OnDestroy {
/** /**
* A FormGroup that combines all inputs * A FormGroup that combines all inputs
*/ */
formGroup: FormGroup; formGroup: UntypedFormGroup;
/** /**
* An EventEmitter that's fired whenever the form is being submitted * An EventEmitter that's fired whenever the form is being submitted
@@ -134,7 +135,8 @@ export class GroupFormComponent implements OnInit, OnDestroy {
groupNameValueChangeSubscribe: Subscription; groupNameValueChangeSubscribe: Subscription;
constructor(public groupDataService: GroupDataService, constructor(
public groupDataService: GroupDataService,
private ePersonDataService: EPersonDataService, private ePersonDataService: EPersonDataService,
private dSpaceObjectDataService: DSpaceObjectDataService, private dSpaceObjectDataService: DSpaceObjectDataService,
private formBuilderService: FormBuilderService, private formBuilderService: FormBuilderService,
@@ -145,7 +147,9 @@ export class GroupFormComponent implements OnInit, OnDestroy {
private authorizationService: AuthorizationDataService, private authorizationService: AuthorizationDataService,
private modalService: NgbModal, private modalService: NgbModal,
public requestService: RequestService, public requestService: RequestService,
protected changeDetectorRef: ChangeDetectorRef) { protected changeDetectorRef: ChangeDetectorRef,
public dsoNameService: DSONameService,
) {
} }
ngOnInit() { ngOnInit() {
@@ -331,7 +335,7 @@ export class GroupFormComponent implements OnInit, OnDestroy {
.subscribe((list: PaginatedList<Group>) => { .subscribe((list: PaginatedList<Group>) => {
if (list.totalElements > 0) { if (list.totalElements > 0) {
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.' + notificationSection + '.failure.groupNameInUse', { this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.' + notificationSection + '.failure.groupNameInUse', {
name: group.name name: this.dsoNameService.getName(group),
})); }));
} }
})); }));
@@ -364,10 +368,10 @@ export class GroupFormComponent implements OnInit, OnDestroy {
getFirstCompletedRemoteData() getFirstCompletedRemoteData()
).subscribe((rd: RemoteData<Group>) => { ).subscribe((rd: RemoteData<Group>) => {
if (rd.hasSucceeded) { if (rd.hasSucceeded) {
this.notificationsService.success(this.translateService.get(this.messagePrefix + '.notification.edited.success', { name: rd.payload.name })); this.notificationsService.success(this.translateService.get(this.messagePrefix + '.notification.edited.success', { name: this.dsoNameService.getName(rd.payload) }));
this.submitForm.emit(rd.payload); this.submitForm.emit(rd.payload);
} else { } else {
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.edited.failure', { name: group.name })); this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.edited.failure', { name: this.dsoNameService.getName(group) }));
this.cancelForm.emit(); this.cancelForm.emit();
} }
}); });
@@ -427,11 +431,11 @@ export class GroupFormComponent implements OnInit, OnDestroy {
this.groupDataService.delete(group.id).pipe(getFirstCompletedRemoteData()) this.groupDataService.delete(group.id).pipe(getFirstCompletedRemoteData())
.subscribe((rd: RemoteData<NoContent>) => { .subscribe((rd: RemoteData<NoContent>) => {
if (rd.hasSucceeded) { if (rd.hasSucceeded) {
this.notificationsService.success(this.translateService.get(this.messagePrefix + '.notification.deleted.success', { name: group.name })); this.notificationsService.success(this.translateService.get(this.messagePrefix + '.notification.deleted.success', { name: this.dsoNameService.getName(group) }));
this.onCancel(); this.onCancel();
} else { } else {
this.notificationsService.error( this.notificationsService.error(
this.translateService.get(this.messagePrefix + '.notification.deleted.failure.title', { name: group.name }), this.translateService.get(this.messagePrefix + '.notification.deleted.failure.title', { name: this.dsoNameService.getName(group) }),
this.translateService.get(this.messagePrefix + '.notification.deleted.failure.content', { cause: rd.errorMessage })); this.translateService.get(this.messagePrefix + '.notification.deleted.failure.content', { cause: rd.errorMessage }));
} }
}); });

View File

@@ -57,8 +57,12 @@
<tbody> <tbody>
<tr *ngFor="let ePerson of (ePeopleSearchDtos | async)?.page"> <tr *ngFor="let ePerson of (ePeopleSearchDtos | async)?.page">
<td class="align-middle">{{ePerson.eperson.id}}</td> <td class="align-middle">{{ePerson.eperson.id}}</td>
<td class="align-middle"><a (click)="ePersonDataService.startEditingNewEPerson(ePerson.eperson)" <td class="align-middle">
[routerLink]="[ePersonDataService.getEPeoplePageRouterLink()]">{{ePerson.eperson.name}}</a></td> <a (click)="ePersonDataService.startEditingNewEPerson(ePerson.eperson)"
[routerLink]="[ePersonDataService.getEPeoplePageRouterLink()]">
{{ dsoNameService.getName(ePerson.eperson) }}
</a>
</td>
<td class="align-middle"> <td class="align-middle">
{{messagePrefix + '.table.email' | translate}}: {{ ePerson.eperson.email ? ePerson.eperson.email : '-' }}<br/> {{messagePrefix + '.table.email' | translate}}: {{ ePerson.eperson.email ? ePerson.eperson.email : '-' }}<br/>
{{messagePrefix + '.table.netid' | translate}}: {{ ePerson.eperson.netid ? ePerson.eperson.netid : '-' }} {{messagePrefix + '.table.netid' | translate}}: {{ ePerson.eperson.netid ? ePerson.eperson.netid : '-' }}
@@ -69,7 +73,7 @@
(click)="deleteMemberFromGroup(ePerson)" (click)="deleteMemberFromGroup(ePerson)"
[disabled]="actionConfig.remove.disabled" [disabled]="actionConfig.remove.disabled"
[ngClass]="['btn btn-sm', actionConfig.remove.css]" [ngClass]="['btn btn-sm', actionConfig.remove.css]"
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: {name: ePerson.eperson.name} }}"> title="{{messagePrefix + '.table.edit.buttons.remove' | translate: { name: dsoNameService.getName(ePerson.eperson) } }}">
<i [ngClass]="actionConfig.remove.icon"></i> <i [ngClass]="actionConfig.remove.icon"></i>
</button> </button>
@@ -77,7 +81,7 @@
(click)="addMemberToGroup(ePerson)" (click)="addMemberToGroup(ePerson)"
[disabled]="actionConfig.add.disabled" [disabled]="actionConfig.add.disabled"
[ngClass]="['btn btn-sm', actionConfig.add.css]" [ngClass]="['btn btn-sm', actionConfig.add.css]"
title="{{messagePrefix + '.table.edit.buttons.add' | translate: {name: ePerson.eperson.name} }}"> title="{{messagePrefix + '.table.edit.buttons.add' | translate: { name: dsoNameService.getName(ePerson.eperson) } }}">
<i [ngClass]="actionConfig.add.icon"></i> <i [ngClass]="actionConfig.add.icon"></i>
</button> </button>
</div> </div>
@@ -117,8 +121,12 @@
<tbody> <tbody>
<tr *ngFor="let ePerson of (ePeopleMembersOfGroupDtos | async)?.page"> <tr *ngFor="let ePerson of (ePeopleMembersOfGroupDtos | async)?.page">
<td class="align-middle">{{ePerson.eperson.id}}</td> <td class="align-middle">{{ePerson.eperson.id}}</td>
<td class="align-middle"><a (click)="ePersonDataService.startEditingNewEPerson(ePerson.eperson)" <td class="align-middle">
[routerLink]="[ePersonDataService.getEPeoplePageRouterLink()]">{{ePerson.eperson.name}}</a></td> <a (click)="ePersonDataService.startEditingNewEPerson(ePerson.eperson)"
[routerLink]="[ePersonDataService.getEPeoplePageRouterLink()]">
{{ dsoNameService.getName(ePerson.eperson) }}
</a>
</td>
<td class="align-middle"> <td class="align-middle">
{{messagePrefix + '.table.email' | translate}}: {{ ePerson.eperson.email ? ePerson.eperson.email : '-' }}<br/> {{messagePrefix + '.table.email' | translate}}: {{ ePerson.eperson.email ? ePerson.eperson.email : '-' }}<br/>
{{messagePrefix + '.table.netid' | translate}}: {{ ePerson.eperson.netid ? ePerson.eperson.netid : '-' }} {{messagePrefix + '.table.netid' | translate}}: {{ ePerson.eperson.netid ? ePerson.eperson.netid : '-' }}
@@ -129,14 +137,14 @@
(click)="deleteMemberFromGroup(ePerson)" (click)="deleteMemberFromGroup(ePerson)"
[disabled]="actionConfig.remove.disabled" [disabled]="actionConfig.remove.disabled"
[ngClass]="['btn btn-sm', actionConfig.remove.css]" [ngClass]="['btn btn-sm', actionConfig.remove.css]"
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: {name: ePerson.eperson.name} }}"> title="{{messagePrefix + '.table.edit.buttons.remove' | translate: { name: dsoNameService.getName(ePerson.eperson) } }}">
<i [ngClass]="actionConfig.remove.icon"></i> <i [ngClass]="actionConfig.remove.icon"></i>
</button> </button>
<button *ngIf="!ePerson.memberOfGroup" <button *ngIf="!ePerson.memberOfGroup"
(click)="addMemberToGroup(ePerson)" (click)="addMemberToGroup(ePerson)"
[disabled]="actionConfig.add.disabled" [disabled]="actionConfig.add.disabled"
[ngClass]="['btn btn-sm', actionConfig.add.css]" [ngClass]="['btn btn-sm', actionConfig.add.css]"
title="{{messagePrefix + '.table.edit.buttons.add' | translate: {name: ePerson.eperson.name} }}"> title="{{messagePrefix + '.table.edit.buttons.add' | translate: { name: dsoNameService.getName(ePerson.eperson) } }}">
<i [ngClass]="actionConfig.add.icon"></i> <i [ngClass]="actionConfig.add.icon"></i>
</button> </button>
</div> </div>

View File

@@ -28,6 +28,8 @@ import { NotificationsServiceStub } from '../../../../shared/testing/notificatio
import { RouterMock } from '../../../../shared/mocks/router.mock'; import { RouterMock } from '../../../../shared/mocks/router.mock';
import { PaginationService } from '../../../../core/pagination/pagination.service'; import { PaginationService } from '../../../../core/pagination/pagination.service';
import { PaginationServiceStub } from '../../../../shared/testing/pagination-service.stub'; import { PaginationServiceStub } from '../../../../shared/testing/pagination-service.stub';
import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
import { DSONameServiceMock } from '../../../../shared/mocks/dso-name.service.mock';
describe('MembersListComponent', () => { describe('MembersListComponent', () => {
let component: MembersListComponent; let component: MembersListComponent;
@@ -118,7 +120,7 @@ describe('MembersListComponent', () => {
translateService = getMockTranslateService(); translateService = getMockTranslateService();
paginationService = new PaginationServiceStub(); paginationService = new PaginationServiceStub();
TestBed.configureTestingModule({ return TestBed.configureTestingModule({
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
TranslateModule.forRoot({ TranslateModule.forRoot({
loader: { loader: {
@@ -135,6 +137,7 @@ describe('MembersListComponent', () => {
{ provide: FormBuilderService, useValue: builderService }, { provide: FormBuilderService, useValue: builderService },
{ provide: Router, useValue: new RouterMock() }, { provide: Router, useValue: new RouterMock() },
{ provide: PaginationService, useValue: paginationService }, { provide: PaginationService, useValue: paginationService },
{ provide: DSONameService, useValue: new DSONameServiceMock() },
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).compileComponents(); }).compileComponents();

View File

@@ -1,5 +1,5 @@
import { Component, Input, OnDestroy, OnInit } from '@angular/core'; import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { FormBuilder } from '@angular/forms'; import { UntypedFormBuilder } from '@angular/forms';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { import {
@@ -27,6 +27,7 @@ import { NotificationsService } from '../../../../shared/notifications/notificat
import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model'; import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model';
import { EpersonDtoModel } from '../../../../core/eperson/models/eperson-dto.model'; import { EpersonDtoModel } from '../../../../core/eperson/models/eperson-dto.model';
import { PaginationService } from '../../../../core/pagination/pagination.service'; import { PaginationService } from '../../../../core/pagination/pagination.service';
import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
/** /**
* Keys to keep track of specific subscriptions * Keys to keep track of specific subscriptions
@@ -141,9 +142,10 @@ export class MembersListComponent implements OnInit, OnDestroy {
public ePersonDataService: EPersonDataService, public ePersonDataService: EPersonDataService,
protected translateService: TranslateService, protected translateService: TranslateService,
protected notificationsService: NotificationsService, protected notificationsService: NotificationsService,
protected formBuilder: FormBuilder, protected formBuilder: UntypedFormBuilder,
protected paginationService: PaginationService, protected paginationService: PaginationService,
private router: Router protected router: Router,
public dsoNameService: DSONameService,
) { ) {
this.currentSearchQuery = ''; this.currentSearchQuery = '';
this.currentSearchScope = 'metadata'; this.currentSearchScope = 'metadata';
@@ -253,7 +255,7 @@ export class MembersListComponent implements OnInit, OnDestroy {
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => { this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => {
if (activeGroup != null) { if (activeGroup != null) {
const response = this.groupDataService.deleteMemberFromGroup(activeGroup, ePerson.eperson); const response = this.groupDataService.deleteMemberFromGroup(activeGroup, ePerson.eperson);
this.showNotifications('deleteMember', response, ePerson.eperson.name, activeGroup); this.showNotifications('deleteMember', response, this.dsoNameService.getName(ePerson.eperson), activeGroup);
} else { } else {
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup')); this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup'));
} }
@@ -269,7 +271,7 @@ export class MembersListComponent implements OnInit, OnDestroy {
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => { this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => {
if (activeGroup != null) { if (activeGroup != null) {
const response = this.groupDataService.addMemberToGroup(activeGroup, ePerson.eperson); const response = this.groupDataService.addMemberToGroup(activeGroup, ePerson.eperson);
this.showNotifications('addMember', response, ePerson.eperson.name, activeGroup); this.showNotifications('addMember', response, this.dsoNameService.getName(ePerson.eperson), activeGroup);
} else { } else {
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup')); this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup'));
} }

View File

@@ -53,15 +53,19 @@
<tbody> <tbody>
<tr *ngFor="let group of (searchResults$ | async)?.payload?.page"> <tr *ngFor="let group of (searchResults$ | async)?.payload?.page">
<td class="align-middle">{{group.id}}</td> <td class="align-middle">{{group.id}}</td>
<td class="align-middle"><a (click)="groupDataService.startEditingNewGroup(group)" <td class="align-middle">
[routerLink]="[groupDataService.getGroupEditPageRouterLink(group)]">{{group.name}}</a></td> <a (click)="groupDataService.startEditingNewGroup(group)"
<td class="align-middle">{{(group.object | async)?.payload?.name}}</td> [routerLink]="[groupDataService.getGroupEditPageRouterLink(group)]">
{{ dsoNameService.getName(group) }}
</a>
</td>
<td class="align-middle">{{ dsoNameService.getName((group.object | async)?.payload) }}</td>
<td class="align-middle"> <td class="align-middle">
<div class="btn-group edit-field"> <div class="btn-group edit-field">
<button *ngIf="(isSubgroupOfGroup(group) | async) && !(isActiveGroup(group) | async)" <button *ngIf="(isSubgroupOfGroup(group) | async) && !(isActiveGroup(group) | async)"
(click)="deleteSubgroupFromGroup(group)" (click)="deleteSubgroupFromGroup(group)"
class="btn btn-outline-danger btn-sm deleteButton" class="btn btn-outline-danger btn-sm deleteButton"
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: {name: group.name} }}"> title="{{messagePrefix + '.table.edit.buttons.remove' | translate: { name: dsoNameService.getName(group) } }}">
<i class="fas fa-trash-alt fa-fw"></i> <i class="fas fa-trash-alt fa-fw"></i>
</button> </button>
@@ -70,7 +74,7 @@
<button *ngIf="!(isSubgroupOfGroup(group) | async) && !(isActiveGroup(group) | async)" <button *ngIf="!(isSubgroupOfGroup(group) | async) && !(isActiveGroup(group) | async)"
(click)="addSubgroupToGroup(group)" (click)="addSubgroupToGroup(group)"
class="btn btn-outline-primary btn-sm addButton" class="btn btn-outline-primary btn-sm addButton"
title="{{messagePrefix + '.table.edit.buttons.add' | translate: {name: group.name} }}"> title="{{messagePrefix + '.table.edit.buttons.add' | translate: { name: dsoNameService.getName(group) } }}">
<i class="fas fa-plus fa-fw"></i> <i class="fas fa-plus fa-fw"></i>
</button> </button>
</div> </div>
@@ -108,14 +112,18 @@
<tbody> <tbody>
<tr *ngFor="let group of (subGroups$ | async)?.payload?.page"> <tr *ngFor="let group of (subGroups$ | async)?.payload?.page">
<td class="align-middle">{{group.id}}</td> <td class="align-middle">{{group.id}}</td>
<td class="align-middle"><a (click)="groupDataService.startEditingNewGroup(group)" <td class="align-middle">
[routerLink]="[groupDataService.getGroupEditPageRouterLink(group)]">{{group.name}}</a></td> <a (click)="groupDataService.startEditingNewGroup(group)"
<td class="align-middle">{{(group.object | async)?.payload?.name}}</td> [routerLink]="[groupDataService.getGroupEditPageRouterLink(group)]">
{{ dsoNameService.getName(group) }}
</a>
</td>
<td class="align-middle">{{ dsoNameService.getName((group.object | async)?.payload)}}</td>
<td class="align-middle"> <td class="align-middle">
<div class="btn-group edit-field"> <div class="btn-group edit-field">
<button (click)="deleteSubgroupFromGroup(group)" <button (click)="deleteSubgroupFromGroup(group)"
class="btn btn-outline-danger btn-sm deleteButton" class="btn btn-outline-danger btn-sm deleteButton"
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: {name: group.name} }}"> title="{{messagePrefix + '.table.edit.buttons.remove' | translate: { name: dsoNameService.getName(group) } }}">
<i class="fas fa-trash-alt fa-fw"></i> <i class="fas fa-trash-alt fa-fw"></i>
</button> </button>
</div> </div>

View File

@@ -29,6 +29,8 @@ import { NotificationsServiceStub } from '../../../../shared/testing/notificatio
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { PaginationService } from '../../../../core/pagination/pagination.service'; import { PaginationService } from '../../../../core/pagination/pagination.service';
import { PaginationServiceStub } from '../../../../shared/testing/pagination-service.stub'; import { PaginationServiceStub } from '../../../../shared/testing/pagination-service.stub';
import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
import { DSONameServiceMock } from '../../../../shared/mocks/dso-name.service.mock';
describe('SubgroupsListComponent', () => { describe('SubgroupsListComponent', () => {
let component: SubgroupsListComponent; let component: SubgroupsListComponent;
@@ -108,6 +110,7 @@ describe('SubgroupsListComponent', () => {
], ],
declarations: [SubgroupsListComponent], declarations: [SubgroupsListComponent],
providers: [SubgroupsListComponent, providers: [SubgroupsListComponent,
{ provide: DSONameService, useValue: new DSONameServiceMock() },
{ provide: GroupDataService, useValue: groupsDataServiceStub }, { provide: GroupDataService, useValue: groupsDataServiceStub },
{ provide: NotificationsService, useValue: new NotificationsServiceStub() }, { provide: NotificationsService, useValue: new NotificationsServiceStub() },
{ provide: FormBuilderService, useValue: builderService }, { provide: FormBuilderService, useValue: builderService },

View File

@@ -1,5 +1,5 @@
import { Component, Input, OnDestroy, OnInit } from '@angular/core'; import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { FormBuilder } from '@angular/forms'; import { UntypedFormBuilder } from '@angular/forms';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { BehaviorSubject, Observable, of as observableOf, Subscription } from 'rxjs'; import { BehaviorSubject, Observable, of as observableOf, Subscription } from 'rxjs';
@@ -18,6 +18,7 @@ import { PaginationComponentOptions } from '../../../../shared/pagination/pagina
import { NoContent } from '../../../../core/shared/NoContent.model'; import { NoContent } from '../../../../core/shared/NoContent.model';
import { PaginationService } from '../../../../core/pagination/pagination.service'; import { PaginationService } from '../../../../core/pagination/pagination.service';
import { followLink } from '../../../../shared/utils/follow-link-config.model'; import { followLink } from '../../../../shared/utils/follow-link-config.model';
import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
/** /**
* Keys to keep track of specific subscriptions * Keys to keep track of specific subscriptions
@@ -86,9 +87,11 @@ export class SubgroupsListComponent implements OnInit, OnDestroy {
constructor(public groupDataService: GroupDataService, constructor(public groupDataService: GroupDataService,
private translateService: TranslateService, private translateService: TranslateService,
private notificationsService: NotificationsService, private notificationsService: NotificationsService,
private formBuilder: FormBuilder, private formBuilder: UntypedFormBuilder,
private paginationService: PaginationService, private paginationService: PaginationService,
private router: Router) { private router: Router,
public dsoNameService: DSONameService,
) {
this.currentSearchQuery = ''; this.currentSearchQuery = '';
} }
@@ -177,7 +180,7 @@ export class SubgroupsListComponent implements OnInit, OnDestroy {
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => { this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => {
if (activeGroup != null) { if (activeGroup != null) {
const response = this.groupDataService.deleteSubGroupFromGroup(activeGroup, subgroup); const response = this.groupDataService.deleteSubGroupFromGroup(activeGroup, subgroup);
this.showNotifications('deleteSubgroup', response, subgroup.name, activeGroup); this.showNotifications('deleteSubgroup', response, this.dsoNameService.getName(subgroup), activeGroup);
} else { } else {
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup')); this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup'));
} }
@@ -193,7 +196,7 @@ export class SubgroupsListComponent implements OnInit, OnDestroy {
if (activeGroup != null) { if (activeGroup != null) {
if (activeGroup.uuid !== subgroup.uuid) { if (activeGroup.uuid !== subgroup.uuid) {
const response = this.groupDataService.addSubGroupToGroup(activeGroup, subgroup); const response = this.groupDataService.addSubGroupToGroup(activeGroup, subgroup);
this.showNotifications('addSubgroup', response, subgroup.name, activeGroup); this.showNotifications('addSubgroup', response, this.dsoNameService.getName(subgroup), activeGroup);
} else { } else {
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.subgroupToAddIsActiveGroup')); this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.subgroupToAddIsActiveGroup'));
} }

View File

@@ -56,8 +56,8 @@
<tbody> <tbody>
<tr *ngFor="let groupDto of (groupsDto$ | async)?.page"> <tr *ngFor="let groupDto of (groupsDto$ | async)?.page">
<td>{{groupDto.group.id}}</td> <td>{{groupDto.group.id}}</td>
<td>{{groupDto.group.name}}</td> <td>{{ dsoNameService.getName(groupDto.group) }}</td>
<td>{{(groupDto.group.object | async)?.payload?.name}}</td> <td>{{ dsoNameService.getName((groupDto.group.object | async)?.payload) }}</td>
<td>{{groupDto.epersons?.totalElements + groupDto.subgroups?.totalElements}}</td> <td>{{groupDto.epersons?.totalElements + groupDto.subgroups?.totalElements}}</td>
<td> <td>
<div class="btn-group edit-field"> <div class="btn-group edit-field">
@@ -65,7 +65,7 @@
<button *ngSwitchCase="true" <button *ngSwitchCase="true"
[routerLink]="groupService.getGroupEditPageRouterLink(groupDto.group)" [routerLink]="groupService.getGroupEditPageRouterLink(groupDto.group)"
class="btn btn-outline-primary btn-sm btn-edit" class="btn btn-outline-primary btn-sm btn-edit"
title="{{messagePrefix + 'table.edit.buttons.edit' | translate: {name: groupDto.group.name} }}" title="{{messagePrefix + 'table.edit.buttons.edit' | translate: {name: dsoNameService.getName(groupDto.group) } }}"
> >
<i class="fas fa-edit fa-fw"></i> <i class="fas fa-edit fa-fw"></i>
</button> </button>
@@ -80,7 +80,7 @@
</ng-container> </ng-container>
<button *ngIf="!groupDto.group?.permanent && groupDto.ableToDelete" <button *ngIf="!groupDto.group?.permanent && groupDto.ableToDelete"
(click)="deleteGroup(groupDto)" class="btn btn-outline-danger btn-sm btn-delete" (click)="deleteGroup(groupDto)" class="btn btn-outline-danger btn-sm btn-delete"
title="{{messagePrefix + 'table.edit.buttons.remove' | translate: {name: groupDto.group.name} }}"> title="{{messagePrefix + 'table.edit.buttons.remove' | translate: {name: dsoNameService.getName(groupDto.group) } }}">
<i class="fas fa-trash-alt fa-fw"></i> <i class="fas fa-trash-alt fa-fw"></i>
</button> </button>
</div> </div>

View File

@@ -32,8 +32,10 @@ import { PaginationService } from '../../core/pagination/pagination.service';
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub'; import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
import { FeatureID } from '../../core/data/feature-authorization/feature-id'; import { FeatureID } from '../../core/data/feature-authorization/feature-id';
import { NoContent } from '../../core/shared/NoContent.model'; import { NoContent } from '../../core/shared/NoContent.model';
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
import { DSONameServiceMock, UNDEFINED_NAME } from '../../shared/mocks/dso-name.service.mock';
describe('GroupRegistryComponent', () => { describe('GroupsRegistryComponent', () => {
let component: GroupsRegistryComponent; let component: GroupsRegistryComponent;
let fixture: ComponentFixture<GroupsRegistryComponent>; let fixture: ComponentFixture<GroupsRegistryComponent>;
let ePersonDataServiceStub: any; let ePersonDataServiceStub: any;
@@ -160,7 +162,7 @@ describe('GroupRegistryComponent', () => {
authorizationService = jasmine.createSpyObj('authorizationService', ['isAuthorized']); authorizationService = jasmine.createSpyObj('authorizationService', ['isAuthorized']);
setIsAuthorized(true, true); setIsAuthorized(true, true);
paginationService = new PaginationServiceStub(); paginationService = new PaginationServiceStub();
TestBed.configureTestingModule({ return TestBed.configureTestingModule({
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
TranslateModule.forRoot({ TranslateModule.forRoot({
loader: { loader: {
@@ -171,6 +173,7 @@ describe('GroupRegistryComponent', () => {
], ],
declarations: [GroupsRegistryComponent], declarations: [GroupsRegistryComponent],
providers: [GroupsRegistryComponent, providers: [GroupsRegistryComponent,
{ provide: DSONameService, useValue: new DSONameServiceMock() },
{ provide: EPersonDataService, useValue: ePersonDataServiceStub }, { provide: EPersonDataService, useValue: ePersonDataServiceStub },
{ provide: GroupDataService, useValue: groupsDataServiceStub }, { provide: GroupDataService, useValue: groupsDataServiceStub },
{ provide: DSpaceObjectDataService, useValue: dsoDataServiceStub }, { provide: DSpaceObjectDataService, useValue: dsoDataServiceStub },
@@ -208,7 +211,7 @@ describe('GroupRegistryComponent', () => {
it('should display community/collection name if present', () => { it('should display community/collection name if present', () => {
const collectionNamesFound = fixture.debugElement.queryAll(By.css('#groups tr td:nth-child(3)')); const collectionNamesFound = fixture.debugElement.queryAll(By.css('#groups tr td:nth-child(3)'));
expect(collectionNamesFound.length).toEqual(2); expect(collectionNamesFound.length).toEqual(2);
expect(collectionNamesFound[0].nativeElement.textContent).toEqual(''); expect(collectionNamesFound[0].nativeElement.textContent).toEqual(UNDEFINED_NAME);
expect(collectionNamesFound[1].nativeElement.textContent).toEqual('testgroupid2objectName'); expect(collectionNamesFound[1].nativeElement.textContent).toEqual('testgroupid2objectName');
}); });

View File

@@ -1,5 +1,5 @@
import { Component, OnDestroy, OnInit } from '@angular/core'; import { Component, OnDestroy, OnInit } from '@angular/core';
import { FormBuilder } from '@angular/forms'; import { UntypedFormBuilder } from '@angular/forms';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { import {
@@ -37,6 +37,7 @@ import { PaginationComponentOptions } from '../../shared/pagination/pagination-c
import { NoContent } from '../../core/shared/NoContent.model'; import { NoContent } from '../../core/shared/NoContent.model';
import { PaginationService } from '../../core/pagination/pagination.service'; import { PaginationService } from '../../core/pagination/pagination.service';
import { followLink } from '../../shared/utils/follow-link-config.model'; import { followLink } from '../../shared/utils/follow-link-config.model';
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
@Component({ @Component({
selector: 'ds-groups-registry', selector: 'ds-groups-registry',
@@ -99,12 +100,14 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
private dSpaceObjectDataService: DSpaceObjectDataService, private dSpaceObjectDataService: DSpaceObjectDataService,
private translateService: TranslateService, private translateService: TranslateService,
private notificationsService: NotificationsService, private notificationsService: NotificationsService,
private formBuilder: FormBuilder, private formBuilder: UntypedFormBuilder,
protected routeService: RouteService, protected routeService: RouteService,
private router: Router, private router: Router,
private authorizationService: AuthorizationDataService, private authorizationService: AuthorizationDataService,
private paginationService: PaginationService, private paginationService: PaginationService,
public requestService: RequestService) { public requestService: RequestService,
public dsoNameService: DSONameService,
) {
this.currentSearchQuery = ''; this.currentSearchQuery = '';
this.searchForm = this.formBuilder.group(({ this.searchForm = this.formBuilder.group(({
query: this.currentSearchQuery, query: this.currentSearchQuery,
@@ -201,10 +204,10 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
.subscribe((rd: RemoteData<NoContent>) => { .subscribe((rd: RemoteData<NoContent>) => {
if (rd.hasSucceeded) { if (rd.hasSucceeded) {
this.deletedGroupsIds = [...this.deletedGroupsIds, group.group.id]; this.deletedGroupsIds = [...this.deletedGroupsIds, group.group.id];
this.notificationsService.success(this.translateService.get(this.messagePrefix + 'notification.deleted.success', { name: group.group.name })); this.notificationsService.success(this.translateService.get(this.messagePrefix + 'notification.deleted.success', { name: this.dsoNameService.getName(group.group) }));
} else { } else {
this.notificationsService.error( this.notificationsService.error(
this.translateService.get(this.messagePrefix + 'notification.deleted.failure.title', { name: group.group.name }), this.translateService.get(this.messagePrefix + 'notification.deleted.failure.title', { name: this.dsoNameService.getName(group.group) }),
this.translateService.get(this.messagePrefix + 'notification.deleted.failure.content', { cause: rd.errorMessage })); this.translateService.get(this.messagePrefix + 'notification.deleted.failure.content', { cause: rd.errorMessage }));
} }
}); });

View File

@@ -20,12 +20,29 @@
</small> </small>
</div> </div>
<ui-switch color="#ebebeb"
[checkedLabel]="'admin.metadata-import.page.toggle.upload' | translate"
[uncheckedLabel]="'admin.metadata-import.page.toggle.url' | translate"
[checked]="isUpload"
(change)="toggleUpload()" ></ui-switch>
<small class="form-text text-muted">
{{'admin.batch-import.page.toggle.help' | translate}}
</small>
<ds-file-dropzone-no-uploader <ds-file-dropzone-no-uploader
*ngIf="isUpload"
data-test="file-dropzone"
(onFileAdded)="setFile($event)" (onFileAdded)="setFile($event)"
[dropMessageLabel]="'admin.batch-import.page.dropMsg'" [dropMessageLabel]="'admin.batch-import.page.dropMsg'"
[dropMessageLabelReplacement]="'admin.batch-import.page.dropMsgReplace'"> [dropMessageLabelReplacement]="'admin.batch-import.page.dropMsgReplace'">
</ds-file-dropzone-no-uploader> </ds-file-dropzone-no-uploader>
<div class="form-group mt-2" *ngIf="!isUpload">
<input class="form-control" type="text" placeholder="{{'admin.metadata-import.page.urlMsg' | translate}}"
data-test="file-url-input" [(ngModel)]="fileURL">
</div>
<div class="space-children-mr"> <div class="space-children-mr">
<button class="btn btn-secondary" id="backButton" <button class="btn btn-secondary" id="backButton"
(click)="this.onReturn();">{{'admin.metadata-import.page.button.return' | translate}}</button> (click)="this.onReturn();">{{'admin.metadata-import.page.button.return' | translate}}</button>

View File

@@ -86,10 +86,18 @@ describe('BatchImportPageComponent', () => {
let fileMock: File; let fileMock: File;
beforeEach(() => { beforeEach(() => {
component.isUpload = true;
fileMock = new File([''], 'filename.zip', { type: 'application/zip' }); fileMock = new File([''], 'filename.zip', { type: 'application/zip' });
component.setFile(fileMock); component.setFile(fileMock);
}); });
it('should show the file dropzone', () => {
const fileDropzone = fixture.debugElement.query(By.css('[data-test="file-dropzone"]'));
const fileUrlInput = fixture.debugElement.query(By.css('[data-test="file-url-input"]'));
expect(fileDropzone).toBeTruthy();
expect(fileUrlInput).toBeFalsy();
});
describe('if proceed button is pressed without validate only', () => { describe('if proceed button is pressed without validate only', () => {
beforeEach(fakeAsync(() => { beforeEach(fakeAsync(() => {
component.validateOnly = false; component.validateOnly = false;
@@ -99,9 +107,9 @@ describe('BatchImportPageComponent', () => {
})); }));
it('metadata-import script is invoked with --zip fileName and the mockFile', () => { it('metadata-import script is invoked with --zip fileName and the mockFile', () => {
const parameterValues: ProcessParameter[] = [ const parameterValues: ProcessParameter[] = [
Object.assign(new ProcessParameter(), { name: '--zip', value: 'filename.zip' }), Object.assign(new ProcessParameter(), { name: '--add' }),
Object.assign(new ProcessParameter(), { name: '--zip', value: 'filename.zip' })
]; ];
parameterValues.push(Object.assign(new ProcessParameter(), { name: '--add' }));
expect(scriptService.invoke).toHaveBeenCalledWith(BATCH_IMPORT_SCRIPT_NAME, parameterValues, [fileMock]); expect(scriptService.invoke).toHaveBeenCalledWith(BATCH_IMPORT_SCRIPT_NAME, parameterValues, [fileMock]);
}); });
it('success notification is shown', () => { it('success notification is shown', () => {
@@ -121,8 +129,8 @@ describe('BatchImportPageComponent', () => {
})); }));
it('metadata-import script is invoked with --zip fileName and the mockFile and -v validate-only', () => { it('metadata-import script is invoked with --zip fileName and the mockFile and -v validate-only', () => {
const parameterValues: ProcessParameter[] = [ const parameterValues: ProcessParameter[] = [
Object.assign(new ProcessParameter(), { name: '--zip', value: 'filename.zip' }),
Object.assign(new ProcessParameter(), { name: '--add' }), Object.assign(new ProcessParameter(), { name: '--add' }),
Object.assign(new ProcessParameter(), { name: '--zip', value: 'filename.zip' }),
Object.assign(new ProcessParameter(), { name: '-v', value: true }), Object.assign(new ProcessParameter(), { name: '-v', value: true }),
]; ];
expect(scriptService.invoke).toHaveBeenCalledWith(BATCH_IMPORT_SCRIPT_NAME, parameterValues, [fileMock]); expect(scriptService.invoke).toHaveBeenCalledWith(BATCH_IMPORT_SCRIPT_NAME, parameterValues, [fileMock]);
@@ -148,4 +156,77 @@ describe('BatchImportPageComponent', () => {
}); });
}); });
}); });
describe('if url is set', () => {
beforeEach(fakeAsync(() => {
component.isUpload = false;
component.fileURL = 'example.fileURL.com';
fixture.detectChanges();
}));
it('should show the file url input', () => {
const fileDropzone = fixture.debugElement.query(By.css('[data-test="file-dropzone"]'));
const fileUrlInput = fixture.debugElement.query(By.css('[data-test="file-url-input"]'));
expect(fileDropzone).toBeFalsy();
expect(fileUrlInput).toBeTruthy();
});
describe('if proceed button is pressed without validate only', () => {
beforeEach(fakeAsync(() => {
component.validateOnly = false;
const proceed = fixture.debugElement.query(By.css('#proceedButton')).nativeElement;
proceed.click();
fixture.detectChanges();
}));
it('metadata-import script is invoked with --url and the file url', () => {
const parameterValues: ProcessParameter[] = [
Object.assign(new ProcessParameter(), { name: '--add' }),
Object.assign(new ProcessParameter(), { name: '--url', value: 'example.fileURL.com' })
];
expect(scriptService.invoke).toHaveBeenCalledWith(BATCH_IMPORT_SCRIPT_NAME, parameterValues, [null]);
});
it('success notification is shown', () => {
expect(notificationService.success).toHaveBeenCalled();
});
it('redirected to process page', () => {
expect(router.navigateByUrl).toHaveBeenCalledWith('/processes/46');
});
});
describe('if proceed button is pressed with validate only', () => {
beforeEach(fakeAsync(() => {
component.validateOnly = true;
const proceed = fixture.debugElement.query(By.css('#proceedButton')).nativeElement;
proceed.click();
fixture.detectChanges();
}));
it('metadata-import script is invoked with --url and the file url and -v validate-only', () => {
const parameterValues: ProcessParameter[] = [
Object.assign(new ProcessParameter(), { name: '--add' }),
Object.assign(new ProcessParameter(), { name: '--url', value: 'example.fileURL.com' }),
Object.assign(new ProcessParameter(), { name: '-v', value: true }),
];
expect(scriptService.invoke).toHaveBeenCalledWith(BATCH_IMPORT_SCRIPT_NAME, parameterValues, [null]);
});
it('success notification is shown', () => {
expect(notificationService.success).toHaveBeenCalled();
});
it('redirected to process page', () => {
expect(router.navigateByUrl).toHaveBeenCalledWith('/processes/46');
});
});
describe('if proceed is pressed; but script invoke fails', () => {
beforeEach(fakeAsync(() => {
jasmine.getEnv().allowRespy(true);
spyOn(scriptService, 'invoke').and.returnValue(createFailedRemoteDataObject$('Error', 500));
const proceed = fixture.debugElement.query(By.css('#proceedButton')).nativeElement;
proceed.click();
fixture.detectChanges();
}));
it('error notification is shown', () => {
expect(notificationService.error).toHaveBeenCalled();
});
});
});
}); });

View File

@@ -8,7 +8,7 @@ import { ProcessParameter } from '../../process-page/processes/process-parameter
import { getFirstCompletedRemoteData } from '../../core/shared/operators'; import { getFirstCompletedRemoteData } from '../../core/shared/operators';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { Process } from '../../process-page/processes/process.model'; import { Process } from '../../process-page/processes/process.model';
import { isNotEmpty } from '../../shared/empty.util'; import { isEmpty, isNotEmpty } from '../../shared/empty.util';
import { getProcessDetailRoute } from '../../process-page/process-page-routing.paths'; import { getProcessDetailRoute } from '../../process-page/process-page-routing.paths';
import { import {
ImportBatchSelectorComponent ImportBatchSelectorComponent
@@ -32,11 +32,22 @@ export class BatchImportPageComponent {
* The validate only flag * The validate only flag
*/ */
validateOnly = true; validateOnly = true;
/** /**
* dso object for community or collection * dso object for community or collection
*/ */
dso: DSpaceObject = null; dso: DSpaceObject = null;
/**
* The flag between upload and url
*/
isUpload = true;
/**
* File URL when flag is for url
*/
fileURL: string;
public constructor(private location: Location, public constructor(private location: Location,
protected translate: TranslateService, protected translate: TranslateService,
protected notificationsService: NotificationsService, protected notificationsService: NotificationsService,
@@ -72,13 +83,22 @@ export class BatchImportPageComponent {
* Starts import-metadata script with --zip fileName (and the selected file) * Starts import-metadata script with --zip fileName (and the selected file)
*/ */
public importMetadata() { public importMetadata() {
if (this.fileObject == null) { if (this.fileObject == null && isEmpty(this.fileURL)) {
this.notificationsService.error(this.translate.get('admin.metadata-import.page.error.addFile')); if (this.isUpload) {
this.notificationsService.error(this.translate.get('admin.metadata-import.page.error.addFile'));
} else {
this.notificationsService.error(this.translate.get('admin.metadata-import.page.error.addFileUrl'));
}
} else { } else {
const parameterValues: ProcessParameter[] = [ const parameterValues: ProcessParameter[] = [
Object.assign(new ProcessParameter(), { name: '--zip', value: this.fileObject.name }),
Object.assign(new ProcessParameter(), { name: '--add' }) Object.assign(new ProcessParameter(), { name: '--add' })
]; ];
if (this.isUpload) {
parameterValues.push(Object.assign(new ProcessParameter(), { name: '--zip', value: this.fileObject.name }));
} else {
this.fileObject = null;
parameterValues.push(Object.assign(new ProcessParameter(), { name: '--url', value: this.fileURL }));
}
if (this.dso) { if (this.dso) {
parameterValues.push(Object.assign(new ProcessParameter(), { name: '--collection', value: this.dso.uuid })); parameterValues.push(Object.assign(new ProcessParameter(), { name: '--collection', value: this.dso.uuid }));
} }
@@ -127,4 +147,11 @@ export class BatchImportPageComponent {
removeDspaceObject(): void { removeDspaceObject(): void {
this.dso = null; this.dso = null;
} }
/**
* toggle the flag between upload and url
*/
toggleUpload() {
this.isUpload = !this.isUpload;
}
} }

View File

@@ -0,0 +1,9 @@
import { URLCombiner } from '../../core/url-combiner/url-combiner';
import { getNotificationsModuleRoute } from '../admin-routing-paths';
export const QUALITY_ASSURANCE_EDIT_PATH = 'quality-assurance';
export const NOTIFICATIONS_RECITER_SUGGESTION_PATH = 'suggestion-targets';
export function getQualityAssuranceRoute(id: string) {
return new URLCombiner(getNotificationsModuleRoute(), QUALITY_ASSURANCE_EDIT_PATH, id).toString();
}

View File

@@ -0,0 +1,100 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { AuthenticatedGuard } from '../../core/auth/authenticated.guard';
import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver';
import { I18nBreadcrumbsService } from '../../core/breadcrumbs/i18n-breadcrumbs.service';
import { NOTIFICATIONS_RECITER_SUGGESTION_PATH } from './admin-notifications-routing-paths';
import { AdminNotificationsSuggestionTargetsPageComponent } from './admin-notifications-suggestion-targets-page/admin-notifications-suggestion-targets-page.component';
import { AdminNotificationsSuggestionTargetsPageResolver } from './admin-notifications-suggestion-targets-page/admin-notifications-suggestion-targets-page-resolver.service';
import { QUALITY_ASSURANCE_EDIT_PATH } from './admin-notifications-routing-paths';
import { AdminQualityAssuranceTopicsPageComponent } from './admin-quality-assurance-topics-page/admin-quality-assurance-topics-page.component';
import { AdminQualityAssuranceEventsPageComponent } from './admin-quality-assurance-events-page/admin-quality-assurance-events-page.component';
import { AdminQualityAssuranceTopicsPageResolver } from './admin-quality-assurance-topics-page/admin-quality-assurance-topics-page-resolver.service';
import { AdminQualityAssuranceEventsPageResolver } from './admin-quality-assurance-events-page/admin-quality-assurance-events-page.resolver';
import { AdminQualityAssuranceSourcePageComponent } from './admin-quality-assurance-source-page-component/admin-quality-assurance-source-page.component';
import { AdminQualityAssuranceSourcePageResolver } from './admin-quality-assurance-source-page-component/admin-quality-assurance-source-page-resolver.service';
import { SourceDataResolver } from './admin-quality-assurance-source-page-component/admin-quality-assurance-source-data.reslover';
@NgModule({
imports: [
RouterModule.forChild([
{
canActivate: [ AuthenticatedGuard ],
path: `${NOTIFICATIONS_RECITER_SUGGESTION_PATH}`,
component: AdminNotificationsSuggestionTargetsPageComponent,
pathMatch: 'full',
resolve: {
breadcrumb: I18nBreadcrumbResolver,
reciterSuggestionTargetParams: AdminNotificationsSuggestionTargetsPageResolver
},
data: {
title: 'admin.notifications.recitersuggestion.page.title',
breadcrumbKey: 'admin.notifications.recitersuggestion',
showBreadcrumbsFluid: false
}
},
{
canActivate: [ AuthenticatedGuard ],
path: `${QUALITY_ASSURANCE_EDIT_PATH}/:sourceId`,
component: AdminQualityAssuranceTopicsPageComponent,
pathMatch: 'full',
resolve: {
breadcrumb: I18nBreadcrumbResolver,
openaireQualityAssuranceTopicsParams: AdminQualityAssuranceTopicsPageResolver
},
data: {
title: 'admin.quality-assurance.page.title',
breadcrumbKey: 'admin.quality-assurance',
showBreadcrumbsFluid: false
}
},
{
canActivate: [ AuthenticatedGuard ],
path: `${QUALITY_ASSURANCE_EDIT_PATH}`,
component: AdminQualityAssuranceSourcePageComponent,
pathMatch: 'full',
resolve: {
breadcrumb: I18nBreadcrumbResolver,
openaireQualityAssuranceSourceParams: AdminQualityAssuranceSourcePageResolver,
sourceData: SourceDataResolver
},
data: {
title: 'admin.notifications.source.breadcrumbs',
breadcrumbKey: 'admin.notifications.source',
showBreadcrumbsFluid: false
}
},
{
canActivate: [ AuthenticatedGuard ],
path: `${QUALITY_ASSURANCE_EDIT_PATH}/:sourceId/:topicId`,
component: AdminQualityAssuranceEventsPageComponent,
pathMatch: 'full',
resolve: {
breadcrumb: I18nBreadcrumbResolver,
openaireQualityAssuranceEventsParams: AdminQualityAssuranceEventsPageResolver
},
data: {
title: 'admin.notifications.event.page.title',
breadcrumbKey: 'admin.notifications.event',
showBreadcrumbsFluid: false
}
}
])
],
providers: [
I18nBreadcrumbResolver,
I18nBreadcrumbsService,
AdminNotificationsSuggestionTargetsPageResolver,
SourceDataResolver,
AdminQualityAssuranceSourcePageResolver,
AdminQualityAssuranceTopicsPageResolver,
AdminQualityAssuranceEventsPageResolver,
]
})
/**
* Routing module for the Notifications section of the admin sidebar
*/
export class AdminNotificationsRoutingModule {
}

View File

@@ -0,0 +1,32 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
/**
* Interface for the route parameters.
*/
export interface AdminNotificationsSuggestionTargetsPageParams {
pageId?: string;
pageSize?: number;
currentPage?: number;
}
/**
* This class represents a resolver that retrieve the route data before the route is activated.
*/
@Injectable()
export class AdminNotificationsSuggestionTargetsPageResolver implements Resolve<AdminNotificationsSuggestionTargetsPageParams> {
/**
* Method for resolving the parameters in the current route.
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
* @returns AdminNotificationsSuggestionTargetsPageParams Emits the route parameters
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): AdminNotificationsSuggestionTargetsPageParams {
return {
pageId: route.queryParams.pageId,
pageSize: parseInt(route.queryParams.pageSize, 10),
currentPage: parseInt(route.queryParams.page, 10)
};
}
}

View File

@@ -0,0 +1 @@
<ds-suggestion-target [source]="'oaire'"></ds-suggestion-target>

View File

@@ -0,0 +1,38 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AdminNotificationsSuggestionTargetsPageComponent } from './admin-notifications-suggestion-targets-page.component';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
describe('AdminNotificationsSuggestionTargetsPageComponent', () => {
let component: AdminNotificationsSuggestionTargetsPageComponent;
let fixture: ComponentFixture<AdminNotificationsSuggestionTargetsPageComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
CommonModule,
TranslateModule.forRoot()
],
declarations: [
AdminNotificationsSuggestionTargetsPageComponent
],
providers: [
AdminNotificationsSuggestionTargetsPageComponent
],
schemas: [NO_ERRORS_SCHEMA]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AdminNotificationsSuggestionTargetsPageComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,10 @@
import { Component } from '@angular/core';
@Component({
selector: 'ds-admin-notifications-reciter-page',
templateUrl: './admin-notifications-suggestion-targets-page.component.html',
styleUrls: ['./admin-notifications-suggestion-targets-page.component.scss']
})
export class AdminNotificationsSuggestionTargetsPageComponent {
}

View File

@@ -0,0 +1,33 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { CoreModule } from '../../core/core.module';
import { SharedModule } from '../../shared/shared.module';
import { AdminNotificationsRoutingModule } from './admin-notifications-routing.module';
import { AdminNotificationsSuggestionTargetsPageComponent } from './admin-notifications-suggestion-targets-page/admin-notifications-suggestion-targets-page.component';
import { AdminQualityAssuranceTopicsPageComponent } from './admin-quality-assurance-topics-page/admin-quality-assurance-topics-page.component';
import { AdminQualityAssuranceEventsPageComponent } from './admin-quality-assurance-events-page/admin-quality-assurance-events-page.component';
import { AdminQualityAssuranceSourcePageComponent } from './admin-quality-assurance-source-page-component/admin-quality-assurance-source-page.component';
import {SuggestionNotificationsModule} from '../../suggestion-notifications/suggestion-notifications.module';
@NgModule({
imports: [
CommonModule,
SharedModule,
CoreModule.forRoot(),
AdminNotificationsRoutingModule,
SuggestionNotificationsModule
],
declarations: [
AdminNotificationsSuggestionTargetsPageComponent,
AdminQualityAssuranceTopicsPageComponent,
AdminQualityAssuranceEventsPageComponent,
AdminQualityAssuranceSourcePageComponent
],
entryComponents: []
})
/**
* This module handles all components related to the notifications pages
*/
export class AdminNotificationsModule {
}

View File

@@ -0,0 +1 @@
<ds-quality-assurance-events></ds-quality-assurance-events>

View File

@@ -0,0 +1,26 @@
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AdminQualityAssuranceEventsPageComponent } from './admin-quality-assurance-events-page.component';
describe('AdminQualityAssuranceEventsPageComponent', () => {
let component: AdminQualityAssuranceEventsPageComponent;
let fixture: ComponentFixture<AdminQualityAssuranceEventsPageComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ AdminQualityAssuranceEventsPageComponent ],
schemas: [NO_ERRORS_SCHEMA]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AdminQualityAssuranceEventsPageComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create AdminQualityAssuranceEventsPageComponent', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,12 @@
import { Component } from '@angular/core';
/**
* Component for the page that show the QA events related to a specific topic.
*/
@Component({
selector: 'ds-quality-assurance-events-page',
templateUrl: './admin-quality-assurance-events-page.component.html'
})
export class AdminQualityAssuranceEventsPageComponent {
}

View File

@@ -0,0 +1,32 @@
import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
/**
* Interface for the route parameters.
*/
export interface AdminQualityAssuranceEventsPageParams {
pageId?: string;
pageSize?: number;
currentPage?: number;
}
/**
* This class represents a resolver that retrieve the route data before the route is activated.
*/
@Injectable()
export class AdminQualityAssuranceEventsPageResolver implements Resolve<AdminQualityAssuranceEventsPageParams> {
/**
* Method for resolving the parameters in the current route.
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
* @returns AdminQualityAssuranceEventsPageParams Emits the route parameters
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): AdminQualityAssuranceEventsPageParams {
return {
pageId: route.queryParams.pageId,
pageSize: parseInt(route.queryParams.pageSize, 10),
currentPage: parseInt(route.queryParams.page, 10)
};
}
}

View File

@@ -0,0 +1,45 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot, Router } from '@angular/router';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { PaginatedList } from '../../../core/data/paginated-list.model';
import { QualityAssuranceSourceObject } from '../../../core/suggestion-notifications/qa/models/quality-assurance-source.model';
import { QualityAssuranceSourceService } from '../../../suggestion-notifications/qa/source/quality-assurance-source.service';
/**
* This class represents a resolver that retrieve the route data before the route is activated.
*/
@Injectable()
export class SourceDataResolver implements Resolve<Observable<QualityAssuranceSourceObject[]>> {
/**
* Initialize the effect class variables.
* @param {QualityAssuranceSourceService} qualityAssuranceSourceService
*/
constructor(
private qualityAssuranceSourceService: QualityAssuranceSourceService,
private router: Router
) { }
/**
* Method for resolving the parameters in the current route.
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
* @returns Observable<QualityAssuranceSourceObject[]>
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<QualityAssuranceSourceObject[]> {
return this.qualityAssuranceSourceService.getSources(5,0).pipe(
map((sources: PaginatedList<QualityAssuranceSourceObject>) => {
if (sources.page.length === 1) {
this.router.navigate([this.getResolvedUrl(route) + '/' + sources.page[0].id]);
}
return sources.page;
}));
}
/**
*
* @param route url path
* @returns url path
*/
getResolvedUrl(route: ActivatedRouteSnapshot): string {
return route.pathFromRoot.map(v => v.url.map(segment => segment.toString()).join('/')).join('/');
}
}

View File

@@ -0,0 +1,32 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
/**
* Interface for the route parameters.
*/
export interface AdminQualityAssuranceSourcePageParams {
pageId?: string;
pageSize?: number;
currentPage?: number;
}
/**
* This class represents a resolver that retrieve the route data before the route is activated.
*/
@Injectable()
export class AdminQualityAssuranceSourcePageResolver implements Resolve<AdminQualityAssuranceSourcePageParams> {
/**
* Method for resolving the parameters in the current route.
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
* @returns AdminQualityAssuranceSourcePageParams Emits the route parameters
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): AdminQualityAssuranceSourcePageParams {
return {
pageId: route.queryParams.pageId,
pageSize: parseInt(route.queryParams.pageSize, 10),
currentPage: parseInt(route.queryParams.page, 10)
};
}
}

View File

@@ -0,0 +1 @@
<ds-quality-assurance-source></ds-quality-assurance-source>

View File

@@ -0,0 +1,27 @@
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AdminQualityAssuranceSourcePageComponent } from './admin-quality-assurance-source-page.component';
describe('AdminQualityAssuranceSourcePageComponent', () => {
let component: AdminQualityAssuranceSourcePageComponent;
let fixture: ComponentFixture<AdminQualityAssuranceSourcePageComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ AdminQualityAssuranceSourcePageComponent ],
schemas: [NO_ERRORS_SCHEMA]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(AdminQualityAssuranceSourcePageComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create AdminQualityAssuranceSourcePageComponent', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,10 @@
import { Component } from '@angular/core';
/**
* Component for the page that show the QA sources.
*/
@Component({
selector: 'ds-admin-quality-assurance-source-page-component',
templateUrl: './admin-quality-assurance-source-page.component.html',
})
export class AdminQualityAssuranceSourcePageComponent {}

View File

@@ -0,0 +1,32 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
/**
* Interface for the route parameters.
*/
export interface AdminQualityAssuranceTopicsPageParams {
pageId?: string;
pageSize?: number;
currentPage?: number;
}
/**
* This class represents a resolver that retrieve the route data before the route is activated.
*/
@Injectable()
export class AdminQualityAssuranceTopicsPageResolver implements Resolve<AdminQualityAssuranceTopicsPageParams> {
/**
* Method for resolving the parameters in the current route.
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
* @returns AdminQualityAssuranceTopicsPageParams Emits the route parameters
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): AdminQualityAssuranceTopicsPageParams {
return {
pageId: route.queryParams.pageId,
pageSize: parseInt(route.queryParams.pageSize, 10),
currentPage: parseInt(route.queryParams.page, 10)
};
}
}

View File

@@ -0,0 +1 @@
<ds-quality-assurance-topic></ds-quality-assurance-topic>

View File

@@ -0,0 +1,26 @@
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AdminQualityAssuranceTopicsPageComponent } from './admin-quality-assurance-topics-page.component';
describe('AdminQualityAssuranceTopicsPageComponent', () => {
let component: AdminQualityAssuranceTopicsPageComponent;
let fixture: ComponentFixture<AdminQualityAssuranceTopicsPageComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ AdminQualityAssuranceTopicsPageComponent ],
schemas: [NO_ERRORS_SCHEMA]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AdminQualityAssuranceTopicsPageComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create AdminQualityAssuranceTopicsPageComponent', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,12 @@
import { Component } from '@angular/core';
/**
* Component for the page that show the QA topics related to a specific source.
*/
@Component({
selector: 'ds-notification-qa-page',
templateUrl: './admin-quality-assurance-topics-page.component.html'
})
export class AdminQualityAssuranceTopicsPageComponent {
}

View File

@@ -1,5 +1,4 @@
import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing'; import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing';
import { MetadataSchemaFormComponent } from './metadata-schema-form.component'; import { MetadataSchemaFormComponent } from './metadata-schema-form.component';
import { NO_ERRORS_SCHEMA } from '@angular/core'; import { NO_ERRORS_SCHEMA } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
@@ -29,14 +28,16 @@ describe('MetadataSchemaFormComponent', () => {
createFormGroup: () => { createFormGroup: () => {
return { return {
patchValue: () => { patchValue: () => {
} },
reset(_value?: any, _options?: { onlySelf?: boolean; emitEvent?: boolean; }): void {
},
}; };
} }
}; };
/* eslint-enable no-empty, @typescript-eslint/no-empty-function */ /* eslint-enable no-empty, @typescript-eslint/no-empty-function */
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ return TestBed.configureTestingModule({
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule], imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
declarations: [MetadataSchemaFormComponent, EnumKeysPipe], declarations: [MetadataSchemaFormComponent, EnumKeysPipe],
providers: [ providers: [
@@ -64,7 +65,7 @@ describe('MetadataSchemaFormComponent', () => {
const expected = Object.assign(new MetadataSchema(), { const expected = Object.assign(new MetadataSchema(), {
namespace: namespace, namespace: namespace,
prefix: prefix prefix: prefix
}); } as MetadataSchema);
beforeEach(() => { beforeEach(() => {
spyOn(component.submitForm, 'emit'); spyOn(component.submitForm, 'emit');
@@ -79,11 +80,10 @@ describe('MetadataSchemaFormComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should emit a new schema using the correct values', waitForAsync(() => { it('should emit a new schema using the correct values', async () => {
fixture.whenStable().then(() => { await fixture.whenStable();
expect(component.submitForm.emit).toHaveBeenCalledWith(expected); expect(component.submitForm.emit).toHaveBeenCalledWith(expected);
}); });
}));
}); });
describe('with an active schema', () => { describe('with an active schema', () => {
@@ -91,7 +91,7 @@ describe('MetadataSchemaFormComponent', () => {
id: 1, id: 1,
namespace: namespace, namespace: namespace,
prefix: prefix prefix: prefix
}); } as MetadataSchema);
beforeEach(() => { beforeEach(() => {
spyOn(registryService, 'getActiveMetadataSchema').and.returnValue(observableOf(expectedWithId)); spyOn(registryService, 'getActiveMetadataSchema').and.returnValue(observableOf(expectedWithId));
@@ -99,11 +99,10 @@ describe('MetadataSchemaFormComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should edit the existing schema using the correct values', waitForAsync(() => { it('should edit the existing schema using the correct values', async () => {
fixture.whenStable().then(() => { await fixture.whenStable();
expect(component.submitForm.emit).toHaveBeenCalledWith(expectedWithId); expect(component.submitForm.emit).toHaveBeenCalledWith(expectedWithId);
}); });
}));
}); });
}); });
}); });

View File

@@ -5,7 +5,7 @@ import {
DynamicFormLayout, DynamicFormLayout,
DynamicInputModel DynamicInputModel
} from '@ng-dynamic-forms/core'; } from '@ng-dynamic-forms/core';
import { FormGroup } from '@angular/forms'; import { UntypedFormGroup } from '@angular/forms';
import { RegistryService } from '../../../../core/registry/registry.service'; 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 { take } from 'rxjs/operators'; import { take } from 'rxjs/operators';
@@ -66,7 +66,7 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
/** /**
* A FormGroup that combines all inputs * A FormGroup that combines all inputs
*/ */
formGroup: FormGroup; formGroup: UntypedFormGroup;
/** /**
* An EventEmitter that's fired whenever the form is being submitted * An EventEmitter that's fired whenever the form is being submitted
@@ -77,19 +77,24 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
} }
ngOnInit() { ngOnInit() {
combineLatest( combineLatest([
this.translateService.get(`${this.messagePrefix}.name`), this.translateService.get(`${this.messagePrefix}.name`),
this.translateService.get(`${this.messagePrefix}.namespace`) this.translateService.get(`${this.messagePrefix}.namespace`)
).subscribe(([name, namespace]) => { ]).subscribe(([name, namespace]) => {
this.name = new DynamicInputModel({ this.name = new DynamicInputModel({
id: 'name', id: 'name',
label: name, label: name,
name: 'name', name: 'name',
validators: { validators: {
required: null, required: null,
pattern: '^[^ ,_]{1,32}$' pattern: '^[^. ,]*$',
maxLength: 32,
}, },
required: true, required: true,
errorMessages: {
pattern: 'error.validation.metadata.name.invalid-pattern',
maxLength: 'error.validation.metadata.name.max-length',
},
}); });
this.namespace = new DynamicInputModel({ this.namespace = new DynamicInputModel({
id: 'namespace', id: 'namespace',
@@ -97,8 +102,12 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
name: 'namespace', name: 'namespace',
validators: { validators: {
required: null, required: null,
maxLength: 256,
}, },
required: true, required: true,
errorMessages: {
maxLength: 'error.validation.metadata.namespace.max-length',
},
}); });
this.formModel = [ this.formModel = [
new DynamicFormGroupModel( new DynamicFormGroupModel(
@@ -108,13 +117,18 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
}) })
]; ];
this.formGroup = this.formBuilderService.createFormGroup(this.formModel); this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
this.registryService.getActiveMetadataSchema().subscribe((schema) => { this.registryService.getActiveMetadataSchema().subscribe((schema: MetadataSchema) => {
this.formGroup.patchValue({ if (schema == null) {
metadatadataschemagroup:{ this.clearFields();
name: schema != null ? schema.prefix : '', } else {
namespace: schema != null ? schema.namespace : '' this.formGroup.patchValue({
} metadatadataschemagroup: {
}); name: schema.prefix,
namespace: schema.namespace,
},
});
this.name.disabled = true;
}
}); });
}); });
} }
@@ -132,10 +146,10 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
* When the schema has no id attached -> Create new schema * When the schema has no id attached -> Create new schema
* Emit the updated/created schema using the EventEmitter submitForm * Emit the updated/created schema using the EventEmitter submitForm
*/ */
onSubmit() { onSubmit(): void {
this.registryService.clearMetadataSchemaRequests().subscribe(); this.registryService.clearMetadataSchemaRequests().subscribe();
this.registryService.getActiveMetadataSchema().pipe(take(1)).subscribe( this.registryService.getActiveMetadataSchema().pipe(take(1)).subscribe(
(schema) => { (schema: MetadataSchema) => {
const values = { const values = {
prefix: this.name.value, prefix: this.name.value,
namespace: this.namespace.value namespace: this.namespace.value
@@ -147,9 +161,9 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
} else { } else {
this.registryService.createOrUpdateMetadataSchema(Object.assign(new MetadataSchema(), schema, { this.registryService.createOrUpdateMetadataSchema(Object.assign(new MetadataSchema(), schema, {
id: schema.id, id: schema.id,
prefix: (values.prefix ? values.prefix : schema.prefix), prefix: schema.prefix,
namespace: (values.namespace ? values.namespace : schema.namespace) namespace: values.namespace,
})).subscribe((updatedSchema) => { })).subscribe((updatedSchema: MetadataSchema) => {
this.submitForm.emit(updatedSchema); this.submitForm.emit(updatedSchema);
}); });
} }
@@ -162,13 +176,9 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
/** /**
* Reset all input-fields to be empty * Reset all input-fields to be empty
*/ */
clearFields() { clearFields(): void {
this.formGroup.patchValue({ this.formGroup.reset('metadatadataschemagroup');
metadatadataschemagroup:{ this.name.disabled = false;
prefix: '',
namespace: ''
}
});
} }
/** /**

View File

@@ -39,14 +39,16 @@ describe('MetadataFieldFormComponent', () => {
createFormGroup: () => { createFormGroup: () => {
return { return {
patchValue: () => { patchValue: () => {
} },
reset(_value?: any, _options?: { onlySelf?: boolean; emitEvent?: boolean; }): void {
},
}; };
} }
}; };
/* eslint-enable no-empty, @typescript-eslint/no-empty-function */ /* eslint-enable no-empty, @typescript-eslint/no-empty-function */
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ return TestBed.configureTestingModule({
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule], imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
declarations: [MetadataFieldFormComponent, EnumKeysPipe], declarations: [MetadataFieldFormComponent, EnumKeysPipe],
providers: [ providers: [
@@ -98,11 +100,10 @@ describe('MetadataFieldFormComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should emit a new field using the correct values', waitForAsync(() => { it('should emit a new field using the correct values', async () => {
fixture.whenStable().then(() => { await fixture.whenStable();
expect(component.submitForm.emit).toHaveBeenCalledWith(expected); expect(component.submitForm.emit).toHaveBeenCalledWith(expected);
}); });
}));
}); });
describe('with an active field', () => { describe('with an active field', () => {
@@ -120,11 +121,10 @@ describe('MetadataFieldFormComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should edit the existing field using the correct values', waitForAsync(() => { it('should edit the existing field using the correct values', async () => {
fixture.whenStable().then(() => { await fixture.whenStable();
expect(component.submitForm.emit).toHaveBeenCalledWith(expectedWithId); expect(component.submitForm.emit).toHaveBeenCalledWith(expectedWithId);
}); });
}));
}); });
}); });
}); });

View File

@@ -5,7 +5,7 @@ import {
DynamicFormLayout, DynamicFormLayout,
DynamicInputModel DynamicInputModel
} from '@ng-dynamic-forms/core'; } from '@ng-dynamic-forms/core';
import { FormGroup } from '@angular/forms'; import { UntypedFormGroup } from '@angular/forms';
import { RegistryService } from '../../../../core/registry/registry.service'; 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 { take } from 'rxjs/operators'; import { take } from 'rxjs/operators';
@@ -82,7 +82,7 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy {
/** /**
* A FormGroup that combines all inputs * A FormGroup that combines all inputs
*/ */
formGroup: FormGroup; formGroup: UntypedFormGroup;
/** /**
* An EventEmitter that's fired whenever the form is being submitted * An EventEmitter that's fired whenever the form is being submitted
@@ -98,25 +98,39 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy {
* Initialize the component, setting up the necessary Models for the dynamic form * Initialize the component, setting up the necessary Models for the dynamic form
*/ */
ngOnInit() { ngOnInit() {
combineLatest( combineLatest([
this.translateService.get(`${this.messagePrefix}.element`), this.translateService.get(`${this.messagePrefix}.element`),
this.translateService.get(`${this.messagePrefix}.qualifier`), this.translateService.get(`${this.messagePrefix}.qualifier`),
this.translateService.get(`${this.messagePrefix}.scopenote`) this.translateService.get(`${this.messagePrefix}.scopenote`)
).subscribe(([element, qualifier, scopenote]) => { ]).subscribe(([element, qualifier, scopenote]) => {
this.element = new DynamicInputModel({ this.element = new DynamicInputModel({
id: 'element', id: 'element',
label: element, label: element,
name: 'element', name: 'element',
validators: { validators: {
required: null, required: null,
pattern: '^[^. ,]*$',
maxLength: 64,
}, },
required: true, required: true,
errorMessages: {
pattern: 'error.validation.metadata.element.invalid-pattern',
maxLength: 'error.validation.metadata.element.max-length',
},
}); });
this.qualifier = new DynamicInputModel({ this.qualifier = new DynamicInputModel({
id: 'qualifier', id: 'qualifier',
label: qualifier, label: qualifier,
name: 'qualifier', name: 'qualifier',
validators: {
pattern: '^[^. ,]*$',
maxLength: 64,
},
required: false, required: false,
errorMessages: {
pattern: 'error.validation.metadata.qualifier.invalid-pattern',
maxLength: 'error.validation.metadata.qualifier.max-length',
},
}); });
this.scopeNote = new DynamicInputModel({ this.scopeNote = new DynamicInputModel({
id: 'scopeNote', id: 'scopeNote',
@@ -132,14 +146,20 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy {
}) })
]; ];
this.formGroup = this.formBuilderService.createFormGroup(this.formModel); this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
this.registryService.getActiveMetadataField().subscribe((field) => { this.registryService.getActiveMetadataField().subscribe((field: MetadataField): void => {
this.formGroup.patchValue({ if (field == null) {
metadatadatafieldgroup: { this.clearFields();
element: field != null ? field.element : '', } else {
qualifier: field != null ? field.qualifier : '', this.formGroup.patchValue({
scopeNote: field != null ? field.scopeNote : '' metadatadatafieldgroup: {
} element: field.element,
}); qualifier: field.qualifier,
scopeNote: field.scopeNote,
},
});
this.element.disabled = true;
this.qualifier.disabled = true;
}
}); });
}); });
} }
@@ -157,25 +177,24 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy {
* When the field has no id attached -> Create new field * When the field has no id attached -> Create new field
* Emit the updated/created field using the EventEmitter submitForm * Emit the updated/created field using the EventEmitter submitForm
*/ */
onSubmit() { onSubmit(): void {
this.registryService.getActiveMetadataField().pipe(take(1)).subscribe( this.registryService.getActiveMetadataField().pipe(take(1)).subscribe(
(field) => { (field: MetadataField) => {
const values = {
element: this.element.value,
qualifier: this.qualifier.value,
scopeNote: this.scopeNote.value
};
if (field == null) { if (field == null) {
this.registryService.createMetadataField(Object.assign(new MetadataField(), values), this.metadataSchema).subscribe((newField) => { this.registryService.createMetadataField(Object.assign(new MetadataField(), {
element: this.element.value,
qualifier: this.qualifier.value,
scopeNote: this.scopeNote.value,
}), this.metadataSchema).subscribe((newField: MetadataField) => {
this.submitForm.emit(newField); this.submitForm.emit(newField);
}); });
} else { } else {
this.registryService.updateMetadataField(Object.assign(new MetadataField(), field, { this.registryService.updateMetadataField(Object.assign(new MetadataField(), field, {
id: field.id, id: field.id,
element: (values.element ? values.element : field.element), element: field.element,
qualifier: (values.qualifier ? values.qualifier : field.qualifier), qualifier: field.qualifier,
scopeNote: (values.scopeNote ? values.scopeNote : field.scopeNote) scopeNote: this.scopeNote.value,
})).subscribe((updatedField) => { })).subscribe((updatedField: MetadataField) => {
this.submitForm.emit(updatedField); this.submitForm.emit(updatedField);
}); });
} }
@@ -188,14 +207,10 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy {
/** /**
* Reset all input-fields to be empty * Reset all input-fields to be empty
*/ */
clearFields() { clearFields(): void {
this.formGroup.patchValue({ this.formGroup.reset('metadatadatafieldgroup');
metadatadatafieldgroup: { this.element.disabled = false;
element: '', this.qualifier.disabled = false;
qualifier: '',
scopeNote: ''
}
});
} }
/** /**

View File

@@ -2,7 +2,12 @@ import { URLCombiner } from '../core/url-combiner/url-combiner';
import { getAdminModuleRoute } from '../app-routing-paths'; import { getAdminModuleRoute } from '../app-routing-paths';
export const REGISTRIES_MODULE_PATH = 'registries'; export const REGISTRIES_MODULE_PATH = 'registries';
export const NOTIFICATIONS_MODULE_PATH = 'notifications';
export function getRegistriesModuleRoute() { export function getRegistriesModuleRoute() {
return new URLCombiner(getAdminModuleRoute(), REGISTRIES_MODULE_PATH).toString(); return new URLCombiner(getAdminModuleRoute(), REGISTRIES_MODULE_PATH).toString();
} }
export function getNotificationsModuleRoute() {
return new URLCombiner(getAdminModuleRoute(), NOTIFICATIONS_MODULE_PATH).toString();
}

View File

@@ -6,12 +6,17 @@ import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.reso
import { AdminWorkflowPageComponent } from './admin-workflow-page/admin-workflow-page.component'; import { AdminWorkflowPageComponent } from './admin-workflow-page/admin-workflow-page.component';
import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service'; import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service';
import { AdminCurationTasksComponent } from './admin-curation-tasks/admin-curation-tasks.component'; import { AdminCurationTasksComponent } from './admin-curation-tasks/admin-curation-tasks.component';
import { REGISTRIES_MODULE_PATH } from './admin-routing-paths'; import { REGISTRIES_MODULE_PATH, NOTIFICATIONS_MODULE_PATH } from './admin-routing-paths';
import { BatchImportPageComponent } from './admin-import-batch-page/batch-import-page.component'; import { BatchImportPageComponent } from './admin-import-batch-page/batch-import-page.component';
@NgModule({ @NgModule({
imports: [ imports: [
RouterModule.forChild([ RouterModule.forChild([
{
path: NOTIFICATIONS_MODULE_PATH,
loadChildren: () => import('./admin-notifications/admin-notifications.module')
.then((m) => m.AdminNotificationsModule),
},
{ {
path: REGISTRIES_MODULE_PATH, path: REGISTRIES_MODULE_PATH,
loadChildren: () => import('./admin-registries/admin-registries.module') loadChildren: () => import('./admin-registries/admin-registries.module')

View File

@@ -19,7 +19,7 @@ import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-
import { getMockThemeService } from '../../../../../shared/mocks/theme-service.mock'; import { getMockThemeService } from '../../../../../shared/mocks/theme-service.mock';
import { ThemeService } from '../../../../../shared/theme-support/theme.service'; import { ThemeService } from '../../../../../shared/theme-support/theme.service';
import { AccessStatusDataService } from '../../../../../core/data/access-status-data.service'; import { AccessStatusDataService } from '../../../../../core/data/access-status-data.service';
import { AccessStatusObject } from '../../../../../shared/object-list/access-status-badge/access-status.model'; import { AccessStatusObject } from '../../../../../shared/object-collection/shared/badges/access-status-badge/access-status.model';
import { AuthService } from '../../../../../core/auth/auth.service'; import { AuthService } from '../../../../../core/auth/auth.service';
import { AuthServiceStub } from '../../../../../shared/testing/auth-service.stub'; import { AuthServiceStub } from '../../../../../shared/testing/auth-service.stub';
import { FileService } from '../../../../../core/shared/file.service'; import { FileService } from '../../../../../core/shared/file.service';

View File

@@ -13,6 +13,7 @@ import { BitstreamDataService } from '../../../../../core/data/bitstream-data.se
import { GenericConstructor } from '../../../../../core/shared/generic-constructor'; import { GenericConstructor } from '../../../../../core/shared/generic-constructor';
import { ListableObjectDirective } from '../../../../../shared/object-collection/shared/listable-object/listable-object.directive'; import { ListableObjectDirective } from '../../../../../shared/object-collection/shared/listable-object/listable-object.directive';
import { ThemeService } from '../../../../../shared/theme-support/theme.service'; import { ThemeService } from '../../../../../shared/theme-support/theme.service';
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
@listableObjectComponent(ItemSearchResult, ViewMode.GridElement, Context.AdminSearch) @listableObjectComponent(ItemSearchResult, ViewMode.GridElement, Context.AdminSearch)
@Component({ @Component({
@@ -28,12 +29,14 @@ export class ItemAdminSearchResultGridElementComponent extends SearchResultGridE
@ViewChild('badges', { static: true }) badges: ElementRef; @ViewChild('badges', { static: true }) badges: ElementRef;
@ViewChild('buttons', { static: true }) buttons: ElementRef; @ViewChild('buttons', { static: true }) buttons: ElementRef;
constructor(protected truncatableService: TruncatableService, constructor(
protected bitstreamDataService: BitstreamDataService, public dsoNameService: DSONameService,
private themeService: ThemeService, protected truncatableService: TruncatableService,
private componentFactoryResolver: ComponentFactoryResolver protected bitstreamDataService: BitstreamDataService,
private themeService: ThemeService,
private componentFactoryResolver: ComponentFactoryResolver,
) { ) {
super(truncatableService, bitstreamDataService); super(dsoNameService, truncatableService, bitstreamDataService);
} }
/** /**

View File

@@ -2,6 +2,5 @@
[viewMode]="viewModes.ListElement" [viewMode]="viewModes.ListElement"
[index]="index" [index]="index"
[linkType]="linkType" [linkType]="linkType"
[listID]="listID" [listID]="listID"></ds-listable-object-component-loader>
[hideBadges]="true"></ds-listable-object-component-loader>
<ds-item-admin-search-result-actions-element [item]="dso" [small]="false"></ds-item-admin-search-result-actions-element> <ds-item-admin-search-result-actions-element [item]="dso" [small]="false"></ds-item-admin-search-result-actions-element>

View File

@@ -82,7 +82,7 @@ export class SupervisionOrderGroupSelectorComponent {
getFirstCompletedRemoteData(), getFirstCompletedRemoteData(),
).subscribe((rd: RemoteData<SupervisionOrder>) => { ).subscribe((rd: RemoteData<SupervisionOrder>) => {
if (rd.state === 'Success') { if (rd.state === 'Success') {
this.notificationsService.success(this.translateService.get('supervision-group-selector.notification.create.success.title', { name: this.selectedGroup.name })); this.notificationsService.success(this.translateService.get('supervision-group-selector.notification.create.success.title', { name: this.dsoNameService.getName(this.selectedGroup) }));
this.create.emit(rd.payload); this.create.emit(rd.payload);
this.close(); this.close();
} else { } else {

View File

@@ -7,7 +7,7 @@
<a class="badge badge-primary mr-1 mb-1 text-capitalize mw-100 text-truncate" *ngFor="let supervisionOrder of supervisionOrders" data-test="soBadge" <a class="badge badge-primary mr-1 mb-1 text-capitalize mw-100 text-truncate" *ngFor="let supervisionOrder of supervisionOrders" data-test="soBadge"
[ngbTooltip]="'workflow-item.search.result.list.element.supervised.remove-tooltip' | translate" [ngbTooltip]="'workflow-item.search.result.list.element.supervised.remove-tooltip' | translate"
(click)="$event.preventDefault(); $event.stopImmediatePropagation(); deleteSupervisionOrder(supervisionOrder)" aria-label="Close"> (click)="$event.preventDefault(); $event.stopImmediatePropagation(); deleteSupervisionOrder(supervisionOrder)" aria-label="Close">
{{supervisionOrder.group.name}} {{ dsoNameService.getName(supervisionOrder.group) }}
<span aria-hidden="true"> ×</span> <span aria-hidden="true"> ×</span>
</a> </a>
</div> </div>

View File

@@ -8,6 +8,7 @@ import { Group } from '../../../../../../core/eperson/models/group.model';
import { getFirstCompletedRemoteData } from '../../../../../../core/shared/operators'; import { getFirstCompletedRemoteData } from '../../../../../../core/shared/operators';
import { isNotEmpty } from '../../../../../../shared/empty.util'; import { isNotEmpty } from '../../../../../../shared/empty.util';
import { RemoteData } from '../../../../../../core/data/remote-data'; import { RemoteData } from '../../../../../../core/data/remote-data';
import { DSONameService } from '../../../../../../core/breadcrumbs/dso-name.service';
export interface SupervisionOrderListEntry { export interface SupervisionOrderListEntry {
supervisionOrder: SupervisionOrder; supervisionOrder: SupervisionOrder;
@@ -33,6 +34,11 @@ export class SupervisionOrderStatusComponent implements OnChanges {
@Output() delete: EventEmitter<SupervisionOrderListEntry> = new EventEmitter<SupervisionOrderListEntry>(); @Output() delete: EventEmitter<SupervisionOrderListEntry> = new EventEmitter<SupervisionOrderListEntry>();
constructor(
public dsoNameService: DSONameService,
) {
}
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
if (changes && changes.supervisionOrderList) { if (changes && changes.supervisionOrderList) {
this.getSupervisionOrderEntries(changes.supervisionOrderList.currentValue) this.getSupervisionOrderEntries(changes.supervisionOrderList.currentValue)

View File

@@ -11,7 +11,7 @@ import { URLCombiner } from '../../../../../core/url-combiner/url-combiner';
import { WorkspaceItemAdminWorkflowActionsComponent } from './workspace-item-admin-workflow-actions.component'; import { WorkspaceItemAdminWorkflowActionsComponent } from './workspace-item-admin-workflow-actions.component';
import { WorkspaceItem } from '../../../../../core/submission/models/workspaceitem.model'; import { WorkspaceItem } from '../../../../../core/submission/models/workspaceitem.model';
import { import {
getWorkflowItemDeleteRoute, getWorkspaceItemDeleteRoute,
} from '../../../../../workflowitems-edit-page/workflowitems-edit-page-routing-paths'; } from '../../../../../workflowitems-edit-page/workflowitems-edit-page-routing-paths';
import { Item } from '../../../../../core/shared/item.model'; import { Item } from '../../../../../core/shared/item.model';
import { RemoteData } from '../../../../../core/data/remote-data'; import { RemoteData } from '../../../../../core/data/remote-data';
@@ -83,7 +83,7 @@ describe('WorkspaceItemAdminWorkflowActionsComponent', () => {
it('should render a delete button with the correct link', () => { it('should render a delete button with the correct link', () => {
const button = fixture.debugElement.query(By.css('a.delete-link')); const button = fixture.debugElement.query(By.css('a.delete-link'));
const link = button.nativeElement.href; const link = button.nativeElement.href;
expect(link).toContain(new URLCombiner(getWorkflowItemDeleteRoute(wsi.id)).toString()); expect(link).toContain(new URLCombiner(getWorkspaceItemDeleteRoute(wsi.id)).toString());
}); });
it('should render a policies button with the correct link', () => { it('should render a policies button with the correct link', () => {

View File

@@ -11,7 +11,7 @@ import {
SupervisionOrderGroupSelectorComponent SupervisionOrderGroupSelectorComponent
} from './supervision-order-group-selector/supervision-order-group-selector.component'; } from './supervision-order-group-selector/supervision-order-group-selector.component';
import { import {
getWorkflowItemDeleteRoute getWorkspaceItemDeleteRoute
} from '../../../../../workflowitems-edit-page/workflowitems-edit-page-routing-paths'; } from '../../../../../workflowitems-edit-page/workflowitems-edit-page-routing-paths';
import { ITEM_EDIT_AUTHORIZATIONS_PATH } from '../../../../../item-page/edit-item-page/edit-item-page.routing-paths'; import { ITEM_EDIT_AUTHORIZATIONS_PATH } from '../../../../../item-page/edit-item-page/edit-item-page.routing-paths';
import { WorkspaceItem } from '../../../../../core/submission/models/workspaceitem.model'; import { WorkspaceItem } from '../../../../../core/submission/models/workspaceitem.model';
@@ -105,10 +105,10 @@ export class WorkspaceItemAdminWorkflowActionsComponent implements OnInit {
} }
/** /**
* Returns the path to the delete page of this workflow item * Returns the path to the delete page of this workspace item
*/ */
getDeleteRoute(): string { getDeleteRoute(): string {
return getWorkflowItemDeleteRoute(this.wsi.id); return getWorkspaceItemDeleteRoute(this.wsi.id);
} }
/** /**

View File

@@ -23,6 +23,7 @@ import {
import { take } from 'rxjs/operators'; import { take } from 'rxjs/operators';
import { WorkflowItemSearchResult } from '../../../../../shared/object-collection/shared/workflow-item-search-result.model'; import { WorkflowItemSearchResult } from '../../../../../shared/object-collection/shared/workflow-item-search-result.model';
import { ThemeService } from '../../../../../shared/theme-support/theme.service'; import { ThemeService } from '../../../../../shared/theme-support/theme.service';
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
@listableObjectComponent(WorkflowItemSearchResult, ViewMode.GridElement, Context.AdminWorkflowSearch) @listableObjectComponent(WorkflowItemSearchResult, ViewMode.GridElement, Context.AdminWorkflowSearch)
@Component({ @Component({
@@ -55,13 +56,14 @@ export class WorkflowItemSearchResultAdminWorkflowGridElementComponent extends S
public item$: Observable<Item>; public item$: Observable<Item>;
constructor( constructor(
public dsoNameService: DSONameService,
private componentFactoryResolver: ComponentFactoryResolver, private componentFactoryResolver: ComponentFactoryResolver,
private linkService: LinkService, private linkService: LinkService,
protected truncatableService: TruncatableService, protected truncatableService: TruncatableService,
private themeService: ThemeService, private themeService: ThemeService,
protected bitstreamDataService: BitstreamDataService protected bitstreamDataService: BitstreamDataService
) { ) {
super(truncatableService, bitstreamDataService); super(dsoNameService, truncatableService, bitstreamDataService);
} }
/** /**

View File

@@ -1,4 +1,4 @@
import { Component, ComponentFactoryResolver, ElementRef, ViewChild } from '@angular/core'; import { Component, ComponentFactoryResolver, ElementRef, ViewChild, OnInit } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs'; import { BehaviorSubject, Observable } from 'rxjs';
import { map, mergeMap, take, tap } from 'rxjs/operators'; import { map, mergeMap, take, tap } from 'rxjs/operators';
@@ -36,6 +36,7 @@ import { DSpaceObject } from '../../../../../core/shared/dspace-object.model';
import { SupervisionOrder } from '../../../../../core/supervision-order/models/supervision-order.model'; import { SupervisionOrder } from '../../../../../core/supervision-order/models/supervision-order.model';
import { PaginatedList } from '../../../../../core/data/paginated-list.model'; import { PaginatedList } from '../../../../../core/data/paginated-list.model';
import { SupervisionOrderDataService } from '../../../../../core/supervision-order/supervision-order-data.service'; import { SupervisionOrderDataService } from '../../../../../core/supervision-order/supervision-order-data.service';
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
@listableObjectComponent(WorkspaceItemSearchResult, ViewMode.GridElement, Context.AdminWorkflowSearch) @listableObjectComponent(WorkspaceItemSearchResult, ViewMode.GridElement, Context.AdminWorkflowSearch)
@Component({ @Component({
@@ -46,7 +47,7 @@ import { SupervisionOrderDataService } from '../../../../../core/supervision-ord
/** /**
* The component for displaying a grid element for an workflow item on the admin workflow search page * The component for displaying a grid element for an workflow item on the admin workflow search page
*/ */
export class WorkspaceItemSearchResultAdminWorkflowGridElementComponent extends SearchResultGridElementComponent<WorkspaceItemSearchResult, WorkspaceItem> { export class WorkspaceItemSearchResultAdminWorkflowGridElementComponent extends SearchResultGridElementComponent<WorkspaceItemSearchResult, WorkspaceItem> implements OnInit {
/** /**
* The item linked to the workspace item * The item linked to the workspace item
@@ -79,6 +80,7 @@ export class WorkspaceItemSearchResultAdminWorkflowGridElementComponent extends
@ViewChild('buttons', { static: true }) buttons: ElementRef; @ViewChild('buttons', { static: true }) buttons: ElementRef;
constructor( constructor(
public dsoNameService: DSONameService,
private componentFactoryResolver: ComponentFactoryResolver, private componentFactoryResolver: ComponentFactoryResolver,
private linkService: LinkService, private linkService: LinkService,
protected truncatableService: TruncatableService, protected truncatableService: TruncatableService,
@@ -86,7 +88,7 @@ export class WorkspaceItemSearchResultAdminWorkflowGridElementComponent extends
protected bitstreamDataService: BitstreamDataService, protected bitstreamDataService: BitstreamDataService,
protected supervisionOrderDataService: SupervisionOrderDataService, protected supervisionOrderDataService: SupervisionOrderDataService,
) { ) {
super(truncatableService, bitstreamDataService); super(dsoNameService, truncatableService, bitstreamDataService);
} }
/** /**

View File

@@ -39,7 +39,7 @@ export class WorkflowItemSearchResultAdminWorkflowListElementComponent extends S
constructor(private linkService: LinkService, constructor(private linkService: LinkService,
protected truncatableService: TruncatableService, protected truncatableService: TruncatableService,
protected dsoNameService: DSONameService, public dsoNameService: DSONameService,
@Inject(APP_CONFIG) protected appConfig: AppConfig @Inject(APP_CONFIG) protected appConfig: AppConfig
) { ) {
super(truncatableService, dsoNameService, appConfig); super(truncatableService, dsoNameService, appConfig);

View File

@@ -59,7 +59,7 @@ export class WorkspaceItemSearchResultAdminWorkflowListElementComponent extends
public supervisionOrder$: BehaviorSubject<SupervisionOrder[]> = new BehaviorSubject<SupervisionOrder[]>([]); public supervisionOrder$: BehaviorSubject<SupervisionOrder[]> = new BehaviorSubject<SupervisionOrder[]>([]);
constructor(private linkService: LinkService, constructor(private linkService: LinkService,
protected dsoNameService: DSONameService, public dsoNameService: DSONameService,
protected supervisionOrderDataService: SupervisionOrderDataService, protected supervisionOrderDataService: SupervisionOrderDataService,
protected truncatableService: TruncatableService, protected truncatableService: TruncatableService,
@Inject(APP_CONFIG) protected appConfig: AppConfig @Inject(APP_CONFIG) protected appConfig: AppConfig

View File

@@ -10,6 +10,7 @@ import { AdminSearchModule } from './admin-search-page/admin-search.module';
import { AdminSidebarSectionComponent } from './admin-sidebar/admin-sidebar-section/admin-sidebar-section.component'; import { AdminSidebarSectionComponent } from './admin-sidebar/admin-sidebar-section/admin-sidebar-section.component';
import { ExpandableAdminSidebarSectionComponent } from './admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component'; import { ExpandableAdminSidebarSectionComponent } from './admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component';
import { BatchImportPageComponent } from './admin-import-batch-page/batch-import-page.component'; import { BatchImportPageComponent } from './admin-import-batch-page/batch-import-page.component';
import { UiSwitchModule } from 'ngx-ui-switch';
import { UploadModule } from '../shared/upload/upload.module'; import { UploadModule } from '../shared/upload/upload.module';
const ENTRY_COMPONENTS = [ const ENTRY_COMPONENTS = [
@@ -27,6 +28,7 @@ const ENTRY_COMPONENTS = [
AdminSearchModule.withEntryComponents(), AdminSearchModule.withEntryComponents(),
AdminWorkflowModuleModule.withEntryComponents(), AdminWorkflowModuleModule.withEntryComponents(),
SharedModule, SharedModule,
UiSwitchModule,
UploadModule, UploadModule,
], ],
declarations: [ declarations: [

View File

@@ -38,6 +38,7 @@ import {
ThemedPageInternalServerErrorComponent ThemedPageInternalServerErrorComponent
} from './page-internal-server-error/themed-page-internal-server-error.component'; } from './page-internal-server-error/themed-page-internal-server-error.component';
import { ServerCheckGuard } from './core/server-check/server-check.guard'; import { ServerCheckGuard } from './core/server-check/server-check.guard';
import { SUGGESTION_MODULE_PATH } from './suggestions-page/suggestions-page-routing-paths';
import { MenuResolver } from './menu.resolver'; import { MenuResolver } from './menu.resolver';
import { ThemedPageErrorComponent } from './page-error/themed-page-error.component'; import { ThemedPageErrorComponent } from './page-error/themed-page-error.component';
@@ -202,6 +203,11 @@ import { ThemedPageErrorComponent } from './page-error/themed-page-error.compone
.then((m) => m.ProcessPageModule), .then((m) => m.ProcessPageModule),
canActivate: [AuthenticatedGuard, EndUserAgreementCurrentUserGuard] canActivate: [AuthenticatedGuard, EndUserAgreementCurrentUserGuard]
}, },
{ path: SUGGESTION_MODULE_PATH,
loadChildren: () => import('./suggestions-page/suggestions-page.module')
.then((m) => m.SuggestionsPageModule),
canActivate: [AuthenticatedGuard, EndUserAgreementCurrentUserGuard]
},
{ {
path: INFO_MODULE_PATH, path: INFO_MODULE_PATH,
loadChildren: () => import('./info/info.module').then((m) => m.InfoModule) loadChildren: () => import('./info/info.module').then((m) => m.InfoModule)
@@ -209,7 +215,7 @@ import { ThemedPageErrorComponent } from './page-error/themed-page-error.compone
{ {
path: REQUEST_COPY_MODULE_PATH, path: REQUEST_COPY_MODULE_PATH,
loadChildren: () => import('./request-copy/request-copy.module').then((m) => m.RequestCopyModule), loadChildren: () => import('./request-copy/request-copy.module').then((m) => m.RequestCopyModule),
canActivate: [AuthenticatedGuard, EndUserAgreementCurrentUserGuard] canActivate: [EndUserAgreementCurrentUserGuard]
}, },
{ {
path: FORBIDDEN_PATH, path: FORBIDDEN_PATH,

View File

@@ -1,5 +1,5 @@
<div class="container"> <div class="container">
<h3>{{'bitstream.download.page' | translate:{bitstream: (bitstream$ | async)?.name} }}</h3> <h3>{{'bitstream.download.page' | translate:{ bitstream: dsoNameService.getName((bitstream$ | async)) } }}</h3>
<div class="pt-3"> <div class="pt-3">
<button (click)="back()" class="btn btn-outline-secondary"> <button (click)="back()" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left"></i> {{'bitstream.download.page.back' | translate}} <i class="fas fa-arrow-left"></i> {{'bitstream.download.page.back' | translate}}

View File

@@ -11,6 +11,9 @@ import { ActivatedRoute, Router } from '@angular/router';
import { getForbiddenRoute } from '../../app-routing-paths'; import { getForbiddenRoute } from '../../app-routing-paths';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { SignpostingDataService } from '../../core/data/signposting-data.service';
import { ServerResponseService } from '../../core/services/server-response.service';
import { PLATFORM_ID } from '@angular/core';
describe('BitstreamDownloadPageComponent', () => { describe('BitstreamDownloadPageComponent', () => {
let component: BitstreamDownloadPageComponent; let component: BitstreamDownloadPageComponent;
@@ -24,6 +27,20 @@ describe('BitstreamDownloadPageComponent', () => {
let router; let router;
let bitstream: Bitstream; let bitstream: Bitstream;
let serverResponseService: jasmine.SpyObj<ServerResponseService>;
let signpostingDataService: jasmine.SpyObj<SignpostingDataService>;
const mocklink = {
href: 'http://test.org',
rel: 'test',
type: 'test'
};
const mocklink2 = {
href: 'http://test2.org',
rel: 'test',
type: 'test'
};
function init() { function init() {
authService = jasmine.createSpyObj('authService', { authService = jasmine.createSpyObj('authService', {
@@ -44,8 +61,8 @@ describe('BitstreamDownloadPageComponent', () => {
bitstream = Object.assign(new Bitstream(), { bitstream = Object.assign(new Bitstream(), {
uuid: 'bitstreamUuid', uuid: 'bitstreamUuid',
_links: { _links: {
content: {href: 'bitstream-content-link'}, content: { href: 'bitstream-content-link' },
self: {href: 'bitstream-self-link'}, self: { href: 'bitstream-self-link' },
} }
}); });
@@ -54,10 +71,21 @@ describe('BitstreamDownloadPageComponent', () => {
bitstream: createSuccessfulRemoteDataObject( bitstream: createSuccessfulRemoteDataObject(
bitstream bitstream
) )
}),
params: observableOf({
id: 'testid'
}) })
}; };
router = jasmine.createSpyObj('router', ['navigateByUrl']); router = jasmine.createSpyObj('router', ['navigateByUrl']);
serverResponseService = jasmine.createSpyObj('ServerResponseService', {
setHeader: jasmine.createSpy('setHeader'),
});
signpostingDataService = jasmine.createSpyObj('SignpostingDataService', {
getLinks: observableOf([mocklink, mocklink2])
});
} }
function initTestbed() { function initTestbed() {
@@ -65,12 +93,15 @@ describe('BitstreamDownloadPageComponent', () => {
imports: [CommonModule, TranslateModule.forRoot()], imports: [CommonModule, TranslateModule.forRoot()],
declarations: [BitstreamDownloadPageComponent], declarations: [BitstreamDownloadPageComponent],
providers: [ providers: [
{provide: ActivatedRoute, useValue: activatedRoute}, { provide: ActivatedRoute, useValue: activatedRoute },
{provide: Router, useValue: router}, { provide: Router, useValue: router },
{provide: AuthorizationDataService, useValue: authorizationService}, { provide: AuthorizationDataService, useValue: authorizationService },
{provide: AuthService, useValue: authService}, { provide: AuthService, useValue: authService },
{provide: FileService, useValue: fileService}, { provide: FileService, useValue: fileService },
{provide: HardRedirectService, useValue: hardRedirectService}, { provide: HardRedirectService, useValue: hardRedirectService },
{ provide: ServerResponseService, useValue: serverResponseService },
{ provide: SignpostingDataService, useValue: signpostingDataService },
{ provide: PLATFORM_ID, useValue: 'server' }
] ]
}) })
.compileComponents(); .compileComponents();
@@ -107,6 +138,9 @@ describe('BitstreamDownloadPageComponent', () => {
it('should redirect to the content link', () => { it('should redirect to the content link', () => {
expect(hardRedirectService.redirect).toHaveBeenCalledWith('bitstream-content-link'); expect(hardRedirectService.redirect).toHaveBeenCalledWith('bitstream-content-link');
}); });
it('should add the signposting links', () => {
expect(serverResponseService.setHeader).toHaveBeenCalled();
});
}); });
describe('when the user is authorized and logged in', () => { describe('when the user is authorized and logged in', () => {
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
@@ -134,7 +168,7 @@ describe('BitstreamDownloadPageComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should navigate to the forbidden route', () => { it('should navigate to the forbidden route', () => {
expect(router.navigateByUrl).toHaveBeenCalledWith(getForbiddenRoute(), {skipLocationChange: true}); expect(router.navigateByUrl).toHaveBeenCalledWith(getForbiddenRoute(), { skipLocationChange: true });
}); });
}); });
describe('when the user is not authorized and not logged in', () => { describe('when the user is not authorized and not logged in', () => {

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