mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
Merge remote-tracking branch 'upstream/main' into added-skip-to-main-content-button_contribute-main
# Conflicts: # src/app/root/root.component.html
This commit is contained in:
@@ -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
|
|
10
.github/workflows/codescan.yml
vendored
10
.github/workflows/codescan.yml
vendored
@@ -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'
|
||||||
|
62
.github/workflows/docker.yml
vendored
62
.github/workflows/docker.yml
vendored
@@ -15,23 +15,19 @@ on:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read # to fetch code (actions/checkout)
|
contents: read # to fetch code (actions/checkout)
|
||||||
|
|
||||||
jobs:
|
|
||||||
docker:
|
env:
|
||||||
# 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
|
|
||||||
env:
|
|
||||||
# Define tags to use for Docker images based on Git tags/branches (for docker/metadata-action)
|
# 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 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 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.
|
# For a new tag, copy that tag name as the tag for Docker image.
|
||||||
IMAGE_TAGS: |
|
IMAGE_TAGS: |
|
||||||
type=raw,value=dspace-7_x,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }}
|
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=branch,enable=${{ !endsWith(github.ref, github.event.repository.default_branch) }}
|
||||||
type=ref,event=tag
|
type=ref,event=tag
|
||||||
# Define default tag "flavor" for docker/metadata-action per
|
# Define default tag "flavor" for docker/metadata-action per
|
||||||
# https://github.com/docker/metadata-action#flavor-input
|
# https://github.com/docker/metadata-action#flavor-input
|
||||||
# We turn off 'latest' tag by default.
|
# We manage the 'latest' tag ourselves to the 'main' branch (see settings above)
|
||||||
TAGS_FLAVOR: |
|
TAGS_FLAVOR: |
|
||||||
latest=false
|
latest=false
|
||||||
# Architectures / Platforms for which we will build Docker images
|
# Architectures / Platforms for which we will build Docker images
|
||||||
@@ -39,6 +35,16 @@ jobs:
|
|||||||
# If this is NOT a PR (e.g. a tag or merge commit), also build for ARM64.
|
# 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' || '' }}
|
PLATFORMS: linux/amd64${{ github.event_name != 'pull_request' && ', linux/arm64' || '' }}
|
||||||
|
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
###############################################
|
||||||
|
# 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'
|
||||||
|
if: github.repository == 'dspace/dspace-angular'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
# https://github.com/actions/checkout
|
# https://github.com/actions/checkout
|
||||||
- name: Checkout codebase
|
- name: Checkout codebase
|
||||||
@@ -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
|
||||||
|
9
.github/workflows/label_merge_conflicts.yml
vendored
9
.github/workflows/label_merge_conflicts.yml
vendored
@@ -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
|
||||||
|
46
.github/workflows/port_merged_pull_request.yml
vendored
Normal file
46
.github/workflows/port_merged_pull_request.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# 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: '.*'
|
||||||
|
# Skip any merge commits in the ported PR. This means only non-merge commits are cherry-picked to the new PR
|
||||||
|
merge_commits: 'skip'
|
||||||
|
# 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 }}
|
24
.github/workflows/pull_request_opened.yml
vendored
Normal file
24
.github/workflows/pull_request_opened.yml
vendored
Normal 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,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
|
||||||
|
|
||||||
|
@@ -157,8 +157,8 @@ DSPACE_UI_SSL => DSPACE_SSL
|
|||||||
|
|
||||||
The same settings can also be overwritten by setting system environment variables instead, E.g.:
|
The same settings can also be overwritten by setting system environment variables instead, E.g.:
|
||||||
```bash
|
```bash
|
||||||
export DSPACE_HOST=api7.dspace.org
|
export DSPACE_HOST=demo.dspace.org
|
||||||
export DSPACE_UI_PORT=4200
|
export DSPACE_UI_PORT=4000
|
||||||
```
|
```
|
||||||
|
|
||||||
The priority works as follows: **environment variable** overrides **variable in `.env` file** overrides external config set by `DSPACE_APP_CONFIG_PATH` overrides **`config.(prod or dev).yml`**
|
The priority works as follows: **environment variable** overrides **variable in `.env` file** overrides external config set by `DSPACE_APP_CONFIG_PATH` overrides **`config.(prod or dev).yml`**
|
||||||
@@ -288,7 +288,7 @@ E2E tests (aka integration tests) use [Cypress.io](https://www.cypress.io/). Con
|
|||||||
The test files can be found in the `./cypress/integration/` folder.
|
The test files can be found in the `./cypress/integration/` folder.
|
||||||
|
|
||||||
Before you can run e2e tests, two things are REQUIRED:
|
Before you can run e2e tests, two things are REQUIRED:
|
||||||
1. You MUST be running the DSpace backend (i.e. REST API) locally. The e2e tests will *NOT* succeed if run against our demo REST API (https://api7.dspace.org/server/), as that server is uncontrolled and may have content added/removed at any time.
|
1. You MUST be running the DSpace backend (i.e. REST API) locally. The e2e tests will *NOT* succeed if run against our demo/sandbox REST API (https://demo.dspace.org/server/ or https://sandbox.dspace.org/server/), as those sites may have content added/removed at any time.
|
||||||
* After starting up your backend on localhost, make sure either your `config.prod.yml` or `config.dev.yml` has its `rest` settings defined to use that localhost backend.
|
* After starting up your backend on localhost, make sure either your `config.prod.yml` or `config.dev.yml` has its `rest` settings defined to use that localhost backend.
|
||||||
* If you'd prefer, you may instead use environment variables as described at [Configuring](#configuring). For example:
|
* If you'd prefer, you may instead use environment variables as described at [Configuring](#configuring). For example:
|
||||||
```
|
```
|
||||||
|
@@ -22,7 +22,7 @@ ui:
|
|||||||
# 'synced' with the 'dspace.server.url' setting in your backend's local.cfg.
|
# 'synced' with the 'dspace.server.url' setting in your backend's local.cfg.
|
||||||
rest:
|
rest:
|
||||||
ssl: true
|
ssl: true
|
||||||
host: api7.dspace.org
|
host: sandbox.dspace.org
|
||||||
port: 443
|
port: 443
|
||||||
# NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript
|
# NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript
|
||||||
nameSpace: /server
|
nameSpace: /server
|
||||||
@@ -208,6 +208,9 @@ languages:
|
|||||||
- code: pt-BR
|
- code: pt-BR
|
||||||
label: Português do Brasil
|
label: Português do Brasil
|
||||||
active: true
|
active: true
|
||||||
|
- code: sr-lat
|
||||||
|
label: Srpski (lat)
|
||||||
|
active: true
|
||||||
- code: fi
|
- code: fi
|
||||||
label: Suomi
|
label: Suomi
|
||||||
active: true
|
active: true
|
||||||
@@ -232,6 +235,9 @@ languages:
|
|||||||
- code: el
|
- code: el
|
||||||
label: Ελληνικά
|
label: Ελληνικά
|
||||||
active: true
|
active: true
|
||||||
|
- code: sr-cyr
|
||||||
|
label: Српски
|
||||||
|
active: true
|
||||||
- code: uk
|
- code: uk
|
||||||
label: Yкраї́нська
|
label: Yкраї́нська
|
||||||
active: true
|
active: true
|
||||||
@@ -292,33 +298,33 @@ themes:
|
|||||||
#
|
#
|
||||||
# # A theme with a handle property will match the community, collection or item with the given
|
# # A theme with a handle property will match the community, collection or item with the given
|
||||||
# # handle, and all collections and/or items within it
|
# # handle, and all collections and/or items within it
|
||||||
# - name: 'custom',
|
# - name: custom
|
||||||
# handle: '10673/1233'
|
# handle: 10673/1233
|
||||||
#
|
#
|
||||||
# # A theme with a regex property will match the route using a regular expression. If it
|
# # A theme with a regex property will match the route using a regular expression. If it
|
||||||
# # matches the route for a community or collection it will also apply to all collections
|
# # matches the route for a community or collection it will also apply to all collections
|
||||||
# # and/or items within it
|
# # and/or items within it
|
||||||
# - name: 'custom',
|
# - name: custom
|
||||||
# regex: 'collections\/e8043bc2.*'
|
# regex: collections\/e8043bc2.*
|
||||||
#
|
#
|
||||||
# # A theme with a uuid property will match the community, collection or item with the given
|
# # A theme with a uuid property will match the community, collection or item with the given
|
||||||
# # ID, and all collections and/or items within it
|
# # ID, and all collections and/or items within it
|
||||||
# - name: 'custom',
|
# - name: custom
|
||||||
# uuid: '0958c910-2037-42a9-81c7-dca80e3892b4'
|
# uuid: 0958c910-2037-42a9-81c7-dca80e3892b4
|
||||||
#
|
#
|
||||||
# # The extends property specifies an ancestor theme (by name). Whenever a themed component is not found
|
# # The extends property specifies an ancestor theme (by name). Whenever a themed component is not found
|
||||||
# # in the current theme, its ancestor theme(s) will be checked recursively before falling back to default.
|
# # in the current theme, its ancestor theme(s) will be checked recursively before falling back to default.
|
||||||
# - name: 'custom-A',
|
# - name: custom-A
|
||||||
# extends: 'custom-B',
|
# extends: custom-B
|
||||||
# # Any of the matching properties above can be used
|
# # Any of the matching properties above can be used
|
||||||
# handle: '10673/34'
|
# handle: 10673/34
|
||||||
#
|
#
|
||||||
# - name: 'custom-B',
|
# - name: custom-B
|
||||||
# extends: 'custom',
|
# extends: custom
|
||||||
# handle: '10673/12'
|
# handle: 10673/12
|
||||||
#
|
#
|
||||||
# # A theme with only a name will match every route
|
# # A theme with only a name will match every route
|
||||||
# name: 'custom'
|
# name: custom
|
||||||
#
|
#
|
||||||
# # This theme will use the default bootstrap styling for DSpace components
|
# # This theme will use the default bootstrap styling for DSpace components
|
||||||
# - name: BASE_THEME_NAME
|
# - name: BASE_THEME_NAME
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
rest:
|
rest:
|
||||||
ssl: true
|
ssl: true
|
||||||
host: api7.dspace.org
|
host: sandbox.dspace.org
|
||||||
port: 443
|
port: 443
|
||||||
nameSpace: /server
|
nameSpace: /server
|
||||||
|
@@ -1,4 +1,3 @@
|
|||||||
import { Options } from 'cypress-axe';
|
|
||||||
import { testA11y } from 'cypress/support/utils';
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
describe('Community List Page', () => {
|
describe('Community List Page', () => {
|
||||||
@@ -13,13 +12,6 @@ describe('Community List Page', () => {
|
|||||||
cy.get('[data-test="expand-button"]').click({ multiple: true });
|
cy.get('[data-test="expand-button"]').click({ multiple: true });
|
||||||
|
|
||||||
// Analyze <ds-community-list-page> for accessibility issues
|
// Analyze <ds-community-list-page> for accessibility issues
|
||||||
// Disable heading-order checks until it is fixed
|
testA11y('ds-community-list-page');
|
||||||
testA11y('ds-community-list-page',
|
|
||||||
{
|
|
||||||
rules: {
|
|
||||||
'heading-order': { enabled: false }
|
|
||||||
}
|
|
||||||
} as Options
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -11,8 +11,7 @@ describe('Header', () => {
|
|||||||
testA11y({
|
testA11y({
|
||||||
include: ['ds-header'],
|
include: ['ds-header'],
|
||||||
exclude: [
|
exclude: [
|
||||||
['#search-navbar-container'], // search in navbar has duplicative ID. Will be fixed in #1174
|
['#search-navbar-container'] // search in navbar has duplicative ID. Will be fixed in #1174
|
||||||
['.dropdownLogin'] // "Log in" link has color contrast issues. Will be fixed in #1149
|
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,4 +1,3 @@
|
|||||||
import { Options } from 'cypress-axe';
|
|
||||||
import { TEST_ENTITY_PUBLICATION } from 'cypress/support/e2e';
|
import { TEST_ENTITY_PUBLICATION } from 'cypress/support/e2e';
|
||||||
import { testA11y } from 'cypress/support/utils';
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
@@ -19,13 +18,16 @@ describe('Item Page', () => {
|
|||||||
cy.get('ds-item-page').should('be.visible');
|
cy.get('ds-item-page').should('be.visible');
|
||||||
|
|
||||||
// Analyze <ds-item-page> for accessibility issues
|
// Analyze <ds-item-page> for accessibility issues
|
||||||
// Disable heading-order checks until it is fixed
|
testA11y('ds-item-page');
|
||||||
testA11y('ds-item-page',
|
});
|
||||||
{
|
|
||||||
rules: {
|
it('should pass accessibility tests on full item page', () => {
|
||||||
'heading-order': { enabled: false }
|
cy.visit(ENTITYPAGE + '/full');
|
||||||
}
|
|
||||||
} as Options
|
// <ds-full-item-page> tag must be loaded
|
||||||
);
|
cy.get('ds-full-item-page').should('be.visible');
|
||||||
|
|
||||||
|
// Analyze <ds-full-item-page> for accessibility issues
|
||||||
|
testA11y('ds-full-item-page');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import { TEST_ADMIN_PASSWORD, TEST_ADMIN_USER, TEST_ENTITY_PUBLICATION } from 'cypress/support/e2e';
|
import { TEST_ADMIN_PASSWORD, TEST_ADMIN_USER, TEST_ENTITY_PUBLICATION } from 'cypress/support/e2e';
|
||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
const page = {
|
const page = {
|
||||||
openLoginMenu() {
|
openLoginMenu() {
|
||||||
@@ -123,4 +124,15 @@ describe('Login Modal', () => {
|
|||||||
cy.location('pathname').should('eq', '/forgot');
|
cy.location('pathname').should('eq', '/forgot');
|
||||||
cy.get('ds-forgot-email').should('exist');
|
cy.get('ds-forgot-email').should('exist');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
cy.visit('/');
|
||||||
|
|
||||||
|
page.openLoginMenu();
|
||||||
|
|
||||||
|
cy.get('ds-log-in').should('exist');
|
||||||
|
|
||||||
|
// Analyze <ds-log-in> for accessibility issues
|
||||||
|
testA11y('ds-log-in');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -19,21 +19,7 @@ describe('My DSpace page', () => {
|
|||||||
cy.get('.filter-toggle').click({ multiple: true });
|
cy.get('.filter-toggle').click({ multiple: true });
|
||||||
|
|
||||||
// Analyze <ds-my-dspace-page> for accessibility issues
|
// Analyze <ds-my-dspace-page> for accessibility issues
|
||||||
testA11y(
|
testA11y('ds-my-dspace-page');
|
||||||
{
|
|
||||||
include: ['ds-my-dspace-page'],
|
|
||||||
exclude: [
|
|
||||||
['nouislider'] // Date filter slider is missing ARIA labels. Will be fixed by #1175
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
rules: {
|
|
||||||
// Search filters fail these two "moderate" impact rules
|
|
||||||
'heading-order': { enabled: false },
|
|
||||||
'landmark-unique': { enabled: false }
|
|
||||||
}
|
|
||||||
} as Options
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have a working detailed view that passes accessibility tests', () => {
|
it('should have a working detailed view that passes accessibility tests', () => {
|
||||||
|
@@ -1,8 +1,13 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
describe('PageNotFound', () => {
|
describe('PageNotFound', () => {
|
||||||
it('should contain element ds-pagenotfound when navigating to page that doesnt exist', () => {
|
it('should contain element ds-pagenotfound when navigating to page that doesnt exist', () => {
|
||||||
// request an invalid page (UUIDs at root path aren't valid)
|
// request an invalid page (UUIDs at root path aren't valid)
|
||||||
cy.visit('/e9019a69-d4f1-4773-b6a3-bd362caa46f2', { failOnStatusCode: false });
|
cy.visit('/e9019a69-d4f1-4773-b6a3-bd362caa46f2', { failOnStatusCode: false });
|
||||||
cy.get('ds-pagenotfound').should('be.visible');
|
cy.get('ds-pagenotfound').should('be.visible');
|
||||||
|
|
||||||
|
// Analyze <ds-pagenotfound> for accessibility issues
|
||||||
|
testA11y('ds-pagenotfound');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not contain element ds-pagenotfound when navigating to existing page', () => {
|
it('should not contain element ds-pagenotfound when navigating to existing page', () => {
|
||||||
|
@@ -27,21 +27,7 @@ describe('Search Page', () => {
|
|||||||
cy.get('[data-test="filter-toggle"]').click({ multiple: true });
|
cy.get('[data-test="filter-toggle"]').click({ multiple: true });
|
||||||
|
|
||||||
// Analyze <ds-search-page> for accessibility issues
|
// Analyze <ds-search-page> for accessibility issues
|
||||||
testA11y(
|
testA11y('ds-search-page');
|
||||||
{
|
|
||||||
include: ['ds-search-page'],
|
|
||||||
exclude: [
|
|
||||||
['nouislider'] // Date filter slider is missing ARIA labels. Will be fixed by #1175
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
rules: {
|
|
||||||
// Search filters fail these two "moderate" impact rules
|
|
||||||
'heading-order': { enabled: false },
|
|
||||||
'landmark-unique': { enabled: false }
|
|
||||||
}
|
|
||||||
} as Options
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have a working grid view that passes accessibility tests', () => {
|
it('should have a working grid view that passes accessibility tests', () => {
|
||||||
|
@@ -177,6 +177,8 @@ function generateViewEvent(uuid: string, dsoType: string): void {
|
|||||||
[XSRF_REQUEST_HEADER] : csrfToken,
|
[XSRF_REQUEST_HEADER] : csrfToken,
|
||||||
// use a known public IP address to avoid being seen as a "bot"
|
// use a known public IP address to avoid being seen as a "bot"
|
||||||
'X-Forwarded-For': '1.1.1.1',
|
'X-Forwarded-For': '1.1.1.1',
|
||||||
|
// Use a user-agent of a Firefox browser on Windows. This again avoids being seen as a "bot"
|
||||||
|
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0',
|
||||||
},
|
},
|
||||||
//form: true, // indicates the body should be form urlencoded
|
//form: true, // indicates the body should be form urlencoded
|
||||||
body: { targetId: uuid, targetType: dsoType },
|
body: { targetId: uuid, targetType: dsoType },
|
||||||
|
@@ -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*.
|
||||||
@@ -101,8 +101,8 @@ and the backend at http://localhost:8080/server/
|
|||||||
|
|
||||||
## Run DSpace Angular dist build with DSpace Demo site backend
|
## Run DSpace Angular dist build with DSpace Demo site backend
|
||||||
|
|
||||||
This allows you to run the Angular UI in *production* mode, pointing it at the demo backend
|
This allows you to run the Angular UI in *production* mode, pointing it at the demo or sandbox backend
|
||||||
(https://api7.dspace.org/server/).
|
(https://demo.dspace.org/server/ or https://sandbox.dspace.org/server/).
|
||||||
|
|
||||||
```
|
```
|
||||||
docker-compose -f docker/docker-compose-dist.yml pull
|
docker-compose -f docker/docker-compose-dist.yml pull
|
||||||
|
@@ -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.
|
||||||
|
@@ -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:
|
||||||
|
@@ -24,10 +24,10 @@ services:
|
|||||||
# This is because Server Side Rendering (SSR) currently requires a public URL,
|
# This is because Server Side Rendering (SSR) currently requires a public URL,
|
||||||
# see this bug: https://github.com/DSpace/dspace-angular/issues/1485
|
# see this bug: https://github.com/DSpace/dspace-angular/issues/1485
|
||||||
DSPACE_REST_SSL: 'true'
|
DSPACE_REST_SSL: 'true'
|
||||||
DSPACE_REST_HOST: api7.dspace.org
|
DSPACE_REST_HOST: sandbox.dspace.org
|
||||||
DSPACE_REST_PORT: 443
|
DSPACE_REST_PORT: 443
|
||||||
DSPACE_REST_NAMESPACE: /server
|
DSPACE_REST_NAMESPACE: /server
|
||||||
image: dspace/dspace-angular:dspace-7_x-dist
|
image: dspace/dspace-angular:${DSPACE_VER:-latest}-dist
|
||||||
build:
|
build:
|
||||||
context: ..
|
context: ..
|
||||||
dockerfile: Dockerfile.dist
|
dockerfile: Dockerfile.dist
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -48,7 +48,7 @@ dspace-angular connects to your DSpace installation by using its REST endpoint.
|
|||||||
```yaml
|
```yaml
|
||||||
rest:
|
rest:
|
||||||
ssl: true
|
ssl: true
|
||||||
host: api7.dspace.org
|
host: demo.dspace.org
|
||||||
port: 443
|
port: 443
|
||||||
nameSpace: /server
|
nameSpace: /server
|
||||||
}
|
}
|
||||||
@@ -57,7 +57,7 @@ rest:
|
|||||||
Alternately you can set the following environment variables. If any of these are set, it will override all configuration files:
|
Alternately you can set the following environment variables. If any of these are set, it will override all configuration files:
|
||||||
```
|
```
|
||||||
DSPACE_REST_SSL=true
|
DSPACE_REST_SSL=true
|
||||||
DSPACE_REST_HOST=api7.dspace.org
|
DSPACE_REST_HOST=demo.dspace.org
|
||||||
DSPACE_REST_PORT=443
|
DSPACE_REST_PORT=443
|
||||||
DSPACE_REST_NAMESPACE=/server
|
DSPACE_REST_NAMESPACE=/server
|
||||||
```
|
```
|
||||||
|
15
package.json
15
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dspace-angular",
|
"name": "dspace-angular",
|
||||||
"version": "7.6.0",
|
"version": "8.0.0-next",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
"config:watch": "nodemon",
|
"config:watch": "nodemon",
|
||||||
@@ -15,14 +15,14 @@
|
|||||||
"analyze": "webpack-bundle-analyzer dist/browser/stats.json",
|
"analyze": "webpack-bundle-analyzer dist/browser/stats.json",
|
||||||
"build": "ng build --configuration development",
|
"build": "ng build --configuration development",
|
||||||
"build:stats": "ng build --stats-json",
|
"build:stats": "ng build --stats-json",
|
||||||
"build:prod": "yarn run build:ssr",
|
"build:prod": "cross-env NODE_ENV=production 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 --source-map=true --watch=false --configuration test",
|
"test": "ng test --source-map=true --watch=false --configuration test",
|
||||||
"test:watch": "nodemon --exec \"ng test --source-map=true --watch=true --configuration test\"",
|
"test:watch": "nodemon --exec \"ng test --source-map=true --watch=true --configuration test\"",
|
||||||
"test:headless": "ng test --source-map=true --watch=false --configuration test --browsers=ChromeHeadless --code-coverage",
|
"test:headless": "ng test --source-map=true --watch=false --configuration test --browsers=ChromeHeadless --code-coverage",
|
||||||
"lint": "ng lint",
|
"lint": "ng lint",
|
||||||
"lint-fix": "ng lint --fix=true",
|
"lint-fix": "ng lint --fix=true",
|
||||||
"e2e": "ng e2e",
|
"e2e": "cross-env NODE_ENV=production ng e2e",
|
||||||
"clean:dev:config": "rimraf src/assets/config.json",
|
"clean:dev:config": "rimraf src/assets/config.json",
|
||||||
"clean:coverage": "rimraf coverage",
|
"clean:coverage": "rimraf coverage",
|
||||||
"clean:dist": "rimraf dist",
|
"clean:dist": "rimraf dist",
|
||||||
@@ -99,6 +99,7 @@
|
|||||||
"fast-json-patch": "^3.1.1",
|
"fast-json-patch": "^3.1.1",
|
||||||
"filesize": "^6.1.0",
|
"filesize": "^6.1.0",
|
||||||
"http-proxy-middleware": "^1.0.5",
|
"http-proxy-middleware": "^1.0.5",
|
||||||
|
"http-terminator": "^3.2.0",
|
||||||
"isbot": "^3.6.10",
|
"isbot": "^3.6.10",
|
||||||
"js-cookie": "2.2.1",
|
"js-cookie": "2.2.1",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
@@ -116,12 +117,12 @@
|
|||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"ng-mocks": "^14.10.0",
|
"ng-mocks": "^14.10.0",
|
||||||
"ng2-file-upload": "1.4.0",
|
"ng2-file-upload": "1.4.0",
|
||||||
"ng2-nouislider": "^1.8.3",
|
"ng2-nouislider": "^2.0.0",
|
||||||
"ngx-infinite-scroll": "^15.0.0",
|
"ngx-infinite-scroll": "^15.0.0",
|
||||||
"ngx-pagination": "6.0.3",
|
"ngx-pagination": "6.0.3",
|
||||||
"ngx-sortablejs": "^11.1.0",
|
"ngx-sortablejs": "^11.1.0",
|
||||||
"ngx-ui-switch": "^14.0.3",
|
"ngx-ui-switch": "^14.0.3",
|
||||||
"nouislider": "^14.6.3",
|
"nouislider": "^15.7.1",
|
||||||
"pem": "1.14.7",
|
"pem": "1.14.7",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"react-copy-to-clipboard": "^5.1.0",
|
"react-copy-to-clipboard": "^5.1.0",
|
||||||
@@ -159,11 +160,11 @@
|
|||||||
"@types/sanitize-html": "^2.9.0",
|
"@types/sanitize-html": "^2.9.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.59.1",
|
"@typescript-eslint/eslint-plugin": "^5.59.1",
|
||||||
"@typescript-eslint/parser": "^5.59.1",
|
"@typescript-eslint/parser": "^5.59.1",
|
||||||
"axe-core": "^4.7.0",
|
"axe-core": "^4.7.2",
|
||||||
"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.10.0",
|
"cypress": "12.17.4",
|
||||||
"cypress-axe": "^1.4.0",
|
"cypress-axe": "^1.4.0",
|
||||||
"deep-freeze": "0.0.1",
|
"deep-freeze": "0.0.1",
|
||||||
"eslint": "^8.39.0",
|
"eslint": "^8.39.0",
|
||||||
|
43
server.ts
43
server.ts
@@ -32,6 +32,7 @@ import isbot from 'isbot';
|
|||||||
import { createCertificate } from 'pem';
|
import { createCertificate } from 'pem';
|
||||||
import { createServer } from 'https';
|
import { createServer } from 'https';
|
||||||
import { json } from 'body-parser';
|
import { json } from 'body-parser';
|
||||||
|
import { createHttpTerminator } from 'http-terminator';
|
||||||
|
|
||||||
import { readFileSync } from 'fs';
|
import { readFileSync } from 'fs';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
@@ -320,22 +321,23 @@ function initCache() {
|
|||||||
if (botCacheEnabled()) {
|
if (botCacheEnabled()) {
|
||||||
// Initialize a new "least-recently-used" item cache (where least recently used pages are removed first)
|
// Initialize a new "least-recently-used" item cache (where least recently used pages are removed first)
|
||||||
// See https://www.npmjs.com/package/lru-cache
|
// See https://www.npmjs.com/package/lru-cache
|
||||||
// When enabled, each page defaults to expiring after 1 day
|
// When enabled, each page defaults to expiring after 1 day (defined in default-app-config.ts)
|
||||||
botCache = new LRU( {
|
botCache = new LRU( {
|
||||||
max: environment.cache.serverSide.botCache.max,
|
max: environment.cache.serverSide.botCache.max,
|
||||||
ttl: environment.cache.serverSide.botCache.timeToLive || 24 * 60 * 60 * 1000, // 1 day
|
ttl: environment.cache.serverSide.botCache.timeToLive,
|
||||||
allowStale: environment.cache.serverSide.botCache.allowStale ?? true // if object is stale, return stale value before deleting
|
allowStale: environment.cache.serverSide.botCache.allowStale
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (anonymousCacheEnabled()) {
|
if (anonymousCacheEnabled()) {
|
||||||
// NOTE: While caches may share SSR pages, this cache must be kept separately because the timeToLive
|
// NOTE: While caches may share SSR pages, this cache must be kept separately because the timeToLive
|
||||||
// may expire pages more frequently.
|
// may expire pages more frequently.
|
||||||
// When enabled, each page defaults to expiring after 10 seconds (to minimize anonymous users seeing out-of-date content)
|
// When enabled, each page defaults to expiring after 10 seconds (defined in default-app-config.ts)
|
||||||
|
// to minimize anonymous users seeing out-of-date content
|
||||||
anonymousCache = new LRU( {
|
anonymousCache = new LRU( {
|
||||||
max: environment.cache.serverSide.anonymousCache.max,
|
max: environment.cache.serverSide.anonymousCache.max,
|
||||||
ttl: environment.cache.serverSide.anonymousCache.timeToLive || 10 * 1000, // 10 seconds
|
ttl: environment.cache.serverSide.anonymousCache.timeToLive,
|
||||||
allowStale: environment.cache.serverSide.anonymousCache.allowStale ?? true // if object is stale, return stale value before deleting
|
allowStale: environment.cache.serverSide.anonymousCache.allowStale
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -487,7 +489,7 @@ function saveToCache(req, page: any) {
|
|||||||
*/
|
*/
|
||||||
function hasNotSucceeded(statusCode) {
|
function hasNotSucceeded(statusCode) {
|
||||||
const rgx = new RegExp(/^20+/);
|
const rgx = new RegExp(/^20+/);
|
||||||
return !rgx.test(statusCode)
|
return !rgx.test(statusCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
function retrieveHeaders(response) {
|
function retrieveHeaders(response) {
|
||||||
@@ -525,23 +527,46 @@ function serverStarted() {
|
|||||||
* @param keys SSL credentials
|
* @param keys SSL credentials
|
||||||
*/
|
*/
|
||||||
function createHttpsServer(keys) {
|
function createHttpsServer(keys) {
|
||||||
createServer({
|
const listener = createServer({
|
||||||
key: keys.serviceKey,
|
key: keys.serviceKey,
|
||||||
cert: keys.certificate
|
cert: keys.certificate
|
||||||
}, app).listen(environment.ui.port, environment.ui.host, () => {
|
}, app).listen(environment.ui.port, environment.ui.host, () => {
|
||||||
serverStarted();
|
serverStarted();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Graceful shutdown when signalled
|
||||||
|
const terminator = createHttpTerminator({server: listener});
|
||||||
|
process.on('SIGINT', () => {
|
||||||
|
void (async ()=> {
|
||||||
|
console.debug('Closing HTTPS server on signal');
|
||||||
|
await terminator.terminate().catch(e => { console.error(e); });
|
||||||
|
console.debug('HTTPS server closed');
|
||||||
|
})();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an HTTP server with the configured port and host.
|
||||||
|
*/
|
||||||
function run() {
|
function run() {
|
||||||
const port = environment.ui.port || 4000;
|
const port = environment.ui.port || 4000;
|
||||||
const host = environment.ui.host || '/';
|
const host = environment.ui.host || '/';
|
||||||
|
|
||||||
// Start up the Node server
|
// Start up the Node server
|
||||||
const server = app();
|
const server = app();
|
||||||
server.listen(port, host, () => {
|
const listener = server.listen(port, host, () => {
|
||||||
serverStarted();
|
serverStarted();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Graceful shutdown when signalled
|
||||||
|
const terminator = createHttpTerminator({server: listener});
|
||||||
|
process.on('SIGINT', () => {
|
||||||
|
void (async () => {
|
||||||
|
console.debug('Closing HTTP server on signal');
|
||||||
|
await terminator.terminate().catch(e => { console.error(e); });
|
||||||
|
console.debug('HTTP server closed.');return undefined;
|
||||||
|
})();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function start() {
|
function start() {
|
||||||
|
@@ -1,12 +1,22 @@
|
|||||||
import { URLCombiner } from '../core/url-combiner/url-combiner';
|
import { URLCombiner } from '../core/url-combiner/url-combiner';
|
||||||
import { getAccessControlModuleRoute } from '../app-routing-paths';
|
import { getAccessControlModuleRoute } from '../app-routing-paths';
|
||||||
|
|
||||||
export const GROUP_EDIT_PATH = 'groups';
|
export const EPERSON_PATH = 'epeople';
|
||||||
|
|
||||||
|
export function getEPersonsRoute(): string {
|
||||||
|
return new URLCombiner(getAccessControlModuleRoute(), EPERSON_PATH).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getEPersonEditRoute(id: string): string {
|
||||||
|
return new URLCombiner(getEPersonsRoute(), id, 'edit').toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GROUP_PATH = 'groups';
|
||||||
|
|
||||||
export function getGroupsRoute() {
|
export function getGroupsRoute() {
|
||||||
return new URLCombiner(getAccessControlModuleRoute(), GROUP_EDIT_PATH).toString();
|
return new URLCombiner(getAccessControlModuleRoute(), GROUP_PATH).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getGroupEditRoute(id: string) {
|
export function getGroupEditRoute(id: string) {
|
||||||
return new URLCombiner(getAccessControlModuleRoute(), GROUP_EDIT_PATH, id).toString();
|
return new URLCombiner(getGroupsRoute(), id, 'edit').toString();
|
||||||
}
|
}
|
||||||
|
@@ -3,7 +3,7 @@ import { RouterModule } from '@angular/router';
|
|||||||
import { EPeopleRegistryComponent } from './epeople-registry/epeople-registry.component';
|
import { EPeopleRegistryComponent } from './epeople-registry/epeople-registry.component';
|
||||||
import { GroupFormComponent } from './group-registry/group-form/group-form.component';
|
import { GroupFormComponent } from './group-registry/group-form/group-form.component';
|
||||||
import { GroupsRegistryComponent } from './group-registry/groups-registry.component';
|
import { GroupsRegistryComponent } from './group-registry/groups-registry.component';
|
||||||
import { GROUP_EDIT_PATH } from './access-control-routing-paths';
|
import { EPERSON_PATH, GROUP_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 {
|
import {
|
||||||
@@ -13,12 +13,14 @@ import {
|
|||||||
SiteAdministratorGuard
|
SiteAdministratorGuard
|
||||||
} from '../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard';
|
} from '../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard';
|
||||||
import { BulkAccessComponent } from './bulk-access/bulk-access.component';
|
import { BulkAccessComponent } from './bulk-access/bulk-access.component';
|
||||||
|
import { EPersonFormComponent } from './epeople-registry/eperson-form/eperson-form.component';
|
||||||
|
import { EPersonResolver } from './epeople-registry/eperson-resolver.service';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
RouterModule.forChild([
|
RouterModule.forChild([
|
||||||
{
|
{
|
||||||
path: 'epeople',
|
path: EPERSON_PATH,
|
||||||
component: EPeopleRegistryComponent,
|
component: EPeopleRegistryComponent,
|
||||||
resolve: {
|
resolve: {
|
||||||
breadcrumb: I18nBreadcrumbResolver
|
breadcrumb: I18nBreadcrumbResolver
|
||||||
@@ -27,7 +29,26 @@ import { BulkAccessComponent } from './bulk-access/bulk-access.component';
|
|||||||
canActivate: [SiteAdministratorGuard]
|
canActivate: [SiteAdministratorGuard]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: GROUP_EDIT_PATH,
|
path: `${EPERSON_PATH}/create`,
|
||||||
|
component: EPersonFormComponent,
|
||||||
|
resolve: {
|
||||||
|
breadcrumb: I18nBreadcrumbResolver,
|
||||||
|
},
|
||||||
|
data: { title: 'admin.access-control.epeople.add.title', breadcrumbKey: 'admin.access-control.epeople.add' },
|
||||||
|
canActivate: [SiteAdministratorGuard],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: `${EPERSON_PATH}/:id/edit`,
|
||||||
|
component: EPersonFormComponent,
|
||||||
|
resolve: {
|
||||||
|
breadcrumb: I18nBreadcrumbResolver,
|
||||||
|
ePerson: EPersonResolver,
|
||||||
|
},
|
||||||
|
data: { title: 'admin.access-control.epeople.edit.title', breadcrumbKey: 'admin.access-control.epeople.edit' },
|
||||||
|
canActivate: [SiteAdministratorGuard],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: GROUP_PATH,
|
||||||
component: GroupsRegistryComponent,
|
component: GroupsRegistryComponent,
|
||||||
resolve: {
|
resolve: {
|
||||||
breadcrumb: I18nBreadcrumbResolver
|
breadcrumb: I18nBreadcrumbResolver
|
||||||
@@ -36,7 +57,7 @@ import { BulkAccessComponent } from './bulk-access/bulk-access.component';
|
|||||||
canActivate: [GroupAdministratorGuard]
|
canActivate: [GroupAdministratorGuard]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: `${GROUP_EDIT_PATH}/newGroup`,
|
path: `${GROUP_PATH}/create`,
|
||||||
component: GroupFormComponent,
|
component: GroupFormComponent,
|
||||||
resolve: {
|
resolve: {
|
||||||
breadcrumb: I18nBreadcrumbResolver
|
breadcrumb: I18nBreadcrumbResolver
|
||||||
@@ -45,7 +66,7 @@ import { BulkAccessComponent } from './bulk-access/bulk-access.component';
|
|||||||
canActivate: [GroupAdministratorGuard]
|
canActivate: [GroupAdministratorGuard]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: `${GROUP_EDIT_PATH}/:groupId`,
|
path: `${GROUP_PATH}/:groupId/edit`,
|
||||||
component: GroupFormComponent,
|
component: GroupFormComponent,
|
||||||
resolve: {
|
resolve: {
|
||||||
breadcrumb: I18nBreadcrumbResolver
|
breadcrumb: I18nBreadcrumbResolver
|
||||||
|
@@ -4,19 +4,15 @@
|
|||||||
<div class="d-flex justify-content-between border-bottom mb-3">
|
<div class="d-flex justify-content-between border-bottom mb-3">
|
||||||
<h2 id="header" class="pb-2">{{labelPrefix + 'head' | translate}}</h2>
|
<h2 id="header" class="pb-2">{{labelPrefix + 'head' | translate}}</h2>
|
||||||
|
|
||||||
<div *ngIf="!isEPersonFormShown">
|
<div>
|
||||||
<button class="mr-auto btn btn-success addEPerson-button"
|
<button class="mr-auto btn btn-success addEPerson-button"
|
||||||
(click)="isEPersonFormShown = true">
|
[routerLink]="'create'">
|
||||||
<i class="fas fa-plus"></i>
|
<i class="fas fa-plus"></i>
|
||||||
<span class="d-none d-sm-inline ml-1">{{labelPrefix + 'button.add' | translate}}</span>
|
<span class="d-none d-sm-inline ml-1">{{labelPrefix + 'button.add' | translate}}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ds-eperson-form *ngIf="isEPersonFormShown" (submitForm)="reset()"
|
|
||||||
(cancelForm)="isEPersonFormShown = false"></ds-eperson-form>
|
|
||||||
|
|
||||||
<div *ngIf="!isEPersonFormShown">
|
|
||||||
<h3 id="search" class="border-bottom pb-2">{{labelPrefix + 'search.head' | translate}}
|
<h3 id="search" class="border-bottom pb-2">{{labelPrefix + 'search.head' | translate}}
|
||||||
|
|
||||||
</h3>
|
</h3>
|
||||||
@@ -72,12 +68,12 @@
|
|||||||
<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 [routerLink]="getEditEPeoplePage(epersonDto.eperson.id)"
|
||||||
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: dsoNameService.getName(epersonDto.eperson) } }}">
|
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 *ngIf="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: dsoNameService.getName(epersonDto.eperson) } }}">
|
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>
|
||||||
@@ -96,5 +92,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
@@ -203,36 +203,6 @@ describe('EPeopleRegistryComponent', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('toggleEditEPerson', () => {
|
|
||||||
describe('when you click on first edit eperson button', () => {
|
|
||||||
beforeEach(fakeAsync(() => {
|
|
||||||
const editButtons = fixture.debugElement.queryAll(By.css('.access-control-editEPersonButton'));
|
|
||||||
editButtons[0].triggerEventHandler('click', {
|
|
||||||
preventDefault: () => {/**/
|
|
||||||
}
|
|
||||||
});
|
|
||||||
tick();
|
|
||||||
fixture.detectChanges();
|
|
||||||
}));
|
|
||||||
|
|
||||||
it('editEPerson form is toggled', () => {
|
|
||||||
const ePeopleIds = fixture.debugElement.queryAll(By.css('#epeople tr td:first-child'));
|
|
||||||
ePersonDataServiceStub.getActiveEPerson().subscribe((activeEPerson: EPerson) => {
|
|
||||||
if (ePeopleIds[0] && activeEPerson === ePeopleIds[0].nativeElement.textContent) {
|
|
||||||
expect(component.isEPersonFormShown).toEqual(false);
|
|
||||||
} else {
|
|
||||||
expect(component.isEPersonFormShown).toEqual(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('EPerson search section is hidden', () => {
|
|
||||||
expect(fixture.debugElement.query(By.css('#search'))).toBeNull();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('deleteEPerson', () => {
|
describe('deleteEPerson', () => {
|
||||||
describe('when you click on first delete eperson button', () => {
|
describe('when you click on first delete eperson button', () => {
|
||||||
let ePeopleIdsFoundBeforeDelete;
|
let ePeopleIdsFoundBeforeDelete;
|
||||||
|
@@ -22,6 +22,7 @@ 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';
|
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||||
|
import { getEPersonEditRoute, getEPersonsRoute } from '../access-control-routing-paths';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-epeople-registry',
|
selector: 'ds-epeople-registry',
|
||||||
@@ -64,11 +65,6 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
|||||||
currentPage: 1
|
currentPage: 1
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether or not to show the EPerson form
|
|
||||||
*/
|
|
||||||
isEPersonFormShown: boolean;
|
|
||||||
|
|
||||||
// The search form
|
// The search form
|
||||||
searchForm;
|
searchForm;
|
||||||
|
|
||||||
@@ -114,17 +110,11 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
initialisePage() {
|
initialisePage() {
|
||||||
this.searching$.next(true);
|
this.searching$.next(true);
|
||||||
this.isEPersonFormShown = false;
|
|
||||||
this.search({scope: this.currentSearchScope, query: this.currentSearchQuery});
|
this.search({scope: this.currentSearchScope, query: this.currentSearchQuery});
|
||||||
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
|
|
||||||
if (eperson != null && eperson.id) {
|
|
||||||
this.isEPersonFormShown = true;
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
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: 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();
|
||||||
@@ -133,7 +123,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 {
|
||||||
@@ -160,14 +150,14 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
|||||||
const query: string = data.query;
|
const query: string = data.query;
|
||||||
const scope: string = data.scope;
|
const scope: string = data.scope;
|
||||||
if (query != null && this.currentSearchQuery !== query) {
|
if (query != null && this.currentSearchQuery !== query) {
|
||||||
this.router.navigate([this.epersonService.getEPeoplePageRouterLink()], {
|
void this.router.navigate([getEPersonsRoute()], {
|
||||||
queryParamsHandling: 'merge'
|
queryParamsHandling: 'merge'
|
||||||
});
|
});
|
||||||
this.currentSearchQuery = query;
|
this.currentSearchQuery = query;
|
||||||
this.paginationService.resetPage(this.config.id);
|
this.paginationService.resetPage(this.config.id);
|
||||||
}
|
}
|
||||||
if (scope != null && this.currentSearchScope !== scope) {
|
if (scope != null && this.currentSearchScope !== scope) {
|
||||||
this.router.navigate([this.epersonService.getEPeoplePageRouterLink()], {
|
void this.router.navigate([getEPersonsRoute()], {
|
||||||
queryParamsHandling: 'merge'
|
queryParamsHandling: 'merge'
|
||||||
});
|
});
|
||||||
this.currentSearchScope = scope;
|
this.currentSearchScope = scope;
|
||||||
@@ -205,23 +195,6 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
|||||||
return this.epersonService.getActiveEPerson();
|
return this.epersonService.getActiveEPerson();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Start editing the selected EPerson
|
|
||||||
* @param ePerson
|
|
||||||
*/
|
|
||||||
toggleEditEPerson(ePerson: EPerson) {
|
|
||||||
this.getActiveEPerson().pipe(take(1)).subscribe((activeEPerson: EPerson) => {
|
|
||||||
if (ePerson === activeEPerson) {
|
|
||||||
this.epersonService.cancelEditEPerson();
|
|
||||||
this.isEPersonFormShown = false;
|
|
||||||
} else {
|
|
||||||
this.epersonService.editEPerson(ePerson);
|
|
||||||
this.isEPersonFormShown = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.scrollToTop();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes EPerson, show notification on success/failure & updates EPeople list
|
* Deletes EPerson, show notification on success/failure & updates EPeople list
|
||||||
*/
|
*/
|
||||||
@@ -242,7 +215,7 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
|||||||
if (restResponse.hasSucceeded) {
|
if (restResponse.hasSucceeded) {
|
||||||
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', {name: this.dsoNameService.getName(ePerson)}));
|
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(this.translateService.get(this.labelPrefix + 'notification.deleted.success', { id: ePerson.id, statusCode: restResponse.statusCode, errorMessage: restResponse.errorMessage }));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -264,16 +237,6 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
|||||||
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollToTop() {
|
|
||||||
(function smoothscroll() {
|
|
||||||
const currentScroll = document.documentElement.scrollTop || document.body.scrollTop;
|
|
||||||
if (currentScroll > 0) {
|
|
||||||
window.requestAnimationFrame(smoothscroll);
|
|
||||||
window.scrollTo(0, currentScroll - (currentScroll / 8));
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset all input-fields to be empty and search all search
|
* Reset all input-fields to be empty and search all search
|
||||||
*/
|
*/
|
||||||
@@ -284,20 +247,7 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
|||||||
this.search({query: ''});
|
this.search({query: ''});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
getEditEPeoplePage(id: string): string {
|
||||||
* This method will set everything to stale, which will cause the lists on this page to update.
|
return getEPersonEditRoute(id);
|
||||||
*/
|
|
||||||
reset(): void {
|
|
||||||
this.epersonService.getBrowseEndpoint().pipe(
|
|
||||||
take(1),
|
|
||||||
switchMap((href: string) => {
|
|
||||||
return this.requestService.setStaleByHrefSubstring(href).pipe(
|
|
||||||
take(1),
|
|
||||||
);
|
|
||||||
})
|
|
||||||
).subscribe(()=>{
|
|
||||||
this.epersonService.cancelEditEPerson();
|
|
||||||
this.isEPersonFormShown = false;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,14 +1,18 @@
|
|||||||
<div *ngIf="epersonService.getActiveEPerson() | async; then editheader; else createHeader"></div>
|
<div class="container">
|
||||||
|
<div class="group-form row">
|
||||||
|
<div class="col-12">
|
||||||
|
|
||||||
<ng-template #createHeader>
|
<div *ngIf="epersonService.getActiveEPerson() | async; then editHeader; else createHeader"></div>
|
||||||
<h4>{{messagePrefix + '.create' | translate}}</h4>
|
|
||||||
</ng-template>
|
|
||||||
|
|
||||||
<ng-template #editheader>
|
<ng-template #createHeader>
|
||||||
<h4>{{messagePrefix + '.edit' | translate}}</h4>
|
<h2 class="border-bottom pb-2">{{messagePrefix + '.create' | translate}}</h2>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ds-form [formId]="formId"
|
<ng-template #editHeader>
|
||||||
|
<h2 class="border-bottom pb-2">{{messagePrefix + '.edit' | translate}}</h2>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<ds-form [formId]="formId"
|
||||||
[formModel]="formModel"
|
[formModel]="formModel"
|
||||||
[formGroup]="formGroup"
|
[formGroup]="formGroup"
|
||||||
[formLayout]="formLayout"
|
[formLayout]="formLayout"
|
||||||
@@ -16,30 +20,31 @@
|
|||||||
[submitLabel]="submitLabel"
|
[submitLabel]="submitLabel"
|
||||||
(submitForm)="onSubmit()">
|
(submitForm)="onSubmit()">
|
||||||
<div before class="btn-group">
|
<div before class="btn-group">
|
||||||
<button (click)="onCancel()"
|
<button (click)="onCancel()" type="button" class="btn btn-outline-secondary">
|
||||||
class="btn btn-outline-secondary"><i class="fas fa-arrow-left"></i> {{messagePrefix + '.return' | translate}}</button>
|
<i class="fas fa-arrow-left"></i> {{messagePrefix + '.return' | translate}}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="displayResetPassword" between class="btn-group">
|
<div *ngIf="displayResetPassword" between class="btn-group">
|
||||||
<button class="btn btn-primary" [disabled]="!(canReset$ | async)" (click)="resetPassword()">
|
<button class="btn btn-primary" [disabled]="!(canReset$ | async)" type="button" (click)="resetPassword()">
|
||||||
<i class="fa fa-key"></i> {{'admin.access-control.epeople.actions.reset' | translate}}
|
<i class="fa fa-key"></i> {{'admin.access-control.epeople.actions.reset' | translate}}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div between class="btn-group ml-1">
|
<div *ngIf="canImpersonate$ | async" between class="btn-group">
|
||||||
<button *ngIf="!isImpersonated" class="btn btn-primary" [ngClass]="{'d-none' : !(canImpersonate$ | async)}" (click)="impersonate()">
|
<button *ngIf="!isImpersonated" class="btn btn-primary" type="button" (click)="impersonate()">
|
||||||
<i class="fa fa-user-secret"></i> {{'admin.access-control.epeople.actions.impersonate' | translate}}
|
<i class="fa fa-user-secret"></i> {{'admin.access-control.epeople.actions.impersonate' | translate}}
|
||||||
</button>
|
</button>
|
||||||
<button *ngIf="isImpersonated" class="btn btn-primary" (click)="stopImpersonating()">
|
<button *ngIf="isImpersonated" class="btn btn-primary" type="button" (click)="stopImpersonating()">
|
||||||
<i class="fa fa-user-secret"></i> {{'admin.access-control.epeople.actions.stop-impersonating' | translate}}
|
<i class="fa fa-user-secret"></i> {{'admin.access-control.epeople.actions.stop-impersonating' | translate}}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button after class="btn btn-danger delete-button" [disabled]="!(canDelete$ | async)" (click)="delete()">
|
<button *ngIf="canDelete$ | async" after class="btn btn-danger delete-button" type="button" (click)="delete()">
|
||||||
<i class="fas fa-trash"></i> {{'admin.access-control.epeople.actions.delete' | translate}}
|
<i class="fas fa-trash"></i> {{'admin.access-control.epeople.actions.delete' | translate}}
|
||||||
</button>
|
</button>
|
||||||
</ds-form>
|
</ds-form>
|
||||||
|
|
||||||
<ds-themed-loading [showMessage]="false" *ngIf="!formGroup"></ds-themed-loading>
|
<ds-themed-loading [showMessage]="false" *ngIf="!formGroup"></ds-themed-loading>
|
||||||
|
|
||||||
<div *ngIf="epersonService.getActiveEPerson() | async">
|
<div *ngIf="epersonService.getActiveEPerson() | async">
|
||||||
<h5>{{messagePrefix + '.groupsEPersonIsMemberOf' | translate}}</h5>
|
<h5>{{messagePrefix + '.groupsEPersonIsMemberOf' | translate}}</h5>
|
||||||
|
|
||||||
<ds-themed-loading [showMessage]="false" *ngIf="!(groups | async)"></ds-themed-loading>
|
<ds-themed-loading [showMessage]="false" *ngIf="!(groups | async)"></ds-themed-loading>
|
||||||
@@ -86,4 +91,7 @@
|
|||||||
class="btn btn-primary">{{messagePrefix + '.goToGroups' | translate}}</button>
|
class="btn btn-primary">{{messagePrefix + '.goToGroups' | translate}}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -31,6 +31,10 @@ import { PaginationServiceStub } from '../../../shared/testing/pagination-servic
|
|||||||
import { FindListOptions } from '../../../core/data/find-list-options.model';
|
import { FindListOptions } from '../../../core/data/find-list-options.model';
|
||||||
import { ValidateEmailNotTaken } from './validators/email-taken.validator';
|
import { ValidateEmailNotTaken } from './validators/email-taken.validator';
|
||||||
import { EpersonRegistrationService } from '../../../core/data/eperson-registration.service';
|
import { EpersonRegistrationService } from '../../../core/data/eperson-registration.service';
|
||||||
|
import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { RouterStub } from '../../../shared/testing/router.stub';
|
||||||
|
import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub';
|
||||||
|
|
||||||
describe('EPersonFormComponent', () => {
|
describe('EPersonFormComponent', () => {
|
||||||
let component: EPersonFormComponent;
|
let component: EPersonFormComponent;
|
||||||
@@ -43,6 +47,8 @@ describe('EPersonFormComponent', () => {
|
|||||||
let authorizationService: AuthorizationDataService;
|
let authorizationService: AuthorizationDataService;
|
||||||
let groupsDataService: GroupDataService;
|
let groupsDataService: GroupDataService;
|
||||||
let epersonRegistrationService: EpersonRegistrationService;
|
let epersonRegistrationService: EpersonRegistrationService;
|
||||||
|
let route: ActivatedRouteStub;
|
||||||
|
let router: RouterStub;
|
||||||
|
|
||||||
let paginationService;
|
let paginationService;
|
||||||
|
|
||||||
@@ -106,6 +112,9 @@ describe('EPersonFormComponent', () => {
|
|||||||
},
|
},
|
||||||
getEPersonByEmail(email): Observable<RemoteData<EPerson>> {
|
getEPersonByEmail(email): Observable<RemoteData<EPerson>> {
|
||||||
return createSuccessfulRemoteDataObject$(null);
|
return createSuccessfulRemoteDataObject$(null);
|
||||||
|
},
|
||||||
|
findById(_id: string, _useCachedVersionIfAvailable = true, _reRequestOnStale = true, ..._linksToFollow: FollowLinkConfig<EPerson>[]): Observable<RemoteData<EPerson>> {
|
||||||
|
return createSuccessfulRemoteDataObject$(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
builderService = Object.assign(getMockFormBuilderService(),{
|
builderService = Object.assign(getMockFormBuilderService(),{
|
||||||
@@ -182,6 +191,8 @@ describe('EPersonFormComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
paginationService = new PaginationServiceStub();
|
paginationService = new PaginationServiceStub();
|
||||||
|
route = new ActivatedRouteStub();
|
||||||
|
router = new RouterStub();
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
||||||
TranslateModule.forRoot({
|
TranslateModule.forRoot({
|
||||||
@@ -202,6 +213,8 @@ describe('EPersonFormComponent', () => {
|
|||||||
{ provide: PaginationService, useValue: paginationService },
|
{ provide: PaginationService, useValue: paginationService },
|
||||||
{ provide: RequestService, useValue: jasmine.createSpyObj('requestService', ['removeByHrefSubstring'])},
|
{ provide: RequestService, useValue: jasmine.createSpyObj('requestService', ['removeByHrefSubstring'])},
|
||||||
{ provide: EpersonRegistrationService, useValue: epersonRegistrationService },
|
{ provide: EpersonRegistrationService, useValue: epersonRegistrationService },
|
||||||
|
{ provide: ActivatedRoute, useValue: route },
|
||||||
|
{ provide: Router, useValue: router },
|
||||||
EPeopleRegistryComponent
|
EPeopleRegistryComponent
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
@@ -263,24 +276,18 @@ describe('EPersonFormComponent', () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
describe('firstName, lastName and email should be required', () => {
|
describe('firstName, lastName and email should be required', () => {
|
||||||
it('form should be invalid because the firstName is required', waitForAsync(() => {
|
it('form should be invalid because the firstName is required', () => {
|
||||||
fixture.whenStable().then(() => {
|
|
||||||
expect(component.formGroup.controls.firstName.valid).toBeFalse();
|
expect(component.formGroup.controls.firstName.valid).toBeFalse();
|
||||||
expect(component.formGroup.controls.firstName.errors.required).toBeTrue();
|
expect(component.formGroup.controls.firstName.errors.required).toBeTrue();
|
||||||
});
|
});
|
||||||
}));
|
it('form should be invalid because the lastName is required', () => {
|
||||||
it('form should be invalid because the lastName is required', waitForAsync(() => {
|
|
||||||
fixture.whenStable().then(() => {
|
|
||||||
expect(component.formGroup.controls.lastName.valid).toBeFalse();
|
expect(component.formGroup.controls.lastName.valid).toBeFalse();
|
||||||
expect(component.formGroup.controls.lastName.errors.required).toBeTrue();
|
expect(component.formGroup.controls.lastName.errors.required).toBeTrue();
|
||||||
});
|
});
|
||||||
}));
|
it('form should be invalid because the email is required', () => {
|
||||||
it('form should be invalid because the email is required', waitForAsync(() => {
|
|
||||||
fixture.whenStable().then(() => {
|
|
||||||
expect(component.formGroup.controls.email.valid).toBeFalse();
|
expect(component.formGroup.controls.email.valid).toBeFalse();
|
||||||
expect(component.formGroup.controls.email.errors.required).toBeTrue();
|
expect(component.formGroup.controls.email.errors.required).toBeTrue();
|
||||||
});
|
});
|
||||||
}));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('after inserting information firstName,lastName and email not required', () => {
|
describe('after inserting information firstName,lastName and email not required', () => {
|
||||||
@@ -290,24 +297,18 @@ describe('EPersonFormComponent', () => {
|
|||||||
component.formGroup.controls.email.setValue('test@test.com');
|
component.formGroup.controls.email.setValue('test@test.com');
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
it('firstName should be valid because the firstName is set', waitForAsync(() => {
|
it('firstName should be valid because the firstName is set', () => {
|
||||||
fixture.whenStable().then(() => {
|
|
||||||
expect(component.formGroup.controls.firstName.valid).toBeTrue();
|
expect(component.formGroup.controls.firstName.valid).toBeTrue();
|
||||||
expect(component.formGroup.controls.firstName.errors).toBeNull();
|
expect(component.formGroup.controls.firstName.errors).toBeNull();
|
||||||
});
|
});
|
||||||
}));
|
it('lastName should be valid because the lastName is set', () => {
|
||||||
it('lastName should be valid because the lastName is set', waitForAsync(() => {
|
|
||||||
fixture.whenStable().then(() => {
|
|
||||||
expect(component.formGroup.controls.lastName.valid).toBeTrue();
|
expect(component.formGroup.controls.lastName.valid).toBeTrue();
|
||||||
expect(component.formGroup.controls.lastName.errors).toBeNull();
|
expect(component.formGroup.controls.lastName.errors).toBeNull();
|
||||||
});
|
});
|
||||||
}));
|
it('email should be valid because the email is set', () => {
|
||||||
it('email should be valid because the email is set', waitForAsync(() => {
|
|
||||||
fixture.whenStable().then(() => {
|
|
||||||
expect(component.formGroup.controls.email.valid).toBeTrue();
|
expect(component.formGroup.controls.email.valid).toBeTrue();
|
||||||
expect(component.formGroup.controls.email.errors).toBeNull();
|
expect(component.formGroup.controls.email.errors).toBeNull();
|
||||||
});
|
});
|
||||||
}));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -316,12 +317,10 @@ describe('EPersonFormComponent', () => {
|
|||||||
component.formGroup.controls.email.setValue('test@test');
|
component.formGroup.controls.email.setValue('test@test');
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
it('email should not be valid because the email pattern', waitForAsync(() => {
|
it('email should not be valid because the email pattern', () => {
|
||||||
fixture.whenStable().then(() => {
|
|
||||||
expect(component.formGroup.controls.email.valid).toBeFalse();
|
expect(component.formGroup.controls.email.valid).toBeFalse();
|
||||||
expect(component.formGroup.controls.email.errors.pattern).toBeTruthy();
|
expect(component.formGroup.controls.email.errors.pattern).toBeTruthy();
|
||||||
});
|
});
|
||||||
}));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('after already utilized email', () => {
|
describe('after already utilized email', () => {
|
||||||
@@ -336,12 +335,10 @@ describe('EPersonFormComponent', () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('email should not be valid because email is already taken', waitForAsync(() => {
|
it('email should not be valid because email is already taken', () => {
|
||||||
fixture.whenStable().then(() => {
|
|
||||||
expect(component.formGroup.controls.email.valid).toBeFalse();
|
expect(component.formGroup.controls.email.valid).toBeFalse();
|
||||||
expect(component.formGroup.controls.email.errors.emailTaken).toBeTruthy();
|
expect(component.formGroup.controls.email.errors.emailTaken).toBeTruthy();
|
||||||
});
|
});
|
||||||
}));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -393,11 +390,9 @@ describe('EPersonFormComponent', () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should emit a new eperson using the correct values', waitForAsync(() => {
|
it('should emit a new eperson using the correct values', () => {
|
||||||
fixture.whenStable().then(() => {
|
|
||||||
expect(component.submitForm.emit).toHaveBeenCalledWith(expected);
|
expect(component.submitForm.emit).toHaveBeenCalledWith(expected);
|
||||||
});
|
});
|
||||||
}));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('with an active eperson', () => {
|
describe('with an active eperson', () => {
|
||||||
@@ -428,11 +423,9 @@ describe('EPersonFormComponent', () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should emit the existing eperson using the correct values', waitForAsync(() => {
|
it('should emit the existing eperson using the correct values', () => {
|
||||||
fixture.whenStable().then(() => {
|
|
||||||
expect(component.submitForm.emit).toHaveBeenCalledWith(expectedWithId);
|
expect(component.submitForm.emit).toHaveBeenCalledWith(expectedWithId);
|
||||||
});
|
});
|
||||||
}));
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -491,16 +484,16 @@ describe('EPersonFormComponent', () => {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('the delete button should be active if the eperson can be deleted', () => {
|
it('the delete button should be visible if the ePerson can be deleted', () => {
|
||||||
const deleteButton = fixture.debugElement.query(By.css('.delete-button'));
|
const deleteButton = fixture.debugElement.query(By.css('.delete-button'));
|
||||||
expect(deleteButton.nativeElement.disabled).toBe(false);
|
expect(deleteButton).not.toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('the delete button should be disabled if the eperson cannot be deleted', () => {
|
it('the delete button should be hidden if the ePerson cannot be deleted', () => {
|
||||||
component.canDelete$ = observableOf(false);
|
component.canDelete$ = observableOf(false);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
const deleteButton = fixture.debugElement.query(By.css('.delete-button'));
|
const deleteButton = fixture.debugElement.query(By.css('.delete-button'));
|
||||||
expect(deleteButton.nativeElement.disabled).toBe(true);
|
expect(deleteButton).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call the epersonFormComponent delete when clicked on the button', () => {
|
it('should call the epersonFormComponent delete when clicked on the button', () => {
|
||||||
|
@@ -38,6 +38,8 @@ 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';
|
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { getEPersonsRoute } from '../../access-control-routing-paths';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-eperson-form',
|
selector: 'ds-eperson-form',
|
||||||
@@ -194,6 +196,8 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
public requestService: RequestService,
|
public requestService: RequestService,
|
||||||
private epersonRegistrationService: EpersonRegistrationService,
|
private epersonRegistrationService: EpersonRegistrationService,
|
||||||
public dsoNameService: DSONameService,
|
public dsoNameService: DSONameService,
|
||||||
|
protected route: ActivatedRoute,
|
||||||
|
protected router: Router,
|
||||||
) {
|
) {
|
||||||
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
|
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
|
||||||
this.epersonInitial = eperson;
|
this.epersonInitial = eperson;
|
||||||
@@ -213,7 +217,9 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
* This method will initialise the page
|
* This method will initialise the page
|
||||||
*/
|
*/
|
||||||
initialisePage() {
|
initialisePage() {
|
||||||
|
this.subs.push(this.epersonService.findById(this.route.snapshot.params.id).subscribe((ePersonRD: RemoteData<EPerson>) => {
|
||||||
|
this.epersonService.editEPerson(ePersonRD.payload);
|
||||||
|
}));
|
||||||
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`),
|
||||||
@@ -339,6 +345,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
onCancel() {
|
onCancel() {
|
||||||
this.epersonService.cancelEditEPerson();
|
this.epersonService.cancelEditEPerson();
|
||||||
this.cancelForm.emit();
|
this.cancelForm.emit();
|
||||||
|
void this.router.navigate([getEPersonsRoute()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -390,6 +397,8 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
if (rd.hasSucceeded) {
|
if (rd.hasSucceeded) {
|
||||||
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.created.success', { name: this.dsoNameService.getName(ePersonToCreate) }));
|
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.created.success', { name: this.dsoNameService.getName(ePersonToCreate) }));
|
||||||
this.submitForm.emit(ePersonToCreate);
|
this.submitForm.emit(ePersonToCreate);
|
||||||
|
this.epersonService.clearEPersonRequests();
|
||||||
|
void this.router.navigateByUrl(getEPersonsRoute());
|
||||||
} else {
|
} else {
|
||||||
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.created.failure', { name: this.dsoNameService.getName(ePersonToCreate) }));
|
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.created.failure', { name: this.dsoNameService.getName(ePersonToCreate) }));
|
||||||
this.cancelForm.emit();
|
this.cancelForm.emit();
|
||||||
@@ -429,6 +438,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
if (rd.hasSucceeded) {
|
if (rd.hasSucceeded) {
|
||||||
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.edited.success', { name: this.dsoNameService.getName(editedEperson) }));
|
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.edited.success', { name: this.dsoNameService.getName(editedEperson) }));
|
||||||
this.submitForm.emit(editedEperson);
|
this.submitForm.emit(editedEperson);
|
||||||
|
void this.router.navigateByUrl(getEPersonsRoute());
|
||||||
} else {
|
} else {
|
||||||
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.edited.failure', { name: this.dsoNameService.getName(editedEperson) }));
|
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.edited.failure', { name: this.dsoNameService.getName(editedEperson) }));
|
||||||
this.cancelForm.emit();
|
this.cancelForm.emit();
|
||||||
@@ -495,6 +505,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
).subscribe(({ restResponse, eperson }: { restResponse: RemoteData<NoContent> | null, eperson: EPerson }) => {
|
).subscribe(({ restResponse, eperson }: { restResponse: RemoteData<NoContent> | null, eperson: EPerson }) => {
|
||||||
if (restResponse?.hasSucceeded) {
|
if (restResponse?.hasSucceeded) {
|
||||||
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', { name: this.dsoNameService.getName(eperson) }));
|
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', { name: this.dsoNameService.getName(eperson) }));
|
||||||
|
void this.router.navigate([getEPersonsRoute()]);
|
||||||
} else {
|
} else {
|
||||||
this.notificationsService.error(`Error occurred when trying to delete EPerson with id: ${eperson?.id} with code: ${restResponse?.statusCode} and message: ${restResponse?.errorMessage}`);
|
this.notificationsService.error(`Error occurred when trying to delete EPerson with id: ${eperson?.id} with code: ${restResponse?.statusCode} and message: ${restResponse?.errorMessage}`);
|
||||||
}
|
}
|
||||||
@@ -541,16 +552,6 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* This method will ensure that the page gets reset and that the cache is cleared
|
|
||||||
*/
|
|
||||||
reset() {
|
|
||||||
this.epersonService.getActiveEPerson().pipe(take(1)).subscribe((eperson: EPerson) => {
|
|
||||||
this.requestService.removeByHrefSubstring(eperson.self);
|
|
||||||
});
|
|
||||||
this.initialisePage();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks for the given ePerson if there is already an ePerson in the system with that email
|
* Checks for the given ePerson if there is already an ePerson in the system with that email
|
||||||
* and shows notification if this is the case
|
* and shows notification if this is the case
|
||||||
|
@@ -0,0 +1,53 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { EPerson } from '../../core/eperson/models/eperson.model';
|
||||||
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
|
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
|
||||||
|
import { ResolvedAction } from '../../core/resolving/resolver.actions';
|
||||||
|
import { EPersonDataService } from '../../core/eperson/eperson-data.service';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
|
|
||||||
|
export const EPERSON_EDIT_FOLLOW_LINKS: FollowLinkConfig<EPerson>[] = [
|
||||||
|
followLink('groups'),
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class represents a resolver that requests a specific {@link EPerson} before the route is activated
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class EPersonResolver implements Resolve<RemoteData<EPerson>> {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected ePersonService: EPersonDataService,
|
||||||
|
protected store: Store<any>,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method for resolving a {@link EPerson} based on the parameters in the current route
|
||||||
|
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
|
||||||
|
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
|
||||||
|
* @returns `Observable<<RemoteData<EPerson>>` Emits the found {@link EPerson} based on the parameters in the current
|
||||||
|
* route, or an error if something went wrong
|
||||||
|
*/
|
||||||
|
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<EPerson>> {
|
||||||
|
const ePersonRD$: Observable<RemoteData<EPerson>> = this.ePersonService.findById(route.params.id,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
...EPERSON_EDIT_FOLLOW_LINKS,
|
||||||
|
).pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
);
|
||||||
|
|
||||||
|
ePersonRD$.subscribe((ePersonRD: RemoteData<EPerson>) => {
|
||||||
|
this.store.dispatch(new ResolvedAction(state.url, ePersonRD.payload));
|
||||||
|
});
|
||||||
|
|
||||||
|
return ePersonRD$;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -2,13 +2,13 @@
|
|||||||
<div class="group-form row">
|
<div class="group-form row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
|
|
||||||
<div *ngIf="groupDataService.getActiveGroup() | async; then editheader; else createHeader"></div>
|
<div *ngIf="groupDataService.getActiveGroup() | async; then editHeader; else createHeader"></div>
|
||||||
|
|
||||||
<ng-template #createHeader>
|
<ng-template #createHeader>
|
||||||
<h2 class="border-bottom pb-2">{{messagePrefix + '.head.create' | translate}}</h2>
|
<h2 class="border-bottom pb-2">{{messagePrefix + '.head.create' | translate}}</h2>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ng-template #editheader>
|
<ng-template #editHeader>
|
||||||
<h2 class="border-bottom pb-2">
|
<h2 class="border-bottom pb-2">
|
||||||
<span
|
<span
|
||||||
*dsContextHelp="{
|
*dsContextHelp="{
|
||||||
@@ -36,12 +36,12 @@
|
|||||||
[displayCancel]="false"
|
[displayCancel]="false"
|
||||||
(submitForm)="onSubmit()">
|
(submitForm)="onSubmit()">
|
||||||
<div before class="btn-group">
|
<div before class="btn-group">
|
||||||
<button (click)="onCancel()"
|
<button (click)="onCancel()" type="button"
|
||||||
class="btn btn-outline-secondary"><i class="fas fa-arrow-left"></i> {{messagePrefix + '.return' | translate}}</button>
|
class="btn btn-outline-secondary"><i class="fas fa-arrow-left"></i> {{messagePrefix + '.return' | translate}}</button>
|
||||||
</div>
|
</div>
|
||||||
<div after *ngIf="groupBeingEdited != null" class="btn-group">
|
<div after *ngIf="(canEdit$ | async) && !groupBeingEdited.permanent" class="btn-group">
|
||||||
<button class="btn btn-danger delete-button" [disabled]="!(canEdit$ | async) || groupBeingEdited.permanent"
|
<button class="btn btn-danger delete-button" [disabled]="!(canEdit$ | async) || groupBeingEdited.permanent"
|
||||||
(click)="delete()">
|
(click)="delete()" type="button">
|
||||||
<i class="fa fa-trash"></i> {{ messagePrefix + '.actions.delete' | translate}}
|
<i class="fa fa-trash"></i> {{ messagePrefix + '.actions.delete' | translate}}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -10,7 +10,6 @@ import {
|
|||||||
} from '@ng-dynamic-forms/core';
|
} from '@ng-dynamic-forms/core';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import {
|
import {
|
||||||
ObservedValueOf,
|
|
||||||
combineLatest as observableCombineLatest,
|
combineLatest as observableCombineLatest,
|
||||||
Observable,
|
Observable,
|
||||||
of as observableOf,
|
of as observableOf,
|
||||||
@@ -37,7 +36,7 @@ import {
|
|||||||
getFirstCompletedRemoteData,
|
getFirstCompletedRemoteData,
|
||||||
getFirstSucceededRemoteDataPayload
|
getFirstSucceededRemoteDataPayload
|
||||||
} from '../../../core/shared/operators';
|
} from '../../../core/shared/operators';
|
||||||
import { AlertType } from '../../../shared/alert/aletr-type';
|
import { AlertType } from '../../../shared/alert/alert-type';
|
||||||
import { ConfirmationModalComponent } from '../../../shared/confirmation-modal/confirmation-modal.component';
|
import { ConfirmationModalComponent } from '../../../shared/confirmation-modal/confirmation-modal.component';
|
||||||
import { hasValue, isNotEmpty, hasValueOperator } from '../../../shared/empty.util';
|
import { hasValue, isNotEmpty, hasValueOperator } from '../../../shared/empty.util';
|
||||||
import { FormBuilderService } from '../../../shared/form/builder/form-builder.service';
|
import { FormBuilderService } from '../../../shared/form/builder/form-builder.service';
|
||||||
@@ -48,6 +47,7 @@ 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 { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
|
||||||
import { environment } from '../../../../environments/environment';
|
import { environment } from '../../../../environments/environment';
|
||||||
|
import { getGroupEditRoute, getGroupsRoute } from '../../access-control-routing-paths';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-group-form',
|
selector: 'ds-group-form',
|
||||||
@@ -165,19 +165,19 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
this.canEdit$ = this.groupDataService.getActiveGroup().pipe(
|
this.canEdit$ = this.groupDataService.getActiveGroup().pipe(
|
||||||
hasValueOperator(),
|
hasValueOperator(),
|
||||||
switchMap((group: Group) => {
|
switchMap((group: Group) => {
|
||||||
return observableCombineLatest(
|
return observableCombineLatest([
|
||||||
this.authorizationService.isAuthorized(FeatureID.CanDelete, isNotEmpty(group) ? group.self : undefined),
|
this.authorizationService.isAuthorized(FeatureID.CanDelete, isNotEmpty(group) ? group.self : undefined),
|
||||||
this.hasLinkedDSO(group),
|
this.hasLinkedDSO(group),
|
||||||
(isAuthorized: ObservedValueOf<Observable<boolean>>, hasLinkedDSO: ObservedValueOf<Observable<boolean>>) => {
|
]).pipe(
|
||||||
return isAuthorized && !hasLinkedDSO;
|
map(([isAuthorized, hasLinkedDSO]: [boolean, boolean]) => isAuthorized && !hasLinkedDSO),
|
||||||
});
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
observableCombineLatest(
|
}),
|
||||||
|
);
|
||||||
|
observableCombineLatest([
|
||||||
this.translateService.get(`${this.messagePrefix}.groupName`),
|
this.translateService.get(`${this.messagePrefix}.groupName`),
|
||||||
this.translateService.get(`${this.messagePrefix}.groupCommunity`),
|
this.translateService.get(`${this.messagePrefix}.groupCommunity`),
|
||||||
this.translateService.get(`${this.messagePrefix}.groupDescription`)
|
this.translateService.get(`${this.messagePrefix}.groupDescription`)
|
||||||
).subscribe(([groupName, groupCommunity, groupDescription]) => {
|
]).subscribe(([groupName, groupCommunity, groupDescription]) => {
|
||||||
this.groupName = new DynamicInputModel({
|
this.groupName = new DynamicInputModel({
|
||||||
id: 'groupName',
|
id: 'groupName',
|
||||||
label: groupName,
|
label: groupName,
|
||||||
@@ -215,12 +215,12 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.subs.push(
|
this.subs.push(
|
||||||
observableCombineLatest(
|
observableCombineLatest([
|
||||||
this.groupDataService.getActiveGroup(),
|
this.groupDataService.getActiveGroup(),
|
||||||
this.canEdit$,
|
this.canEdit$,
|
||||||
this.groupDataService.getActiveGroup()
|
this.groupDataService.getActiveGroup()
|
||||||
.pipe(filter((activeGroup) => hasValue(activeGroup)),switchMap((activeGroup) => this.getLinkedDSO(activeGroup).pipe(getFirstSucceededRemoteDataPayload())))
|
.pipe(filter((activeGroup) => hasValue(activeGroup)),switchMap((activeGroup) => this.getLinkedDSO(activeGroup).pipe(getFirstSucceededRemoteDataPayload())))
|
||||||
).subscribe(([activeGroup, canEdit, linkedObject]) => {
|
]).subscribe(([activeGroup, canEdit, linkedObject]) => {
|
||||||
|
|
||||||
if (activeGroup != null) {
|
if (activeGroup != null) {
|
||||||
|
|
||||||
@@ -263,7 +263,7 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
onCancel() {
|
onCancel() {
|
||||||
this.groupDataService.cancelEditGroup();
|
this.groupDataService.cancelEditGroup();
|
||||||
this.cancelForm.emit();
|
this.cancelForm.emit();
|
||||||
this.router.navigate([this.groupDataService.getGroupRegistryRouterLink()]);
|
void this.router.navigate([getGroupsRoute()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -310,7 +310,7 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
const groupSelfLink = rd.payload._links.self.href;
|
const groupSelfLink = rd.payload._links.self.href;
|
||||||
this.setActiveGroupWithLink(groupSelfLink);
|
this.setActiveGroupWithLink(groupSelfLink);
|
||||||
this.groupDataService.clearGroupsRequests();
|
this.groupDataService.clearGroupsRequests();
|
||||||
this.router.navigateByUrl(this.groupDataService.getGroupEditPageRouterLinkWithID(rd.payload.uuid));
|
void this.router.navigateByUrl(getGroupEditRoute(rd.payload.uuid));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.created.failure', { name: groupToCreate.name }));
|
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.created.failure', { name: groupToCreate.name }));
|
||||||
|
@@ -5,7 +5,7 @@
|
|||||||
<h2 id="header" class="pb-2">{{messagePrefix + 'head' | translate}}</h2>
|
<h2 id="header" class="pb-2">{{messagePrefix + 'head' | translate}}</h2>
|
||||||
<div>
|
<div>
|
||||||
<button class="mr-auto btn btn-success"
|
<button class="mr-auto btn btn-success"
|
||||||
[routerLink]="['newGroup']">
|
[routerLink]="'create'">
|
||||||
<i class="fas fa-plus"></i>
|
<i class="fas fa-plus"></i>
|
||||||
<span class="d-none d-sm-inline ml-1">{{messagePrefix + 'button.add' | translate}}</span>
|
<span class="d-none d-sm-inline ml-1">{{messagePrefix + 'button.add' | translate}}</span>
|
||||||
</button>
|
</button>
|
||||||
|
@@ -8,9 +8,9 @@ import {
|
|||||||
import { UntypedFormGroup } 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 { switchMap, take, tap } from 'rxjs/operators';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { combineLatest } from 'rxjs';
|
import { Observable, combineLatest } from 'rxjs';
|
||||||
import { MetadataSchema } from '../../../../core/metadata/metadata-schema.model';
|
import { MetadataSchema } from '../../../../core/metadata/metadata-schema.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -147,30 +147,48 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
|
|||||||
* Emit the updated/created schema using the EventEmitter submitForm
|
* Emit the updated/created schema using the EventEmitter submitForm
|
||||||
*/
|
*/
|
||||||
onSubmit(): void {
|
onSubmit(): void {
|
||||||
this.registryService.clearMetadataSchemaRequests().subscribe();
|
this.registryService
|
||||||
this.registryService.getActiveMetadataSchema().pipe(take(1)).subscribe(
|
.getActiveMetadataSchema()
|
||||||
(schema: MetadataSchema) => {
|
.pipe(
|
||||||
const values = {
|
take(1),
|
||||||
|
switchMap((schema: MetadataSchema) => {
|
||||||
|
const metadataValues = {
|
||||||
prefix: this.name.value,
|
prefix: this.name.value,
|
||||||
namespace: this.namespace.value
|
namespace: this.namespace.value,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let createOrUpdate$: Observable<MetadataSchema>;
|
||||||
|
|
||||||
if (schema == null) {
|
if (schema == null) {
|
||||||
this.registryService.createOrUpdateMetadataSchema(Object.assign(new MetadataSchema(), values)).subscribe((newSchema) => {
|
createOrUpdate$ =
|
||||||
this.submitForm.emit(newSchema);
|
this.registryService.createOrUpdateMetadataSchema(
|
||||||
});
|
Object.assign(new MetadataSchema(), metadataValues)
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
this.registryService.createOrUpdateMetadataSchema(Object.assign(new MetadataSchema(), schema, {
|
const updatedSchema = Object.assign(
|
||||||
id: schema.id,
|
new MetadataSchema(),
|
||||||
prefix: schema.prefix,
|
schema,
|
||||||
namespace: values.namespace,
|
{
|
||||||
})).subscribe((updatedSchema: MetadataSchema) => {
|
namespace: metadataValues.namespace,
|
||||||
this.submitForm.emit(updatedSchema);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.clearFields();
|
|
||||||
this.registryService.cancelEditMetadataSchema();
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
createOrUpdate$ =
|
||||||
|
this.registryService.createOrUpdateMetadataSchema(
|
||||||
|
updatedSchema
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return createOrUpdate$;
|
||||||
|
}),
|
||||||
|
tap(() => {
|
||||||
|
this.registryService.clearMetadataSchemaRequests().subscribe();
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.subscribe((updatedOrCreatedSchema: MetadataSchema) => {
|
||||||
|
this.submitForm.emit(updatedOrCreatedSchema);
|
||||||
|
this.clearFields();
|
||||||
|
this.registryService.cancelEditMetadataSchema();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -3,7 +3,8 @@ import {
|
|||||||
DynamicFormControlModel,
|
DynamicFormControlModel,
|
||||||
DynamicFormGroupModel,
|
DynamicFormGroupModel,
|
||||||
DynamicFormLayout,
|
DynamicFormLayout,
|
||||||
DynamicInputModel
|
DynamicInputModel,
|
||||||
|
DynamicTextAreaModel
|
||||||
} from '@ng-dynamic-forms/core';
|
} from '@ng-dynamic-forms/core';
|
||||||
import { UntypedFormGroup } from '@angular/forms';
|
import { UntypedFormGroup } from '@angular/forms';
|
||||||
import { RegistryService } from '../../../../core/registry/registry.service';
|
import { RegistryService } from '../../../../core/registry/registry.service';
|
||||||
@@ -51,7 +52,7 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy {
|
|||||||
/**
|
/**
|
||||||
* A dynamic input model for the scopeNote field
|
* A dynamic input model for the scopeNote field
|
||||||
*/
|
*/
|
||||||
scopeNote: DynamicInputModel;
|
scopeNote: DynamicTextAreaModel;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A list of all dynamic input models
|
* A list of all dynamic input models
|
||||||
@@ -132,11 +133,12 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy {
|
|||||||
maxLength: 'error.validation.metadata.qualifier.max-length',
|
maxLength: 'error.validation.metadata.qualifier.max-length',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
this.scopeNote = new DynamicInputModel({
|
this.scopeNote = new DynamicTextAreaModel({
|
||||||
id: 'scopeNote',
|
id: 'scopeNote',
|
||||||
label: scopenote,
|
label: scopenote,
|
||||||
name: 'scopeNote',
|
name: 'scopeNote',
|
||||||
required: false,
|
required: false,
|
||||||
|
rows: 5,
|
||||||
});
|
});
|
||||||
this.formModel = [
|
this.formModel = [
|
||||||
new DynamicFormGroupModel(
|
new DynamicFormGroupModel(
|
||||||
|
@@ -41,7 +41,7 @@
|
|||||||
</label>
|
</label>
|
||||||
</td>
|
</td>
|
||||||
<td class="selectable-row" (click)="editField(field)">{{field.id}}</td>
|
<td class="selectable-row" (click)="editField(field)">{{field.id}}</td>
|
||||||
<td class="selectable-row" (click)="editField(field)">{{schema?.prefix}}.{{field.element}}<label *ngIf="field.qualifier" class="mb-0">.</label>{{field.qualifier}}</td>
|
<td class="selectable-row" (click)="editField(field)">{{schema?.prefix}}.{{field.element}}{{field.qualifier ? '.' + field.qualifier : ''}}</td>
|
||||||
<td class="selectable-row" (click)="editField(field)">{{field.scopeNote}}</td>
|
<td class="selectable-row" (click)="editField(field)">{{field.scopeNote}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@@ -12,8 +12,7 @@ import { Router } from '@angular/router';
|
|||||||
* Represents a non-expandable section in the admin sidebar
|
* Represents a non-expandable section in the admin sidebar
|
||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
/* eslint-disable @angular-eslint/component-selector */
|
selector: 'ds-admin-sidebar-section',
|
||||||
selector: 'li[ds-admin-sidebar-section]',
|
|
||||||
templateUrl: './admin-sidebar-section.component.html',
|
templateUrl: './admin-sidebar-section.component.html',
|
||||||
styleUrls: ['./admin-sidebar-section.component.scss'],
|
styleUrls: ['./admin-sidebar-section.component.scss'],
|
||||||
|
|
||||||
|
@@ -26,10 +26,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<ng-container *ngFor="let section of (sections | async)">
|
<li *ngFor="let section of (sections | async)">
|
||||||
<ng-container
|
<ng-container
|
||||||
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
|
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
|
||||||
</ng-container>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="navbar-nav">
|
<div class="navbar-nav">
|
||||||
|
@@ -15,8 +15,7 @@ import { Router } from '@angular/router';
|
|||||||
* Represents a expandable section in the sidebar
|
* Represents a expandable section in the sidebar
|
||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
/* eslint-disable @angular-eslint/component-selector */
|
selector: 'ds-expandable-admin-sidebar-section',
|
||||||
selector: 'li[ds-expandable-admin-sidebar-section]',
|
|
||||||
templateUrl: './expandable-admin-sidebar-section.component.html',
|
templateUrl: './expandable-admin-sidebar-section.component.html',
|
||||||
styleUrls: ['./expandable-admin-sidebar-section.component.scss'],
|
styleUrls: ['./expandable-admin-sidebar-section.component.scss'],
|
||||||
animations: [rotate, slide, bgColor]
|
animations: [rotate, slide, bgColor]
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
<ng-container *ngVar="(breadcrumbs$ | async) as breadcrumbs">
|
<ng-container *ngVar="(breadcrumbs$ | async) as breadcrumbs">
|
||||||
<nav *ngIf="(showBreadcrumbs$ | async)" aria-label="breadcrumb" class="nav-breadcrumb">
|
<nav *ngIf="(showBreadcrumbs$ | async)" aria-label="breadcrumb" class="nav-breadcrumb">
|
||||||
<ol class="container breadcrumb">
|
<ol class="container breadcrumb my-0">
|
||||||
<ng-container
|
<ng-container
|
||||||
*ngTemplateOutlet="breadcrumbs?.length > 0 ? breadcrumb : activeBreadcrumb; context: {text: 'home.breadcrumbs', url: '/'}"></ng-container>
|
*ngTemplateOutlet="breadcrumbs?.length > 0 ? breadcrumb : activeBreadcrumb; context: {text: 'home.breadcrumbs', url: '/'}"></ng-container>
|
||||||
<ng-container *ngFor="let bc of breadcrumbs; let last = last;">
|
<ng-container *ngFor="let bc of breadcrumbs; let last = last;">
|
||||||
|
@@ -4,9 +4,8 @@
|
|||||||
|
|
||||||
.breadcrumb {
|
.breadcrumb {
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
margin-top: calc(-1 * var(--ds-content-spacing));
|
padding-bottom: calc(var(--ds-content-spacing) / 2);
|
||||||
padding-bottom: var(--ds-content-spacing / 3);
|
padding-top: calc(var(--ds-content-spacing) / 2);
|
||||||
padding-top: var(--ds-content-spacing / 3);
|
|
||||||
background-color: var(--ds-breadcrumb-bg);
|
background-color: var(--ds-breadcrumb-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -89,11 +89,11 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent {
|
|||||||
const lastItemRD = this.browseService.getFirstItemFor(definition, scope, SortDirection.DESC);
|
const lastItemRD = this.browseService.getFirstItemFor(definition, scope, SortDirection.DESC);
|
||||||
this.subs.push(
|
this.subs.push(
|
||||||
observableCombineLatest([firstItemRD, lastItemRD]).subscribe(([firstItem, lastItem]) => {
|
observableCombineLatest([firstItemRD, lastItemRD]).subscribe(([firstItem, lastItem]) => {
|
||||||
let lowerLimit = this.getLimit(firstItem, metadataKeys, this.appConfig.browseBy.defaultLowerLimit);
|
let lowerLimit: number = this.getLimit(firstItem, metadataKeys, this.appConfig.browseBy.defaultLowerLimit);
|
||||||
let upperLimit = this.getLimit(lastItem, metadataKeys, new Date().getUTCFullYear());
|
let upperLimit: number = this.getLimit(lastItem, metadataKeys, new Date().getUTCFullYear());
|
||||||
const options = [];
|
const options: number[] = [];
|
||||||
const oneYearBreak = Math.floor((upperLimit - this.appConfig.browseBy.oneYearLimit) / 5) * 5;
|
const oneYearBreak: number = Math.floor((upperLimit - this.appConfig.browseBy.oneYearLimit) / 5) * 5;
|
||||||
const fiveYearBreak = Math.floor((upperLimit - this.appConfig.browseBy.fiveYearLimit) / 10) * 10;
|
const fiveYearBreak: number = Math.floor((upperLimit - this.appConfig.browseBy.fiveYearLimit) / 10) * 10;
|
||||||
if (lowerLimit <= fiveYearBreak) {
|
if (lowerLimit <= fiveYearBreak) {
|
||||||
lowerLimit -= 10;
|
lowerLimit -= 10;
|
||||||
} else if (lowerLimit <= oneYearBreak) {
|
} else if (lowerLimit <= oneYearBreak) {
|
||||||
@@ -101,7 +101,7 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent {
|
|||||||
} else {
|
} else {
|
||||||
lowerLimit -= 1;
|
lowerLimit -= 1;
|
||||||
}
|
}
|
||||||
let i = upperLimit;
|
let i: number = upperLimit;
|
||||||
while (i > lowerLimit) {
|
while (i > lowerLimit) {
|
||||||
options.push(i);
|
options.push(i);
|
||||||
if (i <= fiveYearBreak) {
|
if (i <= fiveYearBreak) {
|
||||||
|
@@ -32,7 +32,7 @@
|
|||||||
|
|
||||||
<section class="comcol-page-browse-section">
|
<section class="comcol-page-browse-section">
|
||||||
<div class="browse-by-metadata w-100">
|
<div class="browse-by-metadata w-100">
|
||||||
<ds-browse-by *ngIf="startsWithOptions" class="col-xs-12 w-100"
|
<ds-themed-browse-by *ngIf="startsWithOptions" class="col-xs-12 w-100"
|
||||||
title="{{'browse.title' | translate:
|
title="{{'browse.title' | translate:
|
||||||
{
|
{
|
||||||
collection: dsoNameService.getName((parent$ | async)?.payload),
|
collection: dsoNameService.getName((parent$ | async)?.payload),
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
[startsWithOptions]="startsWithOptions"
|
[startsWithOptions]="startsWithOptions"
|
||||||
(prev)="goPrev()"
|
(prev)="goPrev()"
|
||||||
(next)="goNext()">
|
(next)="goNext()">
|
||||||
</ds-browse-by>
|
</ds-themed-browse-by>
|
||||||
<ds-themed-loading *ngIf="!startsWithOptions" message="{{'loading.browse-by-page' | translate}}"></ds-themed-loading>
|
<ds-themed-loading *ngIf="!startsWithOptions" message="{{'loading.browse-by-page' | translate}}"></ds-themed-loading>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
@@ -161,6 +161,10 @@ export class BrowseByMetadataPageComponent implements OnInit, OnDestroy {
|
|||||||
this.value = '';
|
this.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (params.startsWith === undefined || params.startsWith === '') {
|
||||||
|
this.startsWith = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof params.startsWith === 'string'){
|
if (typeof params.startsWith === 'string'){
|
||||||
this.startsWith = params.startsWith.trim();
|
this.startsWith = params.startsWith.trim();
|
||||||
}
|
}
|
||||||
|
@@ -14,6 +14,7 @@ import { ThemedBrowseByTaxonomyPageComponent } from './browse-by-taxonomy-page/t
|
|||||||
import { SharedBrowseByModule } from '../shared/browse-by/shared-browse-by.module';
|
import { SharedBrowseByModule } from '../shared/browse-by/shared-browse-by.module';
|
||||||
import { DsoPageModule } from '../shared/dso-page/dso-page.module';
|
import { DsoPageModule } from '../shared/dso-page/dso-page.module';
|
||||||
import { FormModule } from '../shared/form/form.module';
|
import { FormModule } from '../shared/form/form.module';
|
||||||
|
import { SharedModule } from '../shared/shared.module';
|
||||||
|
|
||||||
const ENTRY_COMPONENTS = [
|
const ENTRY_COMPONENTS = [
|
||||||
// put only entry components that use custom decorator
|
// put only entry components that use custom decorator
|
||||||
@@ -35,6 +36,7 @@ const ENTRY_COMPONENTS = [
|
|||||||
ComcolModule,
|
ComcolModule,
|
||||||
DsoPageModule,
|
DsoPageModule,
|
||||||
FormModule,
|
FormModule,
|
||||||
|
SharedModule,
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
BrowseBySwitcherComponent,
|
BrowseBySwitcherComponent,
|
||||||
|
@@ -98,9 +98,8 @@ export class CollectionFormComponent extends ComColFormComponent<Collection> imp
|
|||||||
// retrieve all entity types to populate the dropdowns selection
|
// retrieve all entity types to populate the dropdowns selection
|
||||||
entities$.subscribe((entityTypes: ItemType[]) => {
|
entities$.subscribe((entityTypes: ItemType[]) => {
|
||||||
|
|
||||||
entityTypes
|
entityTypes = entityTypes.filter((type: ItemType) => type.label !== NONE_ENTITY_TYPE);
|
||||||
.filter((type: ItemType) => type.label !== NONE_ENTITY_TYPE)
|
entityTypes.forEach((type: ItemType, index: number) => {
|
||||||
.forEach((type: ItemType, index: number) => {
|
|
||||||
this.entityTypeSelection.add({
|
this.entityTypeSelection.add({
|
||||||
disabled: false,
|
disabled: false,
|
||||||
label: type.label,
|
label: type.label,
|
||||||
@@ -112,7 +111,7 @@ export class CollectionFormComponent extends ComColFormComponent<Collection> imp
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.formModel = [...collectionFormModels, this.entityTypeSelection];
|
this.formModel = entityTypes.length === 0 ? collectionFormModels : [...collectionFormModels, this.entityTypeSelection];
|
||||||
|
|
||||||
super.ngOnInit();
|
super.ngOnInit();
|
||||||
this.chd.detectChanges();
|
this.chd.detectChanges();
|
||||||
|
@@ -34,9 +34,6 @@
|
|||||||
</ds-comcol-page-content>
|
</ds-comcol-page-content>
|
||||||
</header>
|
</header>
|
||||||
<ds-dso-edit-menu></ds-dso-edit-menu>
|
<ds-dso-edit-menu></ds-dso-edit-menu>
|
||||||
<div class="pl-2 space-children-mr">
|
|
||||||
<ds-dso-page-subscription-button [dso]="collection"></ds-dso-page-subscription-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<section class="comcol-page-browse-section">
|
<section class="comcol-page-browse-section">
|
||||||
<!-- Browse-By Links -->
|
<!-- Browse-By Links -->
|
||||||
|
@@ -8,7 +8,7 @@ import { ItemTemplateDataService } from '../../core/data/item-template-data.serv
|
|||||||
import { getCollectionEditRoute } from '../collection-page-routing-paths';
|
import { getCollectionEditRoute } from '../collection-page-routing-paths';
|
||||||
import { Item } from '../../core/shared/item.model';
|
import { Item } from '../../core/shared/item.model';
|
||||||
import { getFirstSucceededRemoteDataPayload } from '../../core/shared/operators';
|
import { getFirstSucceededRemoteDataPayload } from '../../core/shared/operators';
|
||||||
import { AlertType } from '../../shared/alert/aletr-type';
|
import { AlertType } from '../../shared/alert/alert-type';
|
||||||
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<h2>{{ 'communityList.title' | translate }}</h2>
|
<h1>{{ 'communityList.title' | translate }}</h1>
|
||||||
<ds-themed-community-list></ds-themed-community-list>
|
<ds-themed-community-list></ds-themed-community-list>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -25,7 +25,7 @@ import { ShowMoreFlatNode } from './show-more-flat-node.model';
|
|||||||
import { FindListOptions } from '../core/data/find-list-options.model';
|
import { FindListOptions } from '../core/data/find-list-options.model';
|
||||||
import { AppConfig, APP_CONFIG } from 'src/config/app-config.interface';
|
import { AppConfig, APP_CONFIG } from 'src/config/app-config.interface';
|
||||||
|
|
||||||
// Helper method to combine an flatten an array of observables of flatNode arrays
|
// Helper method to combine and flatten an array of observables of flatNode arrays
|
||||||
export const combineAndFlatten = (obsList: Observable<FlatNode[]>[]): Observable<FlatNode[]> =>
|
export const combineAndFlatten = (obsList: Observable<FlatNode[]>[]): Observable<FlatNode[]> =>
|
||||||
observableCombineLatest([...obsList]).pipe(
|
observableCombineLatest([...obsList]).pipe(
|
||||||
map((matrix: any[][]) => [].concat(...matrix)),
|
map((matrix: any[][]) => [].concat(...matrix)),
|
||||||
@@ -199,7 +199,7 @@ export class CommunityListService {
|
|||||||
* Transforms a community in a list of FlatNodes containing firstly a flatnode of the community itself,
|
* Transforms a community in a list of FlatNodes containing firstly a flatnode of the community itself,
|
||||||
* followed by flatNodes of its possible subcommunities and collection
|
* followed by flatNodes of its possible subcommunities and collection
|
||||||
* It gets called recursively for each subcommunity to add its subcommunities and collections to the list
|
* It gets called recursively for each subcommunity to add its subcommunities and collections to the list
|
||||||
* Number of subcommunities and collections added, is dependant on the current page the parent is at for respectively subcommunities and collections.
|
* Number of subcommunities and collections added, is dependent on the current page the parent is at for respectively subcommunities and collections.
|
||||||
* @param community Community being transformed
|
* @param community Community being transformed
|
||||||
* @param level Depth of the community in the list, subcommunities and collections go one level deeper
|
* @param level Depth of the community in the list, subcommunities and collections go one level deeper
|
||||||
* @param parent Flatnode of the parent community
|
* @param parent Flatnode of the parent community
|
||||||
@@ -275,7 +275,7 @@ export class CommunityListService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if a community has subcommunities or collections by querying the respective services with a pageSize = 0
|
* Checks if a community has subcommunities or collections by querying the respective services with a pageSize = 0
|
||||||
* Returns an observable that combines the result.payload.totalElements fo the observables that the
|
* Returns an observable that combines the result.payload.totalElements of the observables that the
|
||||||
* respective services return when queried
|
* respective services return when queried
|
||||||
* @param community Community being checked whether it is expandable (if it has subcommunities or collections)
|
* @param community Community being checked whether it is expandable (if it has subcommunities or collections)
|
||||||
*/
|
*/
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
<ds-themed-loading *ngIf="(dataSource.loading$ | async) && !loadingNode" class="ds-themed-loading"></ds-themed-loading>
|
<ds-themed-loading *ngIf="(dataSource.loading$ | async) && !loadingNode" class="ds-themed-loading"></ds-themed-loading>
|
||||||
<cdk-tree [dataSource]="dataSource" [treeControl]="treeControl">
|
<cdk-tree [dataSource]="dataSource" [treeControl]="treeControl" [trackBy]="trackBy">
|
||||||
<!-- This is the tree node template for show more node -->
|
<!-- This is the tree node template for show more node -->
|
||||||
<cdk-tree-node *cdkTreeNodeDef="let node; when: isShowMore" cdkTreeNodePadding
|
<cdk-tree-node *cdkTreeNodeDef="let node; when: isShowMore" cdkTreeNodePadding
|
||||||
class="example-tree-node show-more-node">
|
class="example-tree-node show-more-node">
|
||||||
@@ -34,13 +34,13 @@
|
|||||||
aria-hidden="true"></span>
|
aria-hidden="true"></span>
|
||||||
</button>
|
</button>
|
||||||
<div class="d-flex flex-row">
|
<div class="d-flex flex-row">
|
||||||
<h5 class="align-middle pt-2">
|
<span class="align-middle pt-2 lead">
|
||||||
<a [routerLink]="node.route" class="lead">
|
<a [routerLink]="node.route" class="lead">
|
||||||
{{ dsoNameService.getName(node.payload) }}
|
{{ dsoNameService.getName(node.payload) }}
|
||||||
</a>
|
</a>
|
||||||
<span class="pr-2"> </span>
|
<span class="pr-2"> </span>
|
||||||
<span *ngIf="node.payload.archivedItemsCount >= 0" class="badge badge-pill badge-secondary align-top archived-items-lead">{{node.payload.archivedItemsCount}}</span>
|
<span *ngIf="node.payload.archivedItemsCount >= 0" class="badge badge-pill badge-secondary align-top archived-items-lead">{{node.payload.archivedItemsCount}}</span>
|
||||||
</h5>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ds-truncatable [id]="node.id">
|
<ds-truncatable [id]="node.id">
|
||||||
|
@@ -28,10 +28,9 @@ export class CommunityListComponent implements OnInit, OnDestroy {
|
|||||||
treeControl = new FlatTreeControl<FlatNode>(
|
treeControl = new FlatTreeControl<FlatNode>(
|
||||||
(node: FlatNode) => node.level, (node: FlatNode) => true
|
(node: FlatNode) => node.level, (node: FlatNode) => true
|
||||||
);
|
);
|
||||||
|
|
||||||
dataSource: CommunityListDatasource;
|
dataSource: CommunityListDatasource;
|
||||||
|
|
||||||
paginationConfig: FindListOptions;
|
paginationConfig: FindListOptions;
|
||||||
|
trackBy = (index, node: FlatNode) => node.id;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected communityListService: CommunityListService,
|
protected communityListService: CommunityListService,
|
||||||
@@ -58,18 +57,28 @@ export class CommunityListComponent implements OnInit, OnDestroy {
|
|||||||
this.communityListService.saveCommunityListStateToStore(this.expandedNodes, this.loadingNode);
|
this.communityListService.saveCommunityListStateToStore(this.expandedNodes, this.loadingNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
// whether or not this node has children (subcommunities or collections)
|
/**
|
||||||
|
* Whether this node has children (subcommunities or collections)
|
||||||
|
* @param _
|
||||||
|
* @param node
|
||||||
|
*/
|
||||||
hasChild(_: number, node: FlatNode) {
|
hasChild(_: number, node: FlatNode) {
|
||||||
return node.isExpandable$;
|
return node.isExpandable$;
|
||||||
}
|
}
|
||||||
|
|
||||||
// whether or not it is a show more node (contains no data, but is indication that there are more topcoms, subcoms or collections
|
/**
|
||||||
|
* Whether this is a show more node that contains no data, but indicates that there is
|
||||||
|
* one or more community or collection.
|
||||||
|
* @param _
|
||||||
|
* @param node
|
||||||
|
*/
|
||||||
isShowMore(_: number, node: FlatNode) {
|
isShowMore(_: number, node: FlatNode) {
|
||||||
return node.isShowMoreNode;
|
return node.isShowMoreNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggles the expanded variable of a node, adds it to the expanded nodes list and reloads the tree so this node is expanded
|
* Toggles the expanded variable of a node, adds it to the expanded nodes list and reloads the tree
|
||||||
|
* so this node is expanded
|
||||||
* @param node Node we want to expand
|
* @param node Node we want to expand
|
||||||
*/
|
*/
|
||||||
toggleExpanded(node: FlatNode) {
|
toggleExpanded(node: FlatNode) {
|
||||||
@@ -92,9 +101,12 @@ export class CommunityListComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Makes sure the next page of a node is added to the tree (top community, sub community of collection)
|
* Makes sure the next page of a node is added to the tree (top community, sub community of collection)
|
||||||
* > Finds its parent (if not top community) and increases its corresponding collection/subcommunity currentPage
|
* > Finds its parent (if not top community) and increases its corresponding collection/subcommunity
|
||||||
* > Reloads tree with new page added to corresponding top community lis, sub community list or collection list
|
* currentPage
|
||||||
* @param node The show more node indicating whether it's an increase in top communities, sub communities or collections
|
* > Reloads tree with new page added to corresponding top community lis, sub community list or
|
||||||
|
* collection list
|
||||||
|
* @param node The show more node indicating whether it's an increase in top communities, sub communities
|
||||||
|
* or collections
|
||||||
*/
|
*/
|
||||||
getNextPage(node: FlatNode): void {
|
getNextPage(node: FlatNode): void {
|
||||||
this.loadingNode = node;
|
this.loadingNode = node;
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* The show more links in the community tree are also represented by a flatNode so we know where in
|
* The show more links in the community tree are also represented by a flatNode so we know where in
|
||||||
* the tree it should be rendered an who its parent is (needed for the action resulting in clicking this link)
|
* the tree it should be rendered and who its parent is (needed for the action resulting in clicking this link)
|
||||||
*/
|
*/
|
||||||
export class ShowMoreFlatNode {
|
export class ShowMoreFlatNode {
|
||||||
}
|
}
|
||||||
|
@@ -21,9 +21,6 @@
|
|||||||
</ds-comcol-page-content>
|
</ds-comcol-page-content>
|
||||||
</header>
|
</header>
|
||||||
<ds-dso-edit-menu></ds-dso-edit-menu>
|
<ds-dso-edit-menu></ds-dso-edit-menu>
|
||||||
<div class="pl-2 space-children-mr">
|
|
||||||
<ds-dso-page-subscription-button [dso]="communityPayload"></ds-dso-page-subscription-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<section class="comcol-page-browse-section">
|
<section class="comcol-page-browse-section">
|
||||||
|
|
||||||
|
@@ -152,12 +152,12 @@ export class AuthInterceptor implements HttpInterceptor {
|
|||||||
|
|
||||||
let authMethodModel: AuthMethod;
|
let authMethodModel: AuthMethod;
|
||||||
if (splittedRealm.length === 1) {
|
if (splittedRealm.length === 1) {
|
||||||
authMethodModel = new AuthMethod(methodName);
|
authMethodModel = new AuthMethod(methodName, Number(j));
|
||||||
authMethodModels.push(authMethodModel);
|
authMethodModels.push(authMethodModel);
|
||||||
} else if (splittedRealm.length > 1) {
|
} else if (splittedRealm.length > 1) {
|
||||||
let location = splittedRealm[1];
|
let location = splittedRealm[1];
|
||||||
location = this.parseLocation(location);
|
location = this.parseLocation(location);
|
||||||
authMethodModel = new AuthMethod(methodName, location);
|
authMethodModel = new AuthMethod(methodName, Number(j), location);
|
||||||
authMethodModels.push(authMethodModel);
|
authMethodModels.push(authMethodModel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,7 +165,7 @@ export class AuthInterceptor implements HttpInterceptor {
|
|||||||
// make sure the email + password login component gets rendered first
|
// make sure the email + password login component gets rendered first
|
||||||
authMethodModels = this.sortAuthMethods(authMethodModels);
|
authMethodModels = this.sortAuthMethods(authMethodModels);
|
||||||
} else {
|
} else {
|
||||||
authMethodModels.push(new AuthMethod(AuthMethodType.Password));
|
authMethodModels.push(new AuthMethod(AuthMethodType.Password, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
return authMethodModels;
|
return authMethodModels;
|
||||||
|
@@ -598,9 +598,9 @@ describe('authReducer', () => {
|
|||||||
authMethods: [],
|
authMethods: [],
|
||||||
idle: false
|
idle: false
|
||||||
};
|
};
|
||||||
const authMethods = [
|
const authMethods: AuthMethod[] = [
|
||||||
new AuthMethod(AuthMethodType.Password),
|
new AuthMethod(AuthMethodType.Password, 0),
|
||||||
new AuthMethod(AuthMethodType.Shibboleth, 'location')
|
new AuthMethod(AuthMethodType.Shibboleth, 1, 'location'),
|
||||||
];
|
];
|
||||||
const action = new RetrieveAuthMethodsSuccessAction(authMethods);
|
const action = new RetrieveAuthMethodsSuccessAction(authMethods);
|
||||||
const newState = authReducer(initialState, action);
|
const newState = authReducer(initialState, action);
|
||||||
@@ -632,7 +632,7 @@ describe('authReducer', () => {
|
|||||||
loaded: false,
|
loaded: false,
|
||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
authMethods: [new AuthMethod(AuthMethodType.Password)],
|
authMethods: [new AuthMethod(AuthMethodType.Password, 0)],
|
||||||
idle: false
|
idle: false
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
|
@@ -236,7 +236,7 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
|
|||||||
return Object.assign({}, state, {
|
return Object.assign({}, state, {
|
||||||
loading: false,
|
loading: false,
|
||||||
blocking: false,
|
blocking: false,
|
||||||
authMethods: [new AuthMethod(AuthMethodType.Password)]
|
authMethods: [new AuthMethod(AuthMethodType.Password, 0)]
|
||||||
});
|
});
|
||||||
|
|
||||||
case AuthActionTypes.SET_REDIRECT_URL:
|
case AuthActionTypes.SET_REDIRECT_URL:
|
||||||
|
@@ -2,11 +2,12 @@ import { AuthMethodType } from './auth.method-type';
|
|||||||
|
|
||||||
export class AuthMethod {
|
export class AuthMethod {
|
||||||
authMethodType: AuthMethodType;
|
authMethodType: AuthMethodType;
|
||||||
|
position: number;
|
||||||
location?: string;
|
location?: string;
|
||||||
|
|
||||||
// isStandalonePage? = true;
|
constructor(authMethodName: string, position: number, location?: string) {
|
||||||
|
this.position = position;
|
||||||
|
|
||||||
constructor(authMethodName: string, location?: string) {
|
|
||||||
switch (authMethodName) {
|
switch (authMethodName) {
|
||||||
case 'ip': {
|
case 'ip': {
|
||||||
this.authMethodType = AuthMethodType.Ip;
|
this.authMethodType = AuthMethodType.Ip;
|
||||||
|
@@ -7,6 +7,7 @@ import { ServerSyncBufferEffects } from './cache/server-sync-buffer.effects';
|
|||||||
import { ObjectUpdatesEffects } from './data/object-updates/object-updates.effects';
|
import { ObjectUpdatesEffects } from './data/object-updates/object-updates.effects';
|
||||||
import { RouteEffects } from './services/route.effects';
|
import { RouteEffects } from './services/route.effects';
|
||||||
import { RouterEffects } from './router/router.effects';
|
import { RouterEffects } from './router/router.effects';
|
||||||
|
import { MenuEffects } from '../shared/menu/menu.effects';
|
||||||
|
|
||||||
export const coreEffects = [
|
export const coreEffects = [
|
||||||
RequestEffects,
|
RequestEffects,
|
||||||
@@ -18,4 +19,5 @@ export const coreEffects = [
|
|||||||
ObjectUpdatesEffects,
|
ObjectUpdatesEffects,
|
||||||
RouteEffects,
|
RouteEffects,
|
||||||
RouterEffects,
|
RouterEffects,
|
||||||
|
MenuEffects,
|
||||||
];
|
];
|
||||||
|
@@ -74,7 +74,7 @@ export class AuthorizationDataService extends BaseDataService<Authorization> imp
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
catchError(() => observableOf(false)),
|
catchError(() => observableOf([])),
|
||||||
oneAuthorizationMatchesFeature(featureId)
|
oneAuthorizationMatchesFeature(featureId)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -68,13 +68,13 @@ export const oneAuthorizationMatchesFeature = (featureID: FeatureID) =>
|
|||||||
source.pipe(
|
source.pipe(
|
||||||
switchMap((authorizations: Authorization[]) => {
|
switchMap((authorizations: Authorization[]) => {
|
||||||
if (isNotEmpty(authorizations)) {
|
if (isNotEmpty(authorizations)) {
|
||||||
return observableCombineLatest(
|
return observableCombineLatest([
|
||||||
...authorizations
|
...authorizations
|
||||||
.filter((authorization: Authorization) => hasValue(authorization.feature))
|
.filter((authorization: Authorization) => hasValue(authorization.feature))
|
||||||
.map((authorization: Authorization) => authorization.feature.pipe(
|
.map((authorization: Authorization) => authorization.feature.pipe(
|
||||||
getFirstSucceededRemoteDataPayload()
|
getFirstSucceededRemoteDataPayload()
|
||||||
))
|
))
|
||||||
);
|
]);
|
||||||
} else {
|
} else {
|
||||||
return observableOf([]);
|
return observableOf([]);
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { Store, StoreModule } from '@ngrx/store';
|
import { Store, StoreModule } from '@ngrx/store';
|
||||||
import { cold, getTestScheduler } from 'jasmine-marbles';
|
import { cold, getTestScheduler } from 'jasmine-marbles';
|
||||||
import { EMPTY, of as observableOf } from 'rxjs';
|
import { EMPTY, Observable, of as observableOf } from 'rxjs';
|
||||||
import { TestScheduler } from 'rxjs/testing';
|
import { TestScheduler } from 'rxjs/testing';
|
||||||
|
|
||||||
import { getMockObjectCacheService } from '../../shared/mocks/object-cache.service.mock';
|
import { getMockObjectCacheService } from '../../shared/mocks/object-cache.service.mock';
|
||||||
@@ -638,4 +638,87 @@ describe('RequestService', () => {
|
|||||||
expect(done$).toBeObservable(cold('-----(t|)', { t: true }));
|
expect(done$).toBeObservable(cold('-----(t|)', { t: true }));
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('setStaleByHref', () => {
|
||||||
|
const uuid = 'c574a42c-4818-47ac-bbe1-6c3cd622c81f';
|
||||||
|
const href = 'https://rest.api/some/object';
|
||||||
|
const freshRE: any = {
|
||||||
|
request: { uuid, href },
|
||||||
|
state: RequestEntryState.Success
|
||||||
|
};
|
||||||
|
const staleRE: any = {
|
||||||
|
request: { uuid, href },
|
||||||
|
state: RequestEntryState.SuccessStale
|
||||||
|
};
|
||||||
|
|
||||||
|
it(`should call getByHref to retrieve the RequestEntry matching the href`, () => {
|
||||||
|
spyOn(service, 'getByHref').and.returnValue(observableOf(staleRE));
|
||||||
|
service.setStaleByHref(href);
|
||||||
|
expect(service.getByHref).toHaveBeenCalledWith(href);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should dispatch a RequestStaleAction for the RequestEntry returned by getByHref`, (done: DoneFn) => {
|
||||||
|
spyOn(service, 'getByHref').and.returnValue(observableOf(staleRE));
|
||||||
|
spyOn(store, 'dispatch');
|
||||||
|
service.setStaleByHref(href).subscribe(() => {
|
||||||
|
const requestStaleAction = new RequestStaleAction(uuid);
|
||||||
|
requestStaleAction.lastUpdated = jasmine.any(Number) as any;
|
||||||
|
expect(store.dispatch).toHaveBeenCalledWith(requestStaleAction);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should emit true when the request in the store is stale`, () => {
|
||||||
|
spyOn(service, 'getByHref').and.returnValue(cold('a-b', {
|
||||||
|
a: freshRE,
|
||||||
|
b: staleRE
|
||||||
|
}));
|
||||||
|
const result$ = service.setStaleByHref(href);
|
||||||
|
expect(result$).toBeObservable(cold('--(c|)', { c: true }));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setStaleByHrefSubstring', () => {
|
||||||
|
let dispatchSpy: jasmine.Spy;
|
||||||
|
let getByUUIDSpy: jasmine.Spy;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
dispatchSpy = spyOn(store, 'dispatch');
|
||||||
|
getByUUIDSpy = spyOn(service, 'getByUUID').and.callThrough();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with an empty/no matching requests in the state', () => {
|
||||||
|
it('should return true', () => {
|
||||||
|
const done$: Observable<boolean> = service.setStaleByHrefSubstring('https://rest.api/endpoint/selfLink');
|
||||||
|
expect(done$).toBeObservable(cold('(a|)', { a: true }));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with a matching request in the state', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const state = Object.assign({}, initialState, {
|
||||||
|
core: Object.assign({}, initialState.core, {
|
||||||
|
'index': {
|
||||||
|
'get-request/href-to-uuid': {
|
||||||
|
'https://rest.api/endpoint/selfLink': '5f2a0d2a-effa-4d54-bd54-5663b960f9eb'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
mockStore.setState(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an Observable that emits true as soon as the request is stale', () => {
|
||||||
|
dispatchSpy.and.callFake(() => { /* empty */ }); // don't actually set as stale
|
||||||
|
getByUUIDSpy.and.returnValue(cold('a-b--c--d-', { // but fake the state in the cache
|
||||||
|
a: { state: RequestEntryState.ResponsePending },
|
||||||
|
b: { state: RequestEntryState.Success },
|
||||||
|
c: { state: RequestEntryState.SuccessStale },
|
||||||
|
d: { state: RequestEntryState.Error },
|
||||||
|
}));
|
||||||
|
const done$: Observable<boolean> = service.setStaleByHrefSubstring('https://rest.api/endpoint/selfLink');
|
||||||
|
expect(done$).toBeObservable(cold('-----(a|)', { a: true }));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -2,8 +2,8 @@ import { Injectable } from '@angular/core';
|
|||||||
import { HttpHeaders } from '@angular/common/http';
|
import { HttpHeaders } from '@angular/common/http';
|
||||||
|
|
||||||
import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
|
import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable, from as observableFrom } from 'rxjs';
|
||||||
import { filter, map, take, tap } from 'rxjs/operators';
|
import { filter, find, map, mergeMap, switchMap, take, tap, toArray } from 'rxjs/operators';
|
||||||
import cloneDeep from 'lodash/cloneDeep';
|
import cloneDeep from 'lodash/cloneDeep';
|
||||||
import { hasValue, isEmpty, isNotEmpty, hasNoValue } from '../../shared/empty.util';
|
import { hasValue, isEmpty, isNotEmpty, hasNoValue } from '../../shared/empty.util';
|
||||||
import { ObjectCacheEntry } from '../cache/object-cache.reducer';
|
import { ObjectCacheEntry } from '../cache/object-cache.reducer';
|
||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
RequestExecuteAction,
|
RequestExecuteAction,
|
||||||
RequestStaleAction
|
RequestStaleAction
|
||||||
} from './request.actions';
|
} from './request.actions';
|
||||||
import { GetRequest} from './request.models';
|
import { GetRequest } from './request.models';
|
||||||
import { CommitSSBAction } from '../cache/server-sync-buffer.actions';
|
import { CommitSSBAction } from '../cache/server-sync-buffer.actions';
|
||||||
import { RestRequestMethod } from './rest-request-method';
|
import { RestRequestMethod } from './rest-request-method';
|
||||||
import { coreSelector } from '../core.selectors';
|
import { coreSelector } from '../core.selectors';
|
||||||
@@ -300,22 +300,42 @@ export class RequestService {
|
|||||||
* Set all requests that match (part of) the href to stale
|
* Set all requests that match (part of) the href to stale
|
||||||
*
|
*
|
||||||
* @param href A substring of the request(s) href
|
* @param href A substring of the request(s) href
|
||||||
* @return Returns an observable emitting whether or not the cache is removed
|
* @return Returns an observable emitting when those requests are all stale
|
||||||
*/
|
*/
|
||||||
setStaleByHrefSubstring(href: string): Observable<boolean> {
|
setStaleByHrefSubstring(href: string): Observable<boolean> {
|
||||||
this.store.pipe(
|
const requestUUIDs$ = this.store.pipe(
|
||||||
select(uuidsFromHrefSubstringSelector(requestIndexSelector, href)),
|
select(uuidsFromHrefSubstringSelector(requestIndexSelector, href)),
|
||||||
take(1)
|
take(1)
|
||||||
).subscribe((uuids: string[]) => {
|
);
|
||||||
|
requestUUIDs$.subscribe((uuids: string[]) => {
|
||||||
for (const uuid of uuids) {
|
for (const uuid of uuids) {
|
||||||
this.store.dispatch(new RequestStaleAction(uuid));
|
this.store.dispatch(new RequestStaleAction(uuid));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.requestsOnTheirWayToTheStore = this.requestsOnTheirWayToTheStore.filter((reqHref: string) => reqHref.indexOf(href) < 0);
|
this.requestsOnTheirWayToTheStore = this.requestsOnTheirWayToTheStore.filter((reqHref: string) => reqHref.indexOf(href) < 0);
|
||||||
|
|
||||||
return this.store.pipe(
|
// emit true after all requests are stale
|
||||||
select(uuidsFromHrefSubstringSelector(requestIndexSelector, href)),
|
return requestUUIDs$.pipe(
|
||||||
map((uuids) => isEmpty(uuids))
|
switchMap((uuids: string[]) => {
|
||||||
|
if (isEmpty(uuids)) {
|
||||||
|
// if there were no matching requests, emit true immediately
|
||||||
|
return [true];
|
||||||
|
} else {
|
||||||
|
// otherwise emit all request uuids in order
|
||||||
|
return observableFrom(uuids).pipe(
|
||||||
|
// retrieve the RequestEntry for each uuid
|
||||||
|
mergeMap((uuid: string) => this.getByUUID(uuid)),
|
||||||
|
// check whether it is undefined or stale
|
||||||
|
map((request: RequestEntry) => hasNoValue(request) || isStale(request.state)),
|
||||||
|
// if it is, complete
|
||||||
|
find((stale: boolean) => stale === true),
|
||||||
|
// after all observables above are completed, emit them as a single array
|
||||||
|
toArray(),
|
||||||
|
// when the array comes in, emit true
|
||||||
|
map(() => true)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,6 +354,28 @@ export class RequestService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark a request as stale
|
||||||
|
* @param href the href of the request
|
||||||
|
* @return an Observable that will emit true once the Request becomes stale
|
||||||
|
*/
|
||||||
|
setStaleByHref(href: string): Observable<boolean> {
|
||||||
|
const requestEntry$ = this.getByHref(href);
|
||||||
|
|
||||||
|
requestEntry$.pipe(
|
||||||
|
map((re: RequestEntry) => re.request.uuid),
|
||||||
|
take(1),
|
||||||
|
).subscribe((uuid: string) => {
|
||||||
|
this.store.dispatch(new RequestStaleAction(uuid));
|
||||||
|
});
|
||||||
|
|
||||||
|
return requestEntry$.pipe(
|
||||||
|
map((request: RequestEntry) => isStale(request.state)),
|
||||||
|
filter((stale: boolean) => stale),
|
||||||
|
take(1)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a GET request is in the cache or if it's still pending
|
* Check if a GET request is in the cache or if it's still pending
|
||||||
* @param {GetRequest} request The request to check
|
* @param {GetRequest} request The request to check
|
||||||
|
@@ -1,16 +1,18 @@
|
|||||||
import { RootDataService } from './root-data.service';
|
import { RootDataService } from './root-data.service';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
import {
|
||||||
import { Observable, of } from 'rxjs';
|
createSuccessfulRemoteDataObject$,
|
||||||
|
createFailedRemoteDataObject$
|
||||||
|
} from '../../shared/remote-data.utils';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
import { RemoteData } from './remote-data';
|
import { RemoteData } from './remote-data';
|
||||||
import { Root } from './root.model';
|
import { Root } from './root.model';
|
||||||
import { RawRestResponse } from '../dspace-rest/raw-rest-response.model';
|
|
||||||
import { cold } from 'jasmine-marbles';
|
import { cold } from 'jasmine-marbles';
|
||||||
|
|
||||||
describe('RootDataService', () => {
|
describe('RootDataService', () => {
|
||||||
let service: RootDataService;
|
let service: RootDataService;
|
||||||
let halService: HALEndpointService;
|
let halService: HALEndpointService;
|
||||||
let restService;
|
let requestService;
|
||||||
let rootEndpoint;
|
let rootEndpoint;
|
||||||
let findByHrefSpy;
|
let findByHrefSpy;
|
||||||
|
|
||||||
@@ -19,10 +21,10 @@ describe('RootDataService', () => {
|
|||||||
halService = jasmine.createSpyObj('halService', {
|
halService = jasmine.createSpyObj('halService', {
|
||||||
getRootHref: rootEndpoint,
|
getRootHref: rootEndpoint,
|
||||||
});
|
});
|
||||||
restService = jasmine.createSpyObj('halService', {
|
requestService = jasmine.createSpyObj('requestService', [
|
||||||
get: jasmine.createSpy('get'),
|
'setStaleByHref',
|
||||||
});
|
]);
|
||||||
service = new RootDataService(null, null, null, halService, restService);
|
service = new RootDataService(requestService, null, null, halService);
|
||||||
|
|
||||||
findByHrefSpy = spyOn(service as any, 'findByHref');
|
findByHrefSpy = spyOn(service as any, 'findByHref');
|
||||||
findByHrefSpy.and.returnValue(createSuccessfulRemoteDataObject$({}));
|
findByHrefSpy.and.returnValue(createSuccessfulRemoteDataObject$({}));
|
||||||
@@ -47,12 +49,8 @@ describe('RootDataService', () => {
|
|||||||
let result$: Observable<boolean>;
|
let result$: Observable<boolean>;
|
||||||
|
|
||||||
it('should return observable of true when root endpoint is available', () => {
|
it('should return observable of true when root endpoint is available', () => {
|
||||||
const mockResponse = {
|
spyOn(service, 'findRoot').and.returnValue(createSuccessfulRemoteDataObject$<Root>({} as any));
|
||||||
statusCode: 200,
|
|
||||||
statusText: 'OK'
|
|
||||||
} as RawRestResponse;
|
|
||||||
|
|
||||||
restService.get.and.returnValue(of(mockResponse));
|
|
||||||
result$ = service.checkServerAvailability();
|
result$ = service.checkServerAvailability();
|
||||||
|
|
||||||
expect(result$).toBeObservable(cold('(a|)', {
|
expect(result$).toBeObservable(cold('(a|)', {
|
||||||
@@ -61,12 +59,8 @@ describe('RootDataService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return observable of false when root endpoint is not available', () => {
|
it('should return observable of false when root endpoint is not available', () => {
|
||||||
const mockResponse = {
|
spyOn(service, 'findRoot').and.returnValue(createFailedRemoteDataObject$<Root>('500'));
|
||||||
statusCode: 500,
|
|
||||||
statusText: 'Internal Server Error'
|
|
||||||
} as RawRestResponse;
|
|
||||||
|
|
||||||
restService.get.and.returnValue(of(mockResponse));
|
|
||||||
result$ = service.checkServerAvailability();
|
result$ = service.checkServerAvailability();
|
||||||
|
|
||||||
expect(result$).toBeObservable(cold('(a|)', {
|
expect(result$).toBeObservable(cold('(a|)', {
|
||||||
@@ -75,4 +69,12 @@ describe('RootDataService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe(`invalidateRootCache`, () => {
|
||||||
|
it(`should set the cached root request to stale`, () => {
|
||||||
|
service.invalidateRootCache();
|
||||||
|
expect(halService.getRootHref).toHaveBeenCalled();
|
||||||
|
expect(requestService.setStaleByHref).toHaveBeenCalledWith(rootEndpoint);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -7,12 +7,11 @@ import { HALEndpointService } from '../shared/hal-endpoint.service';
|
|||||||
import { Observable, of as observableOf } from 'rxjs';
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
import { RemoteData } from './remote-data';
|
import { RemoteData } from './remote-data';
|
||||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
import { DspaceRestService } from '../dspace-rest/dspace-rest.service';
|
|
||||||
import { RawRestResponse } from '../dspace-rest/raw-rest-response.model';
|
|
||||||
import { catchError, map } from 'rxjs/operators';
|
import { catchError, map } from 'rxjs/operators';
|
||||||
import { BaseDataService } from './base/base-data.service';
|
import { BaseDataService } from './base/base-data.service';
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { dataService } from './base/data-service.decorator';
|
import { dataService } from './base/data-service.decorator';
|
||||||
|
import { getFirstCompletedRemoteData } from '../shared/operators';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A service to retrieve the {@link Root} object from the REST API.
|
* A service to retrieve the {@link Root} object from the REST API.
|
||||||
@@ -25,7 +24,6 @@ export class RootDataService extends BaseDataService<Root> {
|
|||||||
protected rdbService: RemoteDataBuildService,
|
protected rdbService: RemoteDataBuildService,
|
||||||
protected objectCache: ObjectCacheService,
|
protected objectCache: ObjectCacheService,
|
||||||
protected halService: HALEndpointService,
|
protected halService: HALEndpointService,
|
||||||
protected restService: DspaceRestService,
|
|
||||||
) {
|
) {
|
||||||
super('', requestService, rdbService, objectCache, halService, 6 * 60 * 60 * 1000);
|
super('', requestService, rdbService, objectCache, halService, 6 * 60 * 60 * 1000);
|
||||||
}
|
}
|
||||||
@@ -34,12 +32,13 @@ export class RootDataService extends BaseDataService<Root> {
|
|||||||
* Check if root endpoint is available
|
* Check if root endpoint is available
|
||||||
*/
|
*/
|
||||||
checkServerAvailability(): Observable<boolean> {
|
checkServerAvailability(): Observable<boolean> {
|
||||||
return this.restService.get(this.halService.getRootHref()).pipe(
|
return this.findRoot().pipe(
|
||||||
catchError((err ) => {
|
catchError((err ) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
return observableOf(false);
|
return observableOf(false);
|
||||||
}),
|
}),
|
||||||
map((res: RawRestResponse) => res.statusCode === 200)
|
getFirstCompletedRemoteData(),
|
||||||
|
map((rootRd: RemoteData<Root>) => rootRd.statusCode === 200)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,6 +59,6 @@ export class RootDataService extends BaseDataService<Root> {
|
|||||||
* Set to sale the root endpoint cache hit
|
* Set to sale the root endpoint cache hit
|
||||||
*/
|
*/
|
||||||
invalidateRootCache() {
|
invalidateRootCache() {
|
||||||
this.requestService.setStaleByHrefSubstring(this.halService.getRootHref());
|
this.requestService.setStaleByHref(this.halService.getRootHref());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -34,6 +34,7 @@ import { PatchData, PatchDataImpl } from '../data/base/patch-data';
|
|||||||
import { DeleteData, DeleteDataImpl } from '../data/base/delete-data';
|
import { DeleteData, DeleteDataImpl } from '../data/base/delete-data';
|
||||||
import { RestRequestMethod } from '../data/rest-request-method';
|
import { RestRequestMethod } from '../data/rest-request-method';
|
||||||
import { dataService } from '../data/base/data-service.decorator';
|
import { dataService } from '../data/base/data-service.decorator';
|
||||||
|
import { getEPersonEditRoute, getEPersonsRoute } from '../../access-control/access-control-routing-paths';
|
||||||
|
|
||||||
const ePeopleRegistryStateSelector = (state: AppState) => state.epeopleRegistry;
|
const ePeopleRegistryStateSelector = (state: AppState) => state.epeopleRegistry;
|
||||||
const editEPersonSelector = createSelector(ePeopleRegistryStateSelector, (ePeopleRegistryState: EPeopleRegistryState) => ePeopleRegistryState.editEPerson);
|
const editEPersonSelector = createSelector(ePeopleRegistryStateSelector, (ePeopleRegistryState: EPeopleRegistryState) => ePeopleRegistryState.editEPerson);
|
||||||
@@ -281,15 +282,14 @@ export class EPersonDataService extends IdentifiableDataService<EPerson> impleme
|
|||||||
this.editEPerson(ePerson);
|
this.editEPerson(ePerson);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return '/access-control/epeople';
|
return getEPersonEditRoute(ePerson.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get EPeople admin page
|
* Get EPeople admin page
|
||||||
* @param ePerson New EPerson to edit
|
|
||||||
*/
|
*/
|
||||||
public getEPeoplePageRouterLink(): string {
|
public getEPeoplePageRouterLink(): string {
|
||||||
return '/access-control/epeople';
|
return getEPersonsRoute();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -40,6 +40,7 @@ import { DeleteData, DeleteDataImpl } from '../data/base/delete-data';
|
|||||||
import { Operation } from 'fast-json-patch';
|
import { Operation } from 'fast-json-patch';
|
||||||
import { RestRequestMethod } from '../data/rest-request-method';
|
import { RestRequestMethod } from '../data/rest-request-method';
|
||||||
import { dataService } from '../data/base/data-service.decorator';
|
import { dataService } from '../data/base/data-service.decorator';
|
||||||
|
import { getGroupEditRoute } from '../../access-control/access-control-routing-paths';
|
||||||
|
|
||||||
const groupRegistryStateSelector = (state: AppState) => state.groupRegistry;
|
const groupRegistryStateSelector = (state: AppState) => state.groupRegistry;
|
||||||
const editGroupSelector = createSelector(groupRegistryStateSelector, (groupRegistryState: GroupRegistryState) => groupRegistryState.editGroup);
|
const editGroupSelector = createSelector(groupRegistryStateSelector, (groupRegistryState: GroupRegistryState) => groupRegistryState.editGroup);
|
||||||
@@ -264,15 +265,15 @@ export class GroupDataService extends IdentifiableDataService<Group> implements
|
|||||||
* @param group Group we want edit page for
|
* @param group Group we want edit page for
|
||||||
*/
|
*/
|
||||||
public getGroupEditPageRouterLink(group: Group): string {
|
public getGroupEditPageRouterLink(group: Group): string {
|
||||||
return this.getGroupEditPageRouterLinkWithID(group.id);
|
return getGroupEditRoute(group.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get Edit page of group
|
* Get Edit page of group
|
||||||
* @param groupID Group ID we want edit page for
|
* @param groupID Group ID we want edit page for
|
||||||
*/
|
*/
|
||||||
public getGroupEditPageRouterLinkWithID(groupId: string): string {
|
public getGroupEditPageRouterLinkWithID(groupID: string): string {
|
||||||
return '/access-control/groups/' + groupId;
|
return getGroupEditRoute(groupID);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -1,68 +1,86 @@
|
|||||||
import { ServerCheckGuard } from './server-check.guard';
|
import { ServerCheckGuard } from './server-check.guard';
|
||||||
import { Router } from '@angular/router';
|
import { Router, NavigationStart, UrlTree, NavigationEnd, RouterEvent } from '@angular/router';
|
||||||
|
|
||||||
import { of } from 'rxjs';
|
import { of, ReplaySubject } from 'rxjs';
|
||||||
import { take } from 'rxjs/operators';
|
|
||||||
|
|
||||||
import { getPageInternalServerErrorRoute } from '../../app-routing-paths';
|
|
||||||
import { RootDataService } from '../data/root-data.service';
|
import { RootDataService } from '../data/root-data.service';
|
||||||
|
import { TestScheduler } from 'rxjs/testing';
|
||||||
import SpyObj = jasmine.SpyObj;
|
import SpyObj = jasmine.SpyObj;
|
||||||
|
|
||||||
describe('ServerCheckGuard', () => {
|
describe('ServerCheckGuard', () => {
|
||||||
let guard: ServerCheckGuard;
|
let guard: ServerCheckGuard;
|
||||||
let router: SpyObj<Router>;
|
let router: Router;
|
||||||
|
const eventSubject = new ReplaySubject<RouterEvent>(1);
|
||||||
let rootDataServiceStub: SpyObj<RootDataService>;
|
let rootDataServiceStub: SpyObj<RootDataService>;
|
||||||
|
let testScheduler: TestScheduler;
|
||||||
rootDataServiceStub = jasmine.createSpyObj('RootDataService', {
|
let redirectUrlTree: UrlTree;
|
||||||
checkServerAvailability: jasmine.createSpy('checkServerAvailability'),
|
|
||||||
invalidateRootCache: jasmine.createSpy('invalidateRootCache')
|
|
||||||
});
|
|
||||||
router = jasmine.createSpyObj('Router', {
|
|
||||||
navigateByUrl: jasmine.createSpy('navigateByUrl')
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
guard = new ServerCheckGuard(router, rootDataServiceStub);
|
testScheduler = new TestScheduler((actual, expected) => {
|
||||||
|
expect(actual).toEqual(expected);
|
||||||
});
|
});
|
||||||
|
rootDataServiceStub = jasmine.createSpyObj('RootDataService', {
|
||||||
afterEach(() => {
|
checkServerAvailability: jasmine.createSpy('checkServerAvailability'),
|
||||||
router.navigateByUrl.calls.reset();
|
invalidateRootCache: jasmine.createSpy('invalidateRootCache'),
|
||||||
rootDataServiceStub.invalidateRootCache.calls.reset();
|
findRoot: jasmine.createSpy('findRoot')
|
||||||
|
});
|
||||||
|
redirectUrlTree = new UrlTree();
|
||||||
|
router = {
|
||||||
|
events: eventSubject.asObservable(),
|
||||||
|
navigateByUrl: jasmine.createSpy('navigateByUrl'),
|
||||||
|
parseUrl: jasmine.createSpy('parseUrl').and.returnValue(redirectUrlTree)
|
||||||
|
} as any;
|
||||||
|
guard = new ServerCheckGuard(router, rootDataServiceStub);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be created', () => {
|
it('should be created', () => {
|
||||||
expect(guard).toBeTruthy();
|
expect(guard).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when root endpoint has succeeded', () => {
|
describe('when root endpoint request has succeeded', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
rootDataServiceStub.checkServerAvailability.and.returnValue(of(true));
|
rootDataServiceStub.checkServerAvailability.and.returnValue(of(true));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not redirect to error page', () => {
|
it('should return true', () => {
|
||||||
guard.canActivateChild({} as any, {} as any).pipe(
|
testScheduler.run(({ expectObservable }) => {
|
||||||
take(1)
|
const result$ = guard.canActivateChild({} as any, {} as any);
|
||||||
).subscribe((canActivate: boolean) => {
|
expectObservable(result$).toBe('(a|)', { a: true });
|
||||||
expect(canActivate).toEqual(true);
|
|
||||||
expect(rootDataServiceStub.invalidateRootCache).not.toHaveBeenCalled();
|
|
||||||
expect(router.navigateByUrl).not.toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when root endpoint has not succeeded', () => {
|
describe('when root endpoint request has not succeeded', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
rootDataServiceStub.checkServerAvailability.and.returnValue(of(false));
|
rootDataServiceStub.checkServerAvailability.and.returnValue(of(false));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should redirect to error page', () => {
|
it('should return a UrlTree with the route to the 500 error page', () => {
|
||||||
guard.canActivateChild({} as any, {} as any).pipe(
|
testScheduler.run(({ expectObservable }) => {
|
||||||
take(1)
|
const result$ = guard.canActivateChild({} as any, {} as any);
|
||||||
).subscribe((canActivate: boolean) => {
|
expectObservable(result$).toBe('(b|)', { b: redirectUrlTree });
|
||||||
expect(canActivate).toEqual(false);
|
|
||||||
expect(rootDataServiceStub.invalidateRootCache).toHaveBeenCalled();
|
|
||||||
expect(router.navigateByUrl).toHaveBeenCalledWith(getPageInternalServerErrorRoute());
|
|
||||||
});
|
});
|
||||||
|
expect(router.parseUrl).toHaveBeenCalledWith('/500');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(`listenForRouteChanges`, () => {
|
||||||
|
it(`should retrieve the root endpoint, without using the cache, when the method is first called`, () => {
|
||||||
|
testScheduler.run(() => {
|
||||||
|
guard.listenForRouteChanges();
|
||||||
|
expect(rootDataServiceStub.findRoot).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should invalidate the root cache on every NavigationStart event`, () => {
|
||||||
|
testScheduler.run(() => {
|
||||||
|
guard.listenForRouteChanges();
|
||||||
|
eventSubject.next(new NavigationStart(1,''));
|
||||||
|
eventSubject.next(new NavigationEnd(1,'', ''));
|
||||||
|
eventSubject.next(new NavigationStart(2,''));
|
||||||
|
eventSubject.next(new NavigationEnd(2,'', ''));
|
||||||
|
eventSubject.next(new NavigationStart(3,''));
|
||||||
|
});
|
||||||
|
expect(rootDataServiceStub.invalidateRootCache).toHaveBeenCalledTimes(3);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,8 +1,15 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { ActivatedRouteSnapshot, CanActivateChild, Router, RouterStateSnapshot } from '@angular/router';
|
import {
|
||||||
|
ActivatedRouteSnapshot,
|
||||||
|
CanActivateChild,
|
||||||
|
Router,
|
||||||
|
RouterStateSnapshot,
|
||||||
|
UrlTree,
|
||||||
|
NavigationStart
|
||||||
|
} from '@angular/router';
|
||||||
|
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { take, tap } from 'rxjs/operators';
|
import { take, map, filter } from 'rxjs/operators';
|
||||||
|
|
||||||
import { RootDataService } from '../data/root-data.service';
|
import { RootDataService } from '../data/root-data.service';
|
||||||
import { getPageInternalServerErrorRoute } from '../../app-routing-paths';
|
import { getPageInternalServerErrorRoute } from '../../app-routing-paths';
|
||||||
@@ -23,17 +30,38 @@ export class ServerCheckGuard implements CanActivateChild {
|
|||||||
*/
|
*/
|
||||||
canActivateChild(
|
canActivateChild(
|
||||||
route: ActivatedRouteSnapshot,
|
route: ActivatedRouteSnapshot,
|
||||||
state: RouterStateSnapshot): Observable<boolean> {
|
state: RouterStateSnapshot
|
||||||
|
): Observable<boolean | UrlTree> {
|
||||||
|
|
||||||
return this.rootDataService.checkServerAvailability().pipe(
|
return this.rootDataService.checkServerAvailability().pipe(
|
||||||
take(1),
|
take(1),
|
||||||
tap((isAvailable: boolean) => {
|
map((isAvailable: boolean) => {
|
||||||
if (!isAvailable) {
|
if (!isAvailable) {
|
||||||
this.rootDataService.invalidateRootCache();
|
return this.router.parseUrl(getPageInternalServerErrorRoute());
|
||||||
this.router.navigateByUrl(getPageInternalServerErrorRoute());
|
} else {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listen to all router events. Every time a new navigation starts, invalidate the cache
|
||||||
|
* for the root endpoint. That way we retrieve it once per routing operation to ensure the
|
||||||
|
* backend is not down. But if the guard is called multiple times during the same routing
|
||||||
|
* operation, the cached version is used.
|
||||||
|
*/
|
||||||
|
listenForRouteChanges(): void {
|
||||||
|
// we'll always be too late for the first NavigationStart event with the router subscribe below,
|
||||||
|
// so this statement is for the very first route operation. A `find` without using the cache,
|
||||||
|
// rather than an invalidateRootCache, because invalidating as the app is bootstrapping can
|
||||||
|
// break other features
|
||||||
|
this.rootDataService.findRoot(false);
|
||||||
|
|
||||||
|
this.router.events.pipe(
|
||||||
|
filter(event => event instanceof NavigationStart),
|
||||||
|
).subscribe(() => {
|
||||||
|
this.rootDataService.invalidateRootCache();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -38,8 +38,8 @@ export class BrowserHardRedirectService extends HardRedirectService {
|
|||||||
/**
|
/**
|
||||||
* Get the origin of the current URL
|
* Get the origin of the current URL
|
||||||
* i.e. <scheme> "://" <hostname> [ ":" <port> ]
|
* i.e. <scheme> "://" <hostname> [ ":" <port> ]
|
||||||
* e.g. if the URL is https://demo7.dspace.org/search?query=test,
|
* e.g. if the URL is https://demo.dspace.org/search?query=test,
|
||||||
* the origin would be https://demo7.dspace.org
|
* the origin would be https://demo.dspace.org
|
||||||
*/
|
*/
|
||||||
getCurrentOrigin(): string {
|
getCurrentOrigin(): string {
|
||||||
return this.location.origin;
|
return this.location.origin;
|
||||||
|
@@ -25,8 +25,8 @@ export abstract class HardRedirectService {
|
|||||||
/**
|
/**
|
||||||
* Get the origin of the current URL
|
* Get the origin of the current URL
|
||||||
* i.e. <scheme> "://" <hostname> [ ":" <port> ]
|
* i.e. <scheme> "://" <hostname> [ ":" <port> ]
|
||||||
* e.g. if the URL is https://demo7.dspace.org/search?query=test,
|
* e.g. if the URL is https://demo.dspace.org/search?query=test,
|
||||||
* the origin would be https://demo7.dspace.org
|
* the origin would be https://demo.dspace.org
|
||||||
*/
|
*/
|
||||||
abstract getCurrentOrigin(): string;
|
abstract getCurrentOrigin(): string;
|
||||||
}
|
}
|
||||||
|
@@ -69,8 +69,8 @@ export class ServerHardRedirectService extends HardRedirectService {
|
|||||||
/**
|
/**
|
||||||
* Get the origin of the current URL
|
* Get the origin of the current URL
|
||||||
* i.e. <scheme> "://" <hostname> [ ":" <port> ]
|
* i.e. <scheme> "://" <hostname> [ ":" <port> ]
|
||||||
* e.g. if the URL is https://demo7.dspace.org/search?query=test,
|
* e.g. if the URL is https://demo.dspace.org/search?query=test,
|
||||||
* the origin would be https://demo7.dspace.org
|
* the origin would be https://demo.dspace.org
|
||||||
*/
|
*/
|
||||||
getCurrentOrigin(): string {
|
getCurrentOrigin(): string {
|
||||||
return this.req.protocol + '://' + this.req.headers.host;
|
return this.req.protocol + '://' + this.req.headers.host;
|
||||||
|
@@ -29,6 +29,18 @@ export class WorkspaceitemSectionUploadFileObject {
|
|||||||
value: string;
|
value: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The file format information
|
||||||
|
*/
|
||||||
|
format: {
|
||||||
|
shortDescription: string,
|
||||||
|
description: string,
|
||||||
|
mimetype: string,
|
||||||
|
supportLevel: string,
|
||||||
|
internal: boolean,
|
||||||
|
type: string
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The file url
|
* The file url
|
||||||
*/
|
*/
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { Component, Inject, Injector, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
import { Component, Inject, Injector, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
||||||
import { AlertType } from '../../shared/alert/aletr-type';
|
import { AlertType } from '../../shared/alert/alert-type';
|
||||||
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||||
import { DsoEditMetadataForm } from './dso-edit-metadata-form';
|
import { DsoEditMetadataForm } from './dso-edit-metadata-form';
|
||||||
import { map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
|
@@ -1,3 +1,3 @@
|
|||||||
<ds-register-email-form
|
<ds-themed-register-email-form
|
||||||
[MESSAGE_PREFIX]="'forgot-email.form'" [typeRequest]="typeRequest">
|
[MESSAGE_PREFIX]="'forgot-email.form'" [typeRequest]="typeRequest">
|
||||||
</ds-register-email-form>
|
</ds-themed-register-email-form>
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
<div [ngClass]="{'open': !(isNavBarCollapsed | async)}">
|
<div [ngClass]="{'open': !(isNavBarCollapsed | async)}" id="header-navbar-wrapper">
|
||||||
<ds-themed-header></ds-themed-header>
|
<ds-themed-header></ds-themed-header>
|
||||||
<ds-themed-navbar></ds-themed-navbar>
|
<ds-themed-navbar></ds-themed-navbar>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,4 +1,6 @@
|
|||||||
:host {
|
:host {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: var(--ds-nav-z-index);
|
div#header-navbar-wrapper {
|
||||||
|
border-bottom: 1px var(--ds-header-navbar-border-bottom-color) solid;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit, ElementRef } from '@angular/core';
|
||||||
import { ContextHelpService } from '../../shared/context-help.service';
|
import { ContextHelpService } from '../../shared/context-help.service';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable, Subscription } from 'rxjs';
|
||||||
import { map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -15,12 +15,23 @@ import { map } from 'rxjs/operators';
|
|||||||
export class ContextHelpToggleComponent implements OnInit {
|
export class ContextHelpToggleComponent implements OnInit {
|
||||||
buttonVisible$: Observable<boolean>;
|
buttonVisible$: Observable<boolean>;
|
||||||
|
|
||||||
|
subscriptions: Subscription[] = [];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private contextHelpService: ContextHelpService,
|
protected elRef: ElementRef,
|
||||||
) { }
|
protected contextHelpService: ContextHelpService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.buttonVisible$ = this.contextHelpService.tooltipCount$().pipe(map(x => x > 0));
|
this.buttonVisible$ = this.contextHelpService.tooltipCount$().pipe(map(x => x > 0));
|
||||||
|
this.subscriptions.push(this.buttonVisible$.subscribe((showContextHelpToggle: boolean) => {
|
||||||
|
if (showContextHelpToggle) {
|
||||||
|
this.elRef.nativeElement.classList.remove('d-none');
|
||||||
|
} else {
|
||||||
|
this.elRef.nativeElement.classList.add('d-none');
|
||||||
|
}
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
onClick() {
|
onClick() {
|
||||||
|
@@ -7,12 +7,12 @@
|
|||||||
|
|
||||||
<nav role="navigation" [attr.aria-label]="'nav.user.description' | translate" class="navbar navbar-light navbar-expand-md flex-shrink-0 px-0">
|
<nav role="navigation" [attr.aria-label]="'nav.user.description' | translate" class="navbar navbar-light navbar-expand-md flex-shrink-0 px-0">
|
||||||
<ds-themed-search-navbar></ds-themed-search-navbar>
|
<ds-themed-search-navbar></ds-themed-search-navbar>
|
||||||
<ds-lang-switch></ds-lang-switch>
|
<ds-themed-lang-switch></ds-themed-lang-switch>
|
||||||
<ds-context-help-toggle></ds-context-help-toggle>
|
<ds-context-help-toggle></ds-context-help-toggle>
|
||||||
<ds-themed-auth-nav-menu></ds-themed-auth-nav-menu>
|
<ds-themed-auth-nav-menu></ds-themed-auth-nav-menu>
|
||||||
<ds-impersonate-navbar></ds-impersonate-navbar>
|
<ds-impersonate-navbar></ds-impersonate-navbar>
|
||||||
<div class="pl-2">
|
<div *ngIf="isXsOrSm$ | async" class="pl-2">
|
||||||
<button class="navbar-toggler" type="button" (click)="toggleNavbar()"
|
<button class="navbar-toggler px-0" type="button" (click)="toggleNavbar()"
|
||||||
aria-controls="collapsingNav"
|
aria-controls="collapsingNav"
|
||||||
aria-expanded="false" [attr.aria-label]="'nav.toggle' | translate">
|
aria-expanded="false" [attr.aria-label]="'nav.toggle' | translate">
|
||||||
<span class="navbar-toggler-icon fas fa-bars fa-fw" aria-hidden="true"></span>
|
<span class="navbar-toggler-icon fas fa-bars fa-fw" aria-hidden="true"></span>
|
||||||
|
@@ -1,3 +1,7 @@
|
|||||||
|
header {
|
||||||
|
background-color: var(--ds-header-bg);
|
||||||
|
}
|
||||||
|
|
||||||
.navbar-brand img {
|
.navbar-brand img {
|
||||||
max-height: var(--ds-header-logo-height);
|
max-height: var(--ds-header-logo-height);
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
@@ -20,3 +24,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
display: flex;
|
||||||
|
gap: calc(var(--bs-spacer) / 3);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
@@ -10,6 +10,8 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
|||||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
import { MenuService } from '../shared/menu/menu.service';
|
import { MenuService } from '../shared/menu/menu.service';
|
||||||
import { MenuServiceStub } from '../shared/testing/menu-service.stub';
|
import { MenuServiceStub } from '../shared/testing/menu-service.stub';
|
||||||
|
import { HostWindowService } from '../shared/host-window.service';
|
||||||
|
import { HostWindowServiceStub } from '../shared/testing/host-window-service.stub';
|
||||||
|
|
||||||
let comp: HeaderComponent;
|
let comp: HeaderComponent;
|
||||||
let fixture: ComponentFixture<HeaderComponent>;
|
let fixture: ComponentFixture<HeaderComponent>;
|
||||||
@@ -26,6 +28,7 @@ describe('HeaderComponent', () => {
|
|||||||
ReactiveFormsModule],
|
ReactiveFormsModule],
|
||||||
declarations: [HeaderComponent],
|
declarations: [HeaderComponent],
|
||||||
providers: [
|
providers: [
|
||||||
|
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
|
||||||
{ provide: MenuService, useValue: menuService }
|
{ provide: MenuService, useValue: menuService }
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
@@ -40,7 +43,7 @@ describe('HeaderComponent', () => {
|
|||||||
fixture = TestBed.createComponent(HeaderComponent);
|
fixture = TestBed.createComponent(HeaderComponent);
|
||||||
|
|
||||||
comp = fixture.componentInstance;
|
comp = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when the toggle button is clicked', () => {
|
describe('when the toggle button is clicked', () => {
|
||||||
|
@@ -1,7 +1,8 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { MenuService } from '../shared/menu/menu.service';
|
import { MenuService } from '../shared/menu/menu.service';
|
||||||
import { MenuID } from '../shared/menu/menu-id.model';
|
import { MenuID } from '../shared/menu/menu-id.model';
|
||||||
|
import { HostWindowService } from '../shared/host-window.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents the header with the logo and simple navigation
|
* Represents the header with the logo and simple navigation
|
||||||
@@ -11,20 +12,25 @@ import { MenuID } from '../shared/menu/menu-id.model';
|
|||||||
styleUrls: ['header.component.scss'],
|
styleUrls: ['header.component.scss'],
|
||||||
templateUrl: 'header.component.html',
|
templateUrl: 'header.component.html',
|
||||||
})
|
})
|
||||||
export class HeaderComponent {
|
export class HeaderComponent implements OnInit {
|
||||||
/**
|
/**
|
||||||
* Whether user is authenticated.
|
* Whether user is authenticated.
|
||||||
* @type {Observable<string>}
|
* @type {Observable<string>}
|
||||||
*/
|
*/
|
||||||
public isAuthenticated: Observable<boolean>;
|
public isAuthenticated: Observable<boolean>;
|
||||||
public showAuth = false;
|
public isXsOrSm$: Observable<boolean>;
|
||||||
menuID = MenuID.PUBLIC;
|
menuID = MenuID.PUBLIC;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private menuService: MenuService
|
protected menuService: MenuService,
|
||||||
|
protected windowService: HostWindowService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.isXsOrSm$ = this.windowService.isXsOrSm();
|
||||||
|
}
|
||||||
|
|
||||||
public toggleNavbar(): void {
|
public toggleNavbar(): void {
|
||||||
this.menuService.toggleMenu(this.menuID);
|
this.menuService.toggleMenu(this.menuID);
|
||||||
}
|
}
|
||||||
|
@@ -3,7 +3,7 @@ import { Component, Input } from '@angular/core';
|
|||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
import { HealthComponent } from '../../models/health-component.model';
|
import { HealthComponent } from '../../models/health-component.model';
|
||||||
import { AlertType } from '../../../shared/alert/aletr-type';
|
import { AlertType } from '../../../shared/alert/alert-type';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A component to render a "health component" object.
|
* A component to render a "health component" object.
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
<div class="jumbotron jumbotron-fluid">
|
<div class="jumbotron jumbotron-fluid mt-ncs">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="d-flex flex-wrap">
|
<div class="d-flex flex-wrap">
|
||||||
<div>
|
<div>
|
||||||
|
@@ -1,7 +1,5 @@
|
|||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
margin-top: calc(-1 * var(--ds-content-spacing));
|
|
||||||
margin-bottom: calc(-1 * var(--ds-content-spacing));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.display-3 {
|
.display-3 {
|
||||||
|
@@ -1,12 +1,12 @@
|
|||||||
<ng-container *ngVar="(itemRD$ | async) as itemRD">
|
<ng-container *ngVar="(itemRD$ | async) as itemRD">
|
||||||
<div class="mt-4" [ngClass]="placeholderFontClass" *ngIf="itemRD?.hasSucceeded && itemRD?.payload?.page.length > 0" @fadeIn>
|
<div class="mt-4" [ngClass]="placeholderFontClass" *ngIf="itemRD?.hasSucceeded && itemRD?.payload?.page.length > 0" @fadeIn>
|
||||||
<div class="d-flex flex-row border-bottom mb-4 pb-4 ng-tns-c416-2"></div>
|
<div class="d-flex flex-row border-bottom mb-4 pb-4"></div>
|
||||||
<h2> {{'home.recent-submissions.head' | translate}}</h2>
|
<h2> {{'home.recent-submissions.head' | translate}}</h2>
|
||||||
<div class="my-4" *ngFor="let item of itemRD?.payload?.page">
|
<div class="my-4" *ngFor="let item of itemRD?.payload?.page">
|
||||||
<ds-listable-object-component-loader [object]="item" [viewMode]="viewMode" class="pb-4">
|
<ds-listable-object-component-loader [object]="item" [viewMode]="viewMode" class="pb-4">
|
||||||
</ds-listable-object-component-loader>
|
</ds-listable-object-component-loader>
|
||||||
</div>
|
</div>
|
||||||
<button (click)="onLoadMore()" class="btn btn-primary search-button mt-4 float-left ng-tns-c290-40"> {{'vocabulary-treeview.load-more' | translate }} ...</button>
|
<button (click)="onLoadMore()" class="btn btn-primary search-button mt-4"> {{'vocabulary-treeview.load-more' | translate }} ...</button>
|
||||||
</div>
|
</div>
|
||||||
<ds-error *ngIf="itemRD?.hasFailed" message="{{'error.recent-submissions' | translate}}"></ds-error>
|
<ds-error *ngIf="itemRD?.hasFailed" message="{{'error.recent-submissions' | translate}}"></ds-error>
|
||||||
<ds-loading *ngIf="!itemRD || itemRD.isLoading" message="{{'loading.recent-submissions' | translate}}">
|
<ds-loading *ngIf="!itemRD || itemRD.isLoading" message="{{'loading.recent-submissions' | translate}}">
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { Component, Input } from '@angular/core';
|
import { Component, Input } from '@angular/core';
|
||||||
import { Item } from '../../core/shared/item.model';
|
import { Item } from '../../core/shared/item.model';
|
||||||
import { AlertType } from '../../shared/alert/aletr-type';
|
import { AlertType } from '../../shared/alert/alert-type';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-item-alerts',
|
selector: 'ds-item-alerts',
|
||||||
|
@@ -25,7 +25,7 @@
|
|||||||
<div class="{{columnSizes.columns[3].buildClasses()}} row-element d-flex align-items-center">
|
<div class="{{columnSizes.columns[3].buildClasses()}} row-element d-flex align-items-center">
|
||||||
<div class="text-center w-100">
|
<div class="text-center w-100">
|
||||||
<div class="btn-group relationship-action-buttons">
|
<div class="btn-group relationship-action-buttons">
|
||||||
<a *ngIf="bitstreamDownloadUrl != null" [href]="bitstreamDownloadUrl"
|
<a *ngIf="bitstreamDownloadUrl != null" [routerLink]="bitstreamDownloadUrl"
|
||||||
class="btn btn-outline-primary btn-sm"
|
class="btn btn-outline-primary btn-sm"
|
||||||
title="{{'item.edit.bitstreams.edit.buttons.download' | translate}}"
|
title="{{'item.edit.bitstreams.edit.buttons.download' | translate}}"
|
||||||
[attr.data-test]="'download-button' | dsBrowserOnly">
|
[attr.data-test]="'download-button' | dsBrowserOnly">
|
||||||
|
@@ -13,6 +13,7 @@ import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-dat
|
|||||||
import { getBitstreamDownloadRoute } from '../../../../app-routing-paths';
|
import { getBitstreamDownloadRoute } from '../../../../app-routing-paths';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { BrowserOnlyMockPipe } from '../../../../shared/testing/browser-only-mock.pipe';
|
import { BrowserOnlyMockPipe } from '../../../../shared/testing/browser-only-mock.pipe';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
|
||||||
let comp: ItemEditBitstreamComponent;
|
let comp: ItemEditBitstreamComponent;
|
||||||
let fixture: ComponentFixture<ItemEditBitstreamComponent>;
|
let fixture: ComponentFixture<ItemEditBitstreamComponent>;
|
||||||
@@ -72,7 +73,10 @@ describe('ItemEditBitstreamComponent', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [TranslateModule.forRoot()],
|
imports: [
|
||||||
|
RouterTestingModule.withRoutes([]),
|
||||||
|
TranslateModule.forRoot(),
|
||||||
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
ItemEditBitstreamComponent,
|
ItemEditBitstreamComponent,
|
||||||
VarDirective,
|
VarDirective,
|
||||||
|
@@ -21,7 +21,11 @@
|
|||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<p>
|
<p>
|
||||||
<label for="inheritPoliciesCheckbox">
|
<label for="inheritPoliciesCheckbox">
|
||||||
<input type="checkbox" name="tc" [(ngModel)]="inheritPolicies" id="inheritPoliciesCheckbox">
|
<ng-template #tooltipContent>
|
||||||
|
{{ 'item.edit.move.inheritpolicies.tooltip' | translate }}
|
||||||
|
</ng-template>
|
||||||
|
<input type="checkbox" name="tc" [(ngModel)]="inheritPolicies" id="inheritPoliciesCheckbox" [ngbTooltip]="tooltipContent"
|
||||||
|
>
|
||||||
{{'item.edit.move.inheritpolicies.checkbox' |translate}}
|
{{'item.edit.move.inheritpolicies.checkbox' |translate}}
|
||||||
</label>
|
</label>
|
||||||
</p>
|
</p>
|
||||||
|
@@ -5,7 +5,7 @@ import { Item } from '../../../core/shared/item.model';
|
|||||||
import { map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
import { getFirstSucceededRemoteData } from '../../../core/shared/operators';
|
import { getFirstSucceededRemoteData } from '../../../core/shared/operators';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { AlertType } from '../../../shared/alert/aletr-type';
|
import { AlertType } from '../../../shared/alert/alert-type';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-item-version-history',
|
selector: 'ds-item-version-history',
|
||||||
|
@@ -70,7 +70,8 @@ export class MiradorViewerComponent implements OnInit {
|
|||||||
const manifestApiEndpoint = encodeURIComponent(environment.rest.baseUrl + '/iiif/'
|
const manifestApiEndpoint = encodeURIComponent(environment.rest.baseUrl + '/iiif/'
|
||||||
+ this.object.id + '/manifest');
|
+ this.object.id + '/manifest');
|
||||||
// The Express path to Mirador viewer.
|
// The Express path to Mirador viewer.
|
||||||
let viewerPath = '/iiif/mirador/index.html?manifest=' + manifestApiEndpoint;
|
let viewerPath = `${environment.ui.nameSpace}${environment.ui.nameSpace.length > 1 ? '/' : ''}`
|
||||||
|
+ `iiif/mirador/index.html?manifest=${manifestApiEndpoint}`;
|
||||||
if (this.searchable) {
|
if (this.searchable) {
|
||||||
// Tell the viewer add search to menu.
|
// Tell the viewer add search to menu.
|
||||||
viewerPath += '&searchable=' + this.searchable;
|
viewerPath += '&searchable=' + this.searchable;
|
||||||
|
@@ -15,7 +15,7 @@ import { getFirstCompletedRemoteData } from '../../../core/shared/operators';
|
|||||||
import { hasValue } from '../../../shared/empty.util';
|
import { hasValue } from '../../../shared/empty.util';
|
||||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||||
import { AlertType } from '../../../shared/alert/aletr-type';
|
import { AlertType } from '../../../shared/alert/alert-type';
|
||||||
import { Item } from '../../../core/shared/item.model';
|
import { Item } from '../../../core/shared/item.model';
|
||||||
import { OrcidAuthService } from '../../../core/orcid/orcid-auth.service';
|
import { OrcidAuthService } from '../../../core/orcid/orcid-auth.service';
|
||||||
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
<h2 class="item-page-title-field">
|
<h1 class="item-page-title-field">
|
||||||
<div *ngIf="item.firstMetadataValue('dspace.entity.type') as type" class="d-inline">
|
<div *ngIf="item.firstMetadataValue('dspace.entity.type') as type" class="d-inline">
|
||||||
{{ type.toLowerCase() + '.page.titleprefix' | translate }}
|
{{ type.toLowerCase() + '.page.titleprefix' | translate }}
|
||||||
</div>
|
</div>
|
||||||
<span class="dont-break-out">{{ dsoNameService.getName(item) }}</span>
|
<span class="dont-break-out">{{ dsoNameService.getName(item) }}</span>
|
||||||
</h2>
|
</h1>
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user