mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-13 21:13:07 +00:00
Merge remote-tracking branch 'upstream/dspace-7_x' into Angular-SRR-menu-issues
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'
|
||||||
|
84
.github/workflows/docker.yml
vendored
84
.github/workflows/docker.yml
vendored
@@ -15,29 +15,35 @@ on:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read # to fetch code (actions/checkout)
|
contents: read # to fetch code (actions/checkout)
|
||||||
|
|
||||||
|
|
||||||
|
env:
|
||||||
|
# Define tags to use for Docker images based on Git tags/branches (for docker/metadata-action)
|
||||||
|
# For a new commit on default branch (main), use the literal tag 'latest' on Docker image.
|
||||||
|
# For a new commit on other branches, use the branch name as the tag for Docker image.
|
||||||
|
# For a new tag, copy that tag name as the tag for Docker image.
|
||||||
|
IMAGE_TAGS: |
|
||||||
|
type=raw,value=latest,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }}
|
||||||
|
type=ref,event=branch,enable=${{ !endsWith(github.ref, github.event.repository.default_branch) }}
|
||||||
|
type=ref,event=tag
|
||||||
|
# Define default tag "flavor" for docker/metadata-action per
|
||||||
|
# https://github.com/docker/metadata-action#flavor-input
|
||||||
|
# We manage the 'latest' tag ourselves to the 'main' branch (see settings above)
|
||||||
|
TAGS_FLAVOR: |
|
||||||
|
latest=false
|
||||||
|
# Architectures / Platforms for which we will build Docker images
|
||||||
|
# If this is a PR, we ONLY build for AMD64. For PRs we only do a sanity check test to ensure Docker builds work.
|
||||||
|
# If this is NOT a PR (e.g. a tag or merge commit), also build for ARM64.
|
||||||
|
PLATFORMS: linux/amd64${{ github.event_name != 'pull_request' && ', linux/arm64' || '' }}
|
||||||
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
docker:
|
###############################################
|
||||||
|
# Build/Push the 'dspace/dspace-angular' image
|
||||||
|
###############################################
|
||||||
|
dspace-angular:
|
||||||
# Ensure this job never runs on forked repos. It's only executed for 'dspace/dspace-angular'
|
# Ensure this job never runs on forked repos. It's only executed for 'dspace/dspace-angular'
|
||||||
if: github.repository == 'dspace/dspace-angular'
|
if: github.repository == 'dspace/dspace-angular'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
|
||||||
# Define tags to use for Docker images based on Git tags/branches (for docker/metadata-action)
|
|
||||||
# For a new commit on default branch (main), use the literal tag 'dspace-7_x' on Docker image.
|
|
||||||
# For a new commit on other branches, use the branch name as the tag for Docker image.
|
|
||||||
# For a new tag, copy that tag name as the tag for Docker image.
|
|
||||||
IMAGE_TAGS: |
|
|
||||||
type=raw,value=dspace-7_x,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }}
|
|
||||||
type=ref,event=branch,enable=${{ !endsWith(github.ref, github.event.repository.default_branch) }}
|
|
||||||
type=ref,event=tag
|
|
||||||
# Define default tag "flavor" for docker/metadata-action per
|
|
||||||
# https://github.com/docker/metadata-action#flavor-input
|
|
||||||
# We turn off 'latest' tag by default.
|
|
||||||
TAGS_FLAVOR: |
|
|
||||||
latest=false
|
|
||||||
# Architectures / Platforms for which we will build Docker images
|
|
||||||
# If this is a PR, we ONLY build for AMD64. For PRs we only do a sanity check test to ensure Docker builds work.
|
|
||||||
# If this is NOT a PR (e.g. a tag or merge commit), also build for ARM64.
|
|
||||||
PLATFORMS: linux/amd64${{ github.event_name != 'pull_request' && ', linux/arm64' || '' }}
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
# https://github.com/actions/checkout
|
# https://github.com/actions/checkout
|
||||||
@@ -61,9 +67,6 @@ jobs:
|
|||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
|
password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
|
||||||
|
|
||||||
###############################################
|
|
||||||
# Build/Push the 'dspace/dspace-angular' image
|
|
||||||
###############################################
|
|
||||||
# https://github.com/docker/metadata-action
|
# https://github.com/docker/metadata-action
|
||||||
# Get Metadata for docker_build step below
|
# Get Metadata for docker_build step below
|
||||||
- name: Sync metadata (tags, labels) from GitHub to Docker for 'dspace-angular' image
|
- name: Sync metadata (tags, labels) from GitHub to Docker for 'dspace-angular' image
|
||||||
@@ -77,7 +80,7 @@ jobs:
|
|||||||
# https://github.com/docker/build-push-action
|
# https://github.com/docker/build-push-action
|
||||||
- name: Build and push 'dspace-angular' image
|
- name: Build and push 'dspace-angular' image
|
||||||
id: docker_build
|
id: docker_build
|
||||||
uses: docker/build-push-action@v3
|
uses: docker/build-push-action@v4
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
@@ -89,9 +92,36 @@ jobs:
|
|||||||
tags: ${{ steps.meta_build.outputs.tags }}
|
tags: ${{ steps.meta_build.outputs.tags }}
|
||||||
labels: ${{ steps.meta_build.outputs.labels }}
|
labels: ${{ steps.meta_build.outputs.labels }}
|
||||||
|
|
||||||
#####################################################
|
#############################################################
|
||||||
# Build/Push the 'dspace/dspace-angular' image ('-dist' tag)
|
# Build/Push the 'dspace/dspace-angular' image ('-dist' tag)
|
||||||
#####################################################
|
#############################################################
|
||||||
|
dspace-angular-dist:
|
||||||
|
# Ensure this job never runs on forked repos. It's only executed for 'dspace/dspace-angular'
|
||||||
|
if: github.repository == 'dspace/dspace-angular'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
# https://github.com/actions/checkout
|
||||||
|
- name: Checkout codebase
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
# https://github.com/docker/setup-buildx-action
|
||||||
|
- name: Setup Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
|
# https://github.com/docker/setup-qemu-action
|
||||||
|
- name: Set up QEMU emulation to build for multiple architectures
|
||||||
|
uses: docker/setup-qemu-action@v2
|
||||||
|
|
||||||
|
# https://github.com/docker/login-action
|
||||||
|
- name: Login to DockerHub
|
||||||
|
# Only login if not a PR, as PRs only trigger a Docker build and not a push
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
|
||||||
|
|
||||||
# https://github.com/docker/metadata-action
|
# https://github.com/docker/metadata-action
|
||||||
# Get Metadata for docker_build_dist step below
|
# Get Metadata for docker_build_dist step below
|
||||||
- name: Sync metadata (tags, labels) from GitHub to Docker for 'dspace-angular-dist' image
|
- name: Sync metadata (tags, labels) from GitHub to Docker for 'dspace-angular-dist' image
|
||||||
@@ -107,7 +137,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build and push 'dspace-angular-dist' image
|
- name: Build and push 'dspace-angular-dist' image
|
||||||
id: docker_build_dist
|
id: docker_build_dist
|
||||||
uses: docker/build-push-action@v3
|
uses: docker/build-push-action@v4
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile.dist
|
file: ./Dockerfile.dist
|
||||||
|
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
|
@@ -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: demo.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
|
||||||
@@ -292,33 +292,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: demo.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', () => {
|
||||||
|
@@ -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
|
||||||
|
@@ -24,7 +24,7 @@ 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: demo.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-7_x-dist
|
||||||
|
@@ -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
|
||||||
```
|
```
|
||||||
|
11
package.json
11
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dspace-angular",
|
"name": "dspace-angular",
|
||||||
"version": "7.6.0",
|
"version": "7.6.1-next",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
"config:watch": "nodemon",
|
"config:watch": "nodemon",
|
||||||
@@ -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() {
|
||||||
|
@@ -16,23 +16,23 @@
|
|||||||
[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"><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 *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 between class="btn-group ml-1">
|
||||||
<button *ngIf="!isImpersonated" class="btn btn-primary" [ngClass]="{'d-none' : !(canImpersonate$ | async)}" (click)="impersonate()">
|
<button *ngIf="!isImpersonated" class="btn btn-primary" type="button" [ngClass]="{'d-none' : !(canImpersonate$ | async)}" (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 after class="btn btn-danger delete-button" type="button" [disabled]="!(canDelete$ | async)" (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>
|
||||||
|
@@ -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="groupBeingEdited != null" 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>
|
||||||
|
@@ -37,7 +37,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';
|
||||||
|
@@ -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),
|
||||||
prefix: this.name.value,
|
switchMap((schema: MetadataSchema) => {
|
||||||
namespace: this.namespace.value
|
const metadataValues = {
|
||||||
};
|
prefix: this.name.value,
|
||||||
if (schema == null) {
|
namespace: this.namespace.value,
|
||||||
this.registryService.createOrUpdateMetadataSchema(Object.assign(new MetadataSchema(), values)).subscribe((newSchema) => {
|
};
|
||||||
this.submitForm.emit(newSchema);
|
|
||||||
|
let createOrUpdate$: Observable<MetadataSchema>;
|
||||||
|
|
||||||
|
if (schema == null) {
|
||||||
|
createOrUpdate$ =
|
||||||
|
this.registryService.createOrUpdateMetadataSchema(
|
||||||
|
Object.assign(new MetadataSchema(), metadataValues)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const updatedSchema = Object.assign(
|
||||||
|
new MetadataSchema(),
|
||||||
|
schema,
|
||||||
|
{
|
||||||
|
namespace: metadataValues.namespace,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
createOrUpdate$ =
|
||||||
|
this.registryService.createOrUpdateMetadataSchema(
|
||||||
|
updatedSchema
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return createOrUpdate$;
|
||||||
|
}),
|
||||||
|
tap(() => {
|
||||||
|
this.registryService.clearMetadataSchemaRequests().subscribe();
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.subscribe((updatedOrCreatedSchema: MetadataSchema) => {
|
||||||
|
this.submitForm.emit(updatedOrCreatedSchema);
|
||||||
|
this.clearFields();
|
||||||
|
this.registryService.cancelEditMetadataSchema();
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
this.registryService.createOrUpdateMetadataSchema(Object.assign(new MetadataSchema(), schema, {
|
|
||||||
id: schema.id,
|
|
||||||
prefix: schema.prefix,
|
|
||||||
namespace: values.namespace,
|
|
||||||
})).subscribe((updatedSchema: MetadataSchema) => {
|
|
||||||
this.submitForm.emit(updatedSchema);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.clearFields();
|
|
||||||
this.registryService.cancelEditMetadataSchema();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -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();
|
||||||
|
@@ -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 {
|
||||||
}
|
}
|
||||||
|
@@ -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;
|
||||||
|
@@ -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,48 @@ describe('RequestService', () => {
|
|||||||
expect(done$).toBeObservable(cold('-----(t|)', { t: true }));
|
expect(done$).toBeObservable(cold('-----(t|)', { t: 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';
|
||||||
@@ -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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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';
|
||||||
|
@@ -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,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',
|
||||||
|
@@ -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',
|
||||||
|
@@ -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>
|
||||||
|
@@ -5,8 +5,7 @@ import { RemoteData } from '../../../../core/data/remote-data';
|
|||||||
import { Relationship } from '../../../../core/shared/item-relationships/relationship.model';
|
import { Relationship } from '../../../../core/shared/item-relationships/relationship.model';
|
||||||
import { Item } from '../../../../core/shared/item.model';
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
import {
|
import {
|
||||||
getFirstSucceededRemoteDataPayload,
|
getFirstCompletedRemoteData
|
||||||
getFirstSucceededRemoteData
|
|
||||||
} from '../../../../core/shared/operators';
|
} from '../../../../core/shared/operators';
|
||||||
import { hasValue } from '../../../../shared/empty.util';
|
import { hasValue } from '../../../../shared/empty.util';
|
||||||
import { InjectionToken } from '@angular/core';
|
import { InjectionToken } from '@angular/core';
|
||||||
@@ -77,24 +76,42 @@ export const relationsToItems = (thisId: string) =>
|
|||||||
* @param {string} thisId The item's id of which the relations belong to
|
* @param {string} thisId The item's id of which the relations belong to
|
||||||
* @returns {(source: Observable<Relationship[]>) => Observable<Item[]>}
|
* @returns {(source: Observable<Relationship[]>) => Observable<Item[]>}
|
||||||
*/
|
*/
|
||||||
export const paginatedRelationsToItems = (thisId: string) =>
|
export const paginatedRelationsToItems = (thisId: string) => (source: Observable<RemoteData<PaginatedList<Relationship>>>): Observable<RemoteData<PaginatedList<Item>>> =>
|
||||||
(source: Observable<RemoteData<PaginatedList<Relationship>>>): Observable<RemoteData<PaginatedList<Item>>> =>
|
|
||||||
source.pipe(
|
source.pipe(
|
||||||
getFirstSucceededRemoteData(),
|
getFirstCompletedRemoteData(),
|
||||||
switchMap((relationshipsRD: RemoteData<PaginatedList<Relationship>>) => {
|
switchMap((relationshipsRD: RemoteData<PaginatedList<Relationship>>) => {
|
||||||
return observableCombineLatest(
|
return observableCombineLatest(
|
||||||
relationshipsRD.payload.page.map((rel: Relationship) =>
|
relationshipsRD.payload.page.map((rel: Relationship) =>
|
||||||
observableCombineLatest([
|
observableCombineLatest([
|
||||||
rel.leftItem.pipe(getFirstSucceededRemoteDataPayload()),
|
rel.leftItem.pipe(
|
||||||
rel.rightItem.pipe(getFirstSucceededRemoteDataPayload())]
|
getFirstCompletedRemoteData(),
|
||||||
|
map((rd: RemoteData<Item>) => {
|
||||||
|
if (rd.hasSucceeded) {
|
||||||
|
return rd.payload;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
),
|
||||||
|
rel.rightItem.pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
map((rd: RemoteData<Item>) => {
|
||||||
|
if (rd.hasSucceeded) {
|
||||||
|
return rd.payload;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
),
|
||||||
|
]
|
||||||
)
|
)
|
||||||
)).pipe(
|
)
|
||||||
|
).pipe(
|
||||||
map((arr) =>
|
map((arr) =>
|
||||||
arr
|
arr.map(([leftItem, rightItem]) => {
|
||||||
.map(([leftItem, rightItem]) => {
|
if (hasValue(leftItem) && leftItem.id === thisId) {
|
||||||
if (leftItem.id === thisId) {
|
|
||||||
return rightItem;
|
return rightItem;
|
||||||
} else if (rightItem.id === thisId) {
|
} else if (hasValue(rightItem) && rightItem.id === thisId) {
|
||||||
return leftItem;
|
return leftItem;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@@ -2,5 +2,6 @@
|
|||||||
[fixedFilterQuery]="fixedFilter"
|
[fixedFilterQuery]="fixedFilter"
|
||||||
[configuration]="configuration"
|
[configuration]="configuration"
|
||||||
[searchEnabled]="searchEnabled"
|
[searchEnabled]="searchEnabled"
|
||||||
[sideBarWidth]="sideBarWidth">
|
[sideBarWidth]="sideBarWidth"
|
||||||
|
[showCsvExport]="true">
|
||||||
</ds-configuration-search-page>
|
</ds-configuration-search-page>
|
||||||
|
@@ -23,7 +23,7 @@ import { PaginatedList } from '../../core/data/paginated-list.model';
|
|||||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||||
import { VersionHistoryDataService } from '../../core/data/version-history-data.service';
|
import { VersionHistoryDataService } from '../../core/data/version-history-data.service';
|
||||||
import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model';
|
import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model';
|
||||||
import { AlertType } from '../../shared/alert/aletr-type';
|
import { AlertType } from '../../shared/alert/alert-type';
|
||||||
import { followLink } from '../../shared/utils/follow-link-config.model';
|
import { followLink } from '../../shared/utils/follow-link-config.model';
|
||||||
import { hasValue, hasValueOperator } from '../../shared/empty.util';
|
import { hasValue, hasValueOperator } from '../../shared/empty.util';
|
||||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||||
|
@@ -12,7 +12,7 @@ import {
|
|||||||
} from '../../../core/shared/operators';
|
} from '../../../core/shared/operators';
|
||||||
import { map, startWith, switchMap } from 'rxjs/operators';
|
import { map, startWith, switchMap } from 'rxjs/operators';
|
||||||
import { VersionHistoryDataService } from '../../../core/data/version-history-data.service';
|
import { VersionHistoryDataService } from '../../../core/data/version-history-data.service';
|
||||||
import { AlertType } from '../../../shared/alert/aletr-type';
|
import { AlertType } from '../../../shared/alert/alert-type';
|
||||||
import { getItemPageRoute } from '../../item-page-routing-paths';
|
import { getItemPageRoute } from '../../item-page-routing-paths';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
@@ -147,7 +147,7 @@ describe('ProcessDetailComponent', () => {
|
|||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
provide: ActivatedRoute,
|
provide: ActivatedRoute,
|
||||||
useValue: { data: observableOf({ process: createSuccessfulRemoteDataObject(process) }) }
|
useValue: { data: observableOf({ process: createSuccessfulRemoteDataObject(process) }), snapshot: { params: { id: 1 } } },
|
||||||
},
|
},
|
||||||
{ provide: ProcessDataService, useValue: processService },
|
{ provide: ProcessDataService, useValue: processService },
|
||||||
{ provide: BitstreamDataService, useValue: bitstreamDataService },
|
{ provide: BitstreamDataService, useValue: bitstreamDataService },
|
||||||
@@ -310,10 +310,11 @@ describe('ProcessDetailComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should call refresh method every 5 seconds, until process is completed', fakeAsync(() => {
|
it('should call refresh method every 5 seconds, until process is completed', fakeAsync(() => {
|
||||||
spyOn(component, 'refresh');
|
spyOn(component, 'refresh').and.callThrough();
|
||||||
spyOn(component, 'stopRefreshTimer');
|
spyOn(component, 'stopRefreshTimer').and.callThrough();
|
||||||
|
|
||||||
process.processStatus = ProcessStatus.COMPLETED;
|
// start off with a running process in order for the refresh counter starts counting up
|
||||||
|
process.processStatus = ProcessStatus.RUNNING;
|
||||||
// set findbyId to return a completed process
|
// set findbyId to return a completed process
|
||||||
(processService.findById as jasmine.Spy).and.returnValue(observableOf(createSuccessfulRemoteDataObject(process)));
|
(processService.findById as jasmine.Spy).and.returnValue(observableOf(createSuccessfulRemoteDataObject(process)));
|
||||||
|
|
||||||
@@ -336,6 +337,10 @@ describe('ProcessDetailComponent', () => {
|
|||||||
tick(1001); // 1 second + 1 ms by the setTimeout
|
tick(1001); // 1 second + 1 ms by the setTimeout
|
||||||
expect(component.refreshCounter$.value).toBe(0); // 1 - 1
|
expect(component.refreshCounter$.value).toBe(0); // 1 - 1
|
||||||
|
|
||||||
|
// set the process to completed right before the counter checks the process
|
||||||
|
process.processStatus = ProcessStatus.COMPLETED;
|
||||||
|
(processService.findById as jasmine.Spy).and.returnValue(observableOf(createSuccessfulRemoteDataObject(process)));
|
||||||
|
|
||||||
tick(1000); // 1 second
|
tick(1000); // 1 second
|
||||||
|
|
||||||
expect(component.refresh).toHaveBeenCalledTimes(1);
|
expect(component.refresh).toHaveBeenCalledTimes(1);
|
||||||
|
@@ -17,7 +17,7 @@ import {
|
|||||||
getFirstSucceededRemoteDataPayload
|
getFirstSucceededRemoteDataPayload
|
||||||
} from '../../core/shared/operators';
|
} from '../../core/shared/operators';
|
||||||
import { URLCombiner } from '../../core/url-combiner/url-combiner';
|
import { URLCombiner } from '../../core/url-combiner/url-combiner';
|
||||||
import { AlertType } from '../../shared/alert/aletr-type';
|
import { AlertType } from '../../shared/alert/alert-type';
|
||||||
import { hasValue } from '../../shared/empty.util';
|
import { hasValue } from '../../shared/empty.util';
|
||||||
import { ProcessStatus } from '../processes/process-status.model';
|
import { ProcessStatus } from '../processes/process-status.model';
|
||||||
import { Process } from '../processes/process.model';
|
import { Process } from '../processes/process.model';
|
||||||
|
@@ -161,7 +161,7 @@ export class ProfilePageComponent implements OnInit {
|
|||||||
} else {
|
} else {
|
||||||
this.notificationsService.error(
|
this.notificationsService.error(
|
||||||
this.translate.instant(this.PASSWORD_NOTIFICATIONS_PREFIX + 'error.title'),
|
this.translate.instant(this.PASSWORD_NOTIFICATIONS_PREFIX + 'error.title'),
|
||||||
this.translate.instant(this.PASSWORD_NOTIFICATIONS_PREFIX + 'error.change-failed')
|
this.getPasswordErrorMessage(response)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -199,4 +199,18 @@ export class ProfilePageComponent implements OnInit {
|
|||||||
return this.isResearcherProfileEnabled$.asObservable();
|
return this.isResearcherProfileEnabled$.asObservable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an error message from a password validation request with a specific reason or
|
||||||
|
* a default message without specific reason.
|
||||||
|
* @param response from the validation password patch request.
|
||||||
|
*/
|
||||||
|
getPasswordErrorMessage(response) {
|
||||||
|
if (response.hasFailed && isNotEmpty(response.errorMessage)) {
|
||||||
|
// Response has a specific error message. Show this message in the error notification.
|
||||||
|
return this.translate.instant(response.errorMessage);
|
||||||
|
}
|
||||||
|
// Show default error message notification.
|
||||||
|
return this.translate.instant(this.PASSWORD_NOTIFICATIONS_PREFIX + 'error.change-failed');
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -13,7 +13,7 @@ import {isNotEmpty} from '../shared/empty.util';
|
|||||||
import {BehaviorSubject, combineLatest, Observable, of, switchMap} from 'rxjs';
|
import {BehaviorSubject, combineLatest, Observable, of, switchMap} from 'rxjs';
|
||||||
import {map, startWith, take} from 'rxjs/operators';
|
import {map, startWith, take} from 'rxjs/operators';
|
||||||
import {CAPTCHA_NAME, GoogleRecaptchaService} from '../core/google-recaptcha/google-recaptcha.service';
|
import {CAPTCHA_NAME, GoogleRecaptchaService} from '../core/google-recaptcha/google-recaptcha.service';
|
||||||
import {AlertType} from '../shared/alert/aletr-type';
|
import {AlertType} from '../shared/alert/alert-type';
|
||||||
import {KlaroService} from '../shared/cookies/klaro.service';
|
import {KlaroService} from '../shared/cookies/klaro.service';
|
||||||
import {CookieService} from '../core/services/cookie.service';
|
import {CookieService} from '../core/services/cookie.service';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { ThemedComponent } from 'src/app/shared/theme-support/themed.component';
|
import { ThemedComponent } from 'src/app/shared/theme-support/themed.component';
|
||||||
|
|
||||||
import { DenyRequestCopyComponent } from 'src/themes/custom/app/request-copy/deny-request-copy/deny-request-copy.component';
|
import { DenyRequestCopyComponent } from './deny-request-copy.component';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Themed wrapper for deny-request-copy.component
|
* Themed wrapper for deny-request-copy.component
|
||||||
|
@@ -15,7 +15,7 @@ import {
|
|||||||
import { BulkAccessConfigDataService } from '../../core/config/bulk-access-config-data.service';
|
import { BulkAccessConfigDataService } from '../../core/config/bulk-access-config-data.service';
|
||||||
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
|
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
|
||||||
import { BulkAccessConditionOptions } from '../../core/config/models/bulk-access-condition-options.model';
|
import { BulkAccessConditionOptions } from '../../core/config/models/bulk-access-condition-options.model';
|
||||||
import { AlertType } from '../alert/aletr-type';
|
import { AlertType } from '../alert/alert-type';
|
||||||
import {
|
import {
|
||||||
createAccessControlInitialFormState
|
createAccessControlInitialFormState
|
||||||
} from './access-control-form-container-intial-state';
|
} from './access-control-form-container-intial-state';
|
||||||
|
@@ -8,7 +8,7 @@ import { TranslateModule } from '@ngx-translate/core';
|
|||||||
|
|
||||||
import { AlertComponent } from './alert.component';
|
import { AlertComponent } from './alert.component';
|
||||||
import { createTestComponent } from '../testing/utils.test';
|
import { createTestComponent } from '../testing/utils.test';
|
||||||
import { AlertType } from './aletr-type';
|
import { AlertType } from './alert-type';
|
||||||
|
|
||||||
describe('AlertComponent test suite', () => {
|
describe('AlertComponent test suite', () => {
|
||||||
|
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { ChangeDetectorRef, Component, EventEmitter, Input, Output, ViewEncapsulation } from '@angular/core';
|
import { ChangeDetectorRef, Component, EventEmitter, Input, Output, ViewEncapsulation } from '@angular/core';
|
||||||
import { trigger } from '@angular/animations';
|
import { trigger } from '@angular/animations';
|
||||||
|
|
||||||
import { AlertType } from './aletr-type';
|
import { AlertType } from './alert-type';
|
||||||
import { fadeOutLeave, fadeOutState } from '../animations/fade';
|
import { fadeOutLeave, fadeOutState } from '../animations/fade';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -42,7 +42,7 @@
|
|||||||
[formModel]="formModel"
|
[formModel]="formModel"
|
||||||
[displayCancel]="false"
|
[displayCancel]="false"
|
||||||
(submitForm)="onSubmit()">
|
(submitForm)="onSubmit()">
|
||||||
<button before (click)="back.emit()" class="btn btn-outline-secondary">
|
<button before (click)="back.emit()" class="btn btn-outline-secondary" type="button">
|
||||||
<i class="fas fa-arrow-left"></i> {{ type.value + '.edit.return' | translate }}
|
<i class="fas fa-arrow-left"></i> {{ type.value + '.edit.return' | translate }}
|
||||||
</button>
|
</button>
|
||||||
</ds-form>
|
</ds-form>
|
||||||
|
@@ -3,7 +3,7 @@ import { Component, Input } from '@angular/core';
|
|||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { AlertType } from '../alert/aletr-type';
|
import { AlertType } from '../alert/alert-type';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-error',
|
selector: 'ds-error',
|
||||||
|
@@ -130,7 +130,7 @@ export class DsDynamicListComponent extends DynamicFormControlComponent implemen
|
|||||||
(v) => v.value === option.value));
|
(v) => v.value === option.value));
|
||||||
|
|
||||||
const item: ListItem = {
|
const item: ListItem = {
|
||||||
id: value,
|
id: `${this.model.id}_${value}`,
|
||||||
label: option.display,
|
label: option.display,
|
||||||
value: checked,
|
value: checked,
|
||||||
index: key
|
index: key
|
||||||
|
@@ -43,7 +43,7 @@
|
|||||||
|
|
||||||
<button class="dropdown-item disabled" *ngIf="optionsList && optionsList.length == 0">{{'form.no-results' | translate}}</button>
|
<button class="dropdown-item disabled" *ngIf="optionsList && optionsList.length == 0">{{'form.no-results' | translate}}</button>
|
||||||
<button class="dropdown-item collection-item text-truncate" *ngFor="let listEntry of optionsList"
|
<button class="dropdown-item collection-item text-truncate" *ngFor="let listEntry of optionsList"
|
||||||
(click)="onSelect(listEntry); sdRef.close()" (mousedown)="onSelect(listEntry); sdRef.close()"
|
(keydown.enter)="onSelect(listEntry); sdRef.close()" (mousedown)="onSelect(listEntry); sdRef.close()"
|
||||||
title="{{ listEntry.display }}" role="option"
|
title="{{ listEntry.display }}" role="option"
|
||||||
[attr.id]="listEntry.display == (currentValue|async) ? ('combobox_' + id + '_selected') : null">
|
[attr.id]="listEntry.display == (currentValue|async) ? ('combobox_' + id + '_selected') : null">
|
||||||
{{inputFormatter(listEntry)}}
|
{{inputFormatter(listEntry)}}
|
||||||
|
@@ -159,14 +159,15 @@ describe('Dynamic Dynamic Scrollable Dropdown component', () => {
|
|||||||
let de: any = scrollableDropdownFixture.debugElement.query(By.css('input.form-control'));
|
let de: any = scrollableDropdownFixture.debugElement.query(By.css('input.form-control'));
|
||||||
let btnEl = de.nativeElement;
|
let btnEl = de.nativeElement;
|
||||||
|
|
||||||
btnEl.click();
|
const mousedownEvent = new MouseEvent('mousedown');
|
||||||
|
|
||||||
|
btnEl.dispatchEvent(mousedownEvent);
|
||||||
scrollableDropdownFixture.detectChanges();
|
scrollableDropdownFixture.detectChanges();
|
||||||
|
|
||||||
de = scrollableDropdownFixture.debugElement.queryAll(By.css('button.dropdown-item'));
|
de = scrollableDropdownFixture.debugElement.queryAll(By.css('button.dropdown-item'));
|
||||||
btnEl = de[0].nativeElement;
|
btnEl = de[0].nativeElement;
|
||||||
|
|
||||||
btnEl.click();
|
btnEl.dispatchEvent(mousedownEvent);
|
||||||
|
|
||||||
scrollableDropdownFixture.detectChanges();
|
scrollableDropdownFixture.detectChanges();
|
||||||
|
|
||||||
expect((scrollableDropdownComp.model as any).value).toEqual(selectedValue);
|
expect((scrollableDropdownComp.model as any).value).toEqual(selectedValue);
|
||||||
|
@@ -42,6 +42,7 @@
|
|||||||
[collection]="collection"
|
[collection]="collection"
|
||||||
[relationship]="relationshipOptions"
|
[relationship]="relationshipOptions"
|
||||||
[context]="context"
|
[context]="context"
|
||||||
|
[query]="query"
|
||||||
[externalSource]="source"
|
[externalSource]="source"
|
||||||
(importedObject)="imported($event)"
|
(importedObject)="imported($event)"
|
||||||
class="d-block pt-3">
|
class="d-block pt-3">
|
||||||
|
@@ -75,6 +75,12 @@ export class DsDynamicLookupRelationExternalSourceTabComponent implements OnInit
|
|||||||
* The context to displaying lists for
|
* The context to displaying lists for
|
||||||
*/
|
*/
|
||||||
@Input() context: Context;
|
@Input() context: Context;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The search query
|
||||||
|
*/
|
||||||
|
@Input() query: string;
|
||||||
|
|
||||||
@Input() repeatable: boolean;
|
@Input() repeatable: boolean;
|
||||||
/**
|
/**
|
||||||
* Emit an event when an object has been imported (or selected from similar local entries)
|
* Emit an event when an object has been imported (or selected from similar local entries)
|
||||||
@@ -149,8 +155,12 @@ export class DsDynamicLookupRelationExternalSourceTabComponent implements OnInit
|
|||||||
|
|
||||||
this.resetRoute();
|
this.resetRoute();
|
||||||
this.entriesRD$ = this.searchConfigService.paginatedSearchOptions.pipe(
|
this.entriesRD$ = this.searchConfigService.paginatedSearchOptions.pipe(
|
||||||
switchMap((searchOptions: PaginatedSearchOptions) =>
|
switchMap((searchOptions: PaginatedSearchOptions) => {
|
||||||
this.externalSourceService.getExternalSourceEntries(this.externalSource.id, searchOptions).pipe(startWith(undefined)))
|
if (searchOptions.query === '') {
|
||||||
|
searchOptions.query = this.query;
|
||||||
|
}
|
||||||
|
return this.externalSourceService.getExternalSourceEntries(this.externalSource.id, searchOptions).pipe(startWith(undefined));
|
||||||
|
})
|
||||||
);
|
);
|
||||||
this.currentPagination$ = this.paginationService.getCurrentPagination(this.searchConfigService.paginationID, this.initialPagination);
|
this.currentPagination$ = this.paginationService.getCurrentPagination(this.searchConfigService.paginationID, this.initialPagination);
|
||||||
this.importConfig = {
|
this.importConfig = {
|
||||||
|
@@ -15,7 +15,7 @@ import { DsDynamicLookupRelationExternalSourceTabComponent } from './dynamic-loo
|
|||||||
})
|
})
|
||||||
export class ThemedDynamicLookupRelationExternalSourceTabComponent extends ThemedComponent<DsDynamicLookupRelationExternalSourceTabComponent> {
|
export class ThemedDynamicLookupRelationExternalSourceTabComponent extends ThemedComponent<DsDynamicLookupRelationExternalSourceTabComponent> {
|
||||||
protected inAndOutputNames: (keyof DsDynamicLookupRelationExternalSourceTabComponent & keyof this)[] = ['label', 'listId',
|
protected inAndOutputNames: (keyof DsDynamicLookupRelationExternalSourceTabComponent & keyof this)[] = ['label', 'listId',
|
||||||
'item', 'collection', 'relationship', 'context', 'repeatable', 'importedObject', 'externalSource'];
|
'item', 'collection', 'relationship', 'context', 'query', 'repeatable', 'importedObject', 'externalSource'];
|
||||||
|
|
||||||
@Input() label: string;
|
@Input() label: string;
|
||||||
|
|
||||||
@@ -29,6 +29,8 @@ export class ThemedDynamicLookupRelationExternalSourceTabComponent extends Theme
|
|||||||
|
|
||||||
@Input() context: Context;
|
@Input() context: Context;
|
||||||
|
|
||||||
|
@Input() query: string;
|
||||||
|
|
||||||
@Input() repeatable: boolean;
|
@Input() repeatable: boolean;
|
||||||
|
|
||||||
@Output() importedObject: EventEmitter<ListableObject> = new EventEmitter();
|
@Output() importedObject: EventEmitter<ListableObject> = new EventEmitter();
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { FlatTreeControl } from '@angular/cdk/tree';
|
import { FlatTreeControl } from '@angular/cdk/tree';
|
||||||
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output, OnChanges, SimpleChanges } from '@angular/core';
|
||||||
|
|
||||||
import { map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
import { Observable, Subscription } from 'rxjs';
|
import { Observable, Subscription } from 'rxjs';
|
||||||
@@ -28,7 +28,7 @@ import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operato
|
|||||||
templateUrl: './vocabulary-treeview.component.html',
|
templateUrl: './vocabulary-treeview.component.html',
|
||||||
styleUrls: ['./vocabulary-treeview.component.scss']
|
styleUrls: ['./vocabulary-treeview.component.scss']
|
||||||
})
|
})
|
||||||
export class VocabularyTreeviewComponent implements OnDestroy, OnInit {
|
export class VocabularyTreeviewComponent implements OnDestroy, OnInit, OnChanges {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The {@link VocabularyOptions} object
|
* The {@link VocabularyOptions} object
|
||||||
@@ -322,4 +322,9 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit {
|
|||||||
private getEntryId(entry: VocabularyEntry): string {
|
private getEntryId(entry: VocabularyEntry): string {
|
||||||
return entry.authority || entry.otherInformation.id || undefined;
|
return entry.authority || entry.otherInformation.id || undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
|
this.reset();
|
||||||
|
this.vocabularyTreeviewService.initialize(this.vocabularyOptions, new PageInfo(), this.selectedItems, null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -13,13 +13,17 @@ import { AuthMethod } from '../../../core/auth/models/auth.method';
|
|||||||
import { AuthServiceStub } from '../../testing/auth-service.stub';
|
import { AuthServiceStub } from '../../testing/auth-service.stub';
|
||||||
import { createTestComponent } from '../../testing/utils.test';
|
import { createTestComponent } from '../../testing/utils.test';
|
||||||
import { HardRedirectService } from '../../../core/services/hard-redirect.service';
|
import { HardRedirectService } from '../../../core/services/hard-redirect.service';
|
||||||
|
import { AuthMethodType } from '../../../core/auth/models/auth.method-type';
|
||||||
|
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { AuthorizationDataServiceStub } from '../../testing/authorization-service.stub';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
|
||||||
describe('LogInContainerComponent', () => {
|
describe('LogInContainerComponent', () => {
|
||||||
|
|
||||||
let component: LogInContainerComponent;
|
let component: LogInContainerComponent;
|
||||||
let fixture: ComponentFixture<LogInContainerComponent>;
|
let fixture: ComponentFixture<LogInContainerComponent>;
|
||||||
|
|
||||||
const authMethod = new AuthMethod('password');
|
const authMethod = new AuthMethod(AuthMethodType.Password, 0);
|
||||||
|
|
||||||
let hardRedirectService: HardRedirectService;
|
let hardRedirectService: HardRedirectService;
|
||||||
|
|
||||||
@@ -35,13 +39,15 @@ describe('LogInContainerComponent', () => {
|
|||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
StoreModule.forRoot(authReducer),
|
StoreModule.forRoot(authReducer),
|
||||||
SharedModule,
|
SharedModule,
|
||||||
TranslateModule.forRoot()
|
TranslateModule.forRoot(),
|
||||||
|
RouterTestingModule,
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
TestComponent
|
TestComponent
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: AuthService, useClass: AuthServiceStub },
|
{ provide: AuthService, useClass: AuthServiceStub },
|
||||||
|
{ provide: AuthorizationDataService, useClass: AuthorizationDataServiceStub },
|
||||||
{ provide: HardRedirectService, useValue: hardRedirectService },
|
{ provide: HardRedirectService, useValue: hardRedirectService },
|
||||||
LogInContainerComponent
|
LogInContainerComponent
|
||||||
],
|
],
|
||||||
@@ -113,6 +119,6 @@ describe('LogInContainerComponent', () => {
|
|||||||
class TestComponent {
|
class TestComponent {
|
||||||
|
|
||||||
isStandalonePage = true;
|
isStandalonePage = true;
|
||||||
authMethod = new AuthMethod('password');
|
authMethod = new AuthMethod(AuthMethodType.Password, 0);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,13 +1,11 @@
|
|||||||
<ds-themed-loading *ngIf="(loading | async) || (isAuthenticated | async)" class="m-5"></ds-themed-loading>
|
<ds-themed-loading *ngIf="(loading | async) || (isAuthenticated | async)" class="m-5"></ds-themed-loading>
|
||||||
<div *ngIf="!(loading | async) && !(isAuthenticated | async)" class="px-4 py-3 mx-auto login-container">
|
<div *ngIf="!(loading | async) && !(isAuthenticated | async)" class="px-4 py-3 mx-auto login-container">
|
||||||
<ng-container *ngFor="let authMethod of (authMethods); let i = index">
|
<ng-container *ngFor="let authMethod of getOrderedAuthMethods(authMethods | async); let last = last">
|
||||||
<div *ngIf="i === 1" class="text-center mt-2">
|
<div [class.d-none]="contentRef.innerText?.trim().length === 0">
|
||||||
<span class="align-middle">{{"login.form.or-divider" | translate}}</span>
|
<div #contentRef>
|
||||||
|
<ds-log-in-container [authMethod]="authMethod" [isStandalonePage]="isStandalonePage"></ds-log-in-container>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="!last" class="dropdown-divider my-2"></div>
|
||||||
</div>
|
</div>
|
||||||
<ds-log-in-container [authMethod]="authMethod" [isStandalonePage]="isStandalonePage"></ds-log-in-container>
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<div class="dropdown-divider"></div>
|
|
||||||
<a class="dropdown-item" *ngIf="canRegister$ | async" [routerLink]="[getRegisterRoute()]" [attr.data-test]="'register' | dsBrowserOnly">{{"login.form.new-user" | translate}}</a>
|
|
||||||
<a class="dropdown-item" [routerLink]="[getForgotRoute()]" [attr.data-test]="'forgot' | dsBrowserOnly">{{"login.form.forgot-password" | translate}}</a>
|
|
||||||
</div>
|
</div>
|
||||||
|
@@ -50,7 +50,7 @@ describe('LogInComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// refine the test module by declaring the test component
|
// refine the test module by declaring the test component
|
||||||
TestBed.configureTestingModule({
|
void TestBed.configureTestingModule({
|
||||||
imports: [
|
imports: [
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { Component, Input, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
||||||
import { Observable } from 'rxjs';
|
import { map, Observable } from 'rxjs';
|
||||||
import { select, Store } from '@ngrx/store';
|
import { select, Store } from '@ngrx/store';
|
||||||
import { AuthMethod } from '../../core/auth/models/auth.method';
|
import { AuthMethod } from '../../core/auth/models/auth.method';
|
||||||
import {
|
import {
|
||||||
@@ -8,11 +8,8 @@ import {
|
|||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
isAuthenticationLoading
|
isAuthenticationLoading
|
||||||
} from '../../core/auth/selectors';
|
} from '../../core/auth/selectors';
|
||||||
import { getForgotPasswordRoute, getRegisterRoute } from '../../app-routing-paths';
|
|
||||||
import { hasValue } from '../empty.util';
|
import { hasValue } from '../empty.util';
|
||||||
import { AuthService } from '../../core/auth/auth.service';
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
|
||||||
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
|
||||||
import { CoreState } from '../../core/core-state.model';
|
import { CoreState } from '../../core/core-state.model';
|
||||||
import { AuthMethodType } from '../../core/auth/models/auth.method-type';
|
import { AuthMethodType } from '../../core/auth/models/auth.method-type';
|
||||||
|
|
||||||
@@ -23,7 +20,8 @@ import { AuthMethodType } from '../../core/auth/models/auth.method-type';
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-log-in',
|
selector: 'ds-log-in',
|
||||||
templateUrl: './log-in.component.html',
|
templateUrl: './log-in.component.html',
|
||||||
styleUrls: ['./log-in.component.scss']
|
styleUrls: ['./log-in.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class LogInComponent implements OnInit {
|
export class LogInComponent implements OnInit {
|
||||||
|
|
||||||
@@ -37,7 +35,7 @@ export class LogInComponent implements OnInit {
|
|||||||
* The list of authentication methods available
|
* The list of authentication methods available
|
||||||
* @type {AuthMethod[]}
|
* @type {AuthMethod[]}
|
||||||
*/
|
*/
|
||||||
public authMethods: AuthMethod[];
|
public authMethods: Observable<AuthMethod[]>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether user is authenticated.
|
* Whether user is authenticated.
|
||||||
@@ -51,24 +49,17 @@ export class LogInComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
public loading: Observable<boolean>;
|
public loading: Observable<boolean>;
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether or not the current user (or anonymous) is authorized to register an account
|
|
||||||
*/
|
|
||||||
canRegister$: Observable<boolean>;
|
|
||||||
|
|
||||||
constructor(private store: Store<CoreState>,
|
constructor(private store: Store<CoreState>,
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private authorizationService: AuthorizationDataService) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
this.authMethods = this.store.pipe(
|
||||||
this.store.pipe(
|
|
||||||
select(getAuthenticationMethods),
|
select(getAuthenticationMethods),
|
||||||
).subscribe(methods => {
|
|
||||||
// ignore the ip authentication method when it's returned by the backend
|
// ignore the ip authentication method when it's returned by the backend
|
||||||
this.authMethods = methods.filter(a => a.authMethodType !== AuthMethodType.Ip);
|
map((methods: AuthMethod[]) => methods.filter((authMethod: AuthMethod) => authMethod.authMethodType !== AuthMethodType.Ip)),
|
||||||
});
|
);
|
||||||
|
|
||||||
// set loading
|
// set loading
|
||||||
this.loading = this.store.pipe(select(isAuthenticationLoading));
|
this.loading = this.store.pipe(select(isAuthenticationLoading));
|
||||||
@@ -82,15 +73,18 @@ export class LogInComponent implements OnInit {
|
|||||||
this.authService.clearRedirectUrl();
|
this.authService.clearRedirectUrl();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.canRegister$ = this.authorizationService.isAuthorized(FeatureID.EPersonRegistration);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getRegisterRoute() {
|
/**
|
||||||
return getRegisterRoute();
|
* Returns an ordered list of {@link AuthMethod}s based on their position.
|
||||||
}
|
*
|
||||||
|
* @param authMethods The {@link AuthMethod}s to sort
|
||||||
getForgotRoute() {
|
*/
|
||||||
return getForgotPasswordRoute();
|
getOrderedAuthMethods(authMethods: AuthMethod[] | null): AuthMethod[] {
|
||||||
|
if (hasValue(authMethods)) {
|
||||||
|
return [...authMethods].sort((method1: AuthMethod, method2: AuthMethod) => method1.position - method2.position);
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,3 +1,3 @@
|
|||||||
<button class="btn btn-lg btn-primary btn-block mt-2 text-white" (click)="redirectToExternalProvider()">
|
<button class="btn btn-lg btn-primary btn-block text-white" (click)="redirectToExternalProvider()">
|
||||||
<i class="fas fa-sign-in-alt"></i> {{getButtonLabel() | translate}}
|
<i class="fas fa-sign-in-alt"></i> {{getButtonLabel() | translate}}
|
||||||
</button>
|
</button>
|
||||||
|
@@ -3,11 +3,8 @@ import { ActivatedRoute, Router } from '@angular/router';
|
|||||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
|
||||||
import { provideMockStore } from '@ngrx/store/testing';
|
import { provideMockStore } from '@ngrx/store/testing';
|
||||||
import { Store, StoreModule } from '@ngrx/store';
|
import { StoreModule } from '@ngrx/store';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
import { EPerson } from '../../../../core/eperson/models/eperson.model';
|
|
||||||
import { EPersonMock } from '../../../testing/eperson.mock';
|
|
||||||
import { authReducer } from '../../../../core/auth/auth.reducer';
|
import { authReducer } from '../../../../core/auth/auth.reducer';
|
||||||
import { AuthService } from '../../../../core/auth/auth.service';
|
import { AuthService } from '../../../../core/auth/auth.service';
|
||||||
import { AuthServiceStub } from '../../../testing/auth-service.stub';
|
import { AuthServiceStub } from '../../../testing/auth-service.stub';
|
||||||
@@ -25,17 +22,14 @@ describe('LogInExternalProviderComponent', () => {
|
|||||||
|
|
||||||
let component: LogInExternalProviderComponent;
|
let component: LogInExternalProviderComponent;
|
||||||
let fixture: ComponentFixture<LogInExternalProviderComponent>;
|
let fixture: ComponentFixture<LogInExternalProviderComponent>;
|
||||||
let page: Page;
|
|
||||||
let user: EPerson;
|
|
||||||
let componentAsAny: any;
|
let componentAsAny: any;
|
||||||
let setHrefSpy;
|
let setHrefSpy;
|
||||||
let orcidBaseUrl;
|
let orcidBaseUrl: string;
|
||||||
let location;
|
let location: string;
|
||||||
let initialState: any;
|
let initialState: any;
|
||||||
let hardRedirectService: HardRedirectService;
|
let hardRedirectService: HardRedirectService;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
user = EPersonMock;
|
|
||||||
orcidBaseUrl = 'dspace-rest.test/orcid?redirectUrl=';
|
orcidBaseUrl = 'dspace-rest.test/orcid?redirectUrl=';
|
||||||
location = orcidBaseUrl + 'http://dspace-angular.test/home';
|
location = orcidBaseUrl + 'http://dspace-angular.test/home';
|
||||||
|
|
||||||
@@ -59,7 +53,7 @@ describe('LogInExternalProviderComponent', () => {
|
|||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
// refine the test module by declaring the test component
|
// refine the test module by declaring the test component
|
||||||
TestBed.configureTestingModule({
|
void TestBed.configureTestingModule({
|
||||||
imports: [
|
imports: [
|
||||||
StoreModule.forRoot({ auth: authReducer }, storeModuleConfig),
|
StoreModule.forRoot({ auth: authReducer }, storeModuleConfig),
|
||||||
TranslateModule.forRoot()
|
TranslateModule.forRoot()
|
||||||
@@ -69,7 +63,7 @@ describe('LogInExternalProviderComponent', () => {
|
|||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: AuthService, useClass: AuthServiceStub },
|
{ provide: AuthService, useClass: AuthServiceStub },
|
||||||
{ provide: 'authMethodProvider', useValue: new AuthMethod(AuthMethodType.Orcid, location) },
|
{ provide: 'authMethodProvider', useValue: new AuthMethod(AuthMethodType.Orcid, 0, location) },
|
||||||
{ provide: 'isStandalonePage', useValue: true },
|
{ provide: 'isStandalonePage', useValue: true },
|
||||||
{ provide: NativeWindowService, useFactory: NativeWindowMockFactory },
|
{ provide: NativeWindowService, useFactory: NativeWindowMockFactory },
|
||||||
{ provide: Router, useValue: new RouterStub() },
|
{ provide: Router, useValue: new RouterStub() },
|
||||||
@@ -94,7 +88,6 @@ describe('LogInExternalProviderComponent', () => {
|
|||||||
componentAsAny = component;
|
componentAsAny = component;
|
||||||
|
|
||||||
// create page
|
// create page
|
||||||
page = new Page(component, fixture);
|
|
||||||
setHrefSpy = spyOnProperty(componentAsAny._window.nativeWindow.location, 'href', 'set').and.callThrough();
|
setHrefSpy = spyOnProperty(componentAsAny._window.nativeWindow.location, 'href', 'set').and.callThrough();
|
||||||
|
|
||||||
});
|
});
|
||||||
@@ -130,25 +123,3 @@ describe('LogInExternalProviderComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* I represent the DOM elements and attach spies.
|
|
||||||
*
|
|
||||||
* @class Page
|
|
||||||
*/
|
|
||||||
class Page {
|
|
||||||
|
|
||||||
public emailInput: HTMLInputElement;
|
|
||||||
public navigateSpy: jasmine.Spy;
|
|
||||||
public passwordInput: HTMLInputElement;
|
|
||||||
|
|
||||||
constructor(private component: LogInExternalProviderComponent, private fixture: ComponentFixture<LogInExternalProviderComponent>) {
|
|
||||||
// use injector to get services
|
|
||||||
const injector = fixture.debugElement.injector;
|
|
||||||
const store = injector.get(Store);
|
|
||||||
|
|
||||||
// add spies
|
|
||||||
this.navigateSpy = spyOn(store, 'dispatch');
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
@@ -28,3 +28,12 @@
|
|||||||
<button class="btn btn-lg btn-primary btn-block mt-3" type="submit" [attr.data-test]="'login-button' | dsBrowserOnly"
|
<button class="btn btn-lg btn-primary btn-block mt-3" type="submit" [attr.data-test]="'login-button' | dsBrowserOnly"
|
||||||
[disabled]="!form.valid"><i class="fas fa-sign-in-alt"></i> {{"login.form.submit" | translate}}</button>
|
[disabled]="!form.valid"><i class="fas fa-sign-in-alt"></i> {{"login.form.submit" | translate}}</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<div class="mt-2">
|
||||||
|
<a class="dropdown-item" *ngIf="canRegister$ | async" [routerLink]="[getRegisterRoute()]" [attr.data-test]="'register' | dsBrowserOnly">
|
||||||
|
{{ 'login.form.new-user' | translate }}
|
||||||
|
</a>
|
||||||
|
<a class="dropdown-item" [routerLink]="[getForgotRoute()]" [attr.data-test]="'forgot' | dsBrowserOnly">
|
||||||
|
{{ 'login.form.forgot-password' | translate }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
@@ -11,3 +11,7 @@
|
|||||||
border-top-right-radius: 0;
|
border-top-right-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dropdown-item {
|
||||||
|
white-space: normal;
|
||||||
|
padding: .25rem .75rem;
|
||||||
|
}
|
||||||
|
@@ -8,8 +8,6 @@ import { Store, StoreModule } from '@ngrx/store';
|
|||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
import { LogInPasswordComponent } from './log-in-password.component';
|
import { LogInPasswordComponent } from './log-in-password.component';
|
||||||
import { EPerson } from '../../../../core/eperson/models/eperson.model';
|
|
||||||
import { EPersonMock } from '../../../testing/eperson.mock';
|
|
||||||
import { authReducer } from '../../../../core/auth/auth.reducer';
|
import { authReducer } from '../../../../core/auth/auth.reducer';
|
||||||
import { AuthService } from '../../../../core/auth/auth.service';
|
import { AuthService } from '../../../../core/auth/auth.service';
|
||||||
import { AuthServiceStub } from '../../../testing/auth-service.stub';
|
import { AuthServiceStub } from '../../../testing/auth-service.stub';
|
||||||
@@ -18,19 +16,18 @@ import { AuthMethod } from '../../../../core/auth/models/auth.method';
|
|||||||
import { AuthMethodType } from '../../../../core/auth/models/auth.method-type';
|
import { AuthMethodType } from '../../../../core/auth/models/auth.method-type';
|
||||||
import { HardRedirectService } from '../../../../core/services/hard-redirect.service';
|
import { HardRedirectService } from '../../../../core/services/hard-redirect.service';
|
||||||
import { BrowserOnlyMockPipe } from '../../../testing/browser-only-mock.pipe';
|
import { BrowserOnlyMockPipe } from '../../../testing/browser-only-mock.pipe';
|
||||||
|
import { AuthorizationDataService } from '../../../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { AuthorizationDataServiceStub } from '../../../testing/authorization-service.stub';
|
||||||
|
|
||||||
describe('LogInPasswordComponent', () => {
|
describe('LogInPasswordComponent', () => {
|
||||||
|
|
||||||
let component: LogInPasswordComponent;
|
let component: LogInPasswordComponent;
|
||||||
let fixture: ComponentFixture<LogInPasswordComponent>;
|
let fixture: ComponentFixture<LogInPasswordComponent>;
|
||||||
let page: Page;
|
let page: Page;
|
||||||
let user: EPerson;
|
|
||||||
let initialState: any;
|
let initialState: any;
|
||||||
let hardRedirectService: HardRedirectService;
|
let hardRedirectService: HardRedirectService;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
user = EPersonMock;
|
|
||||||
|
|
||||||
hardRedirectService = jasmine.createSpyObj('hardRedirectService', {
|
hardRedirectService = jasmine.createSpyObj('hardRedirectService', {
|
||||||
getCurrentRoute: {}
|
getCurrentRoute: {}
|
||||||
});
|
});
|
||||||
@@ -50,7 +47,7 @@ describe('LogInPasswordComponent', () => {
|
|||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
// refine the test module by declaring the test component
|
// refine the test module by declaring the test component
|
||||||
TestBed.configureTestingModule({
|
void TestBed.configureTestingModule({
|
||||||
imports: [
|
imports: [
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
@@ -63,7 +60,8 @@ describe('LogInPasswordComponent', () => {
|
|||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: AuthService, useClass: AuthServiceStub },
|
{ provide: AuthService, useClass: AuthServiceStub },
|
||||||
{ provide: 'authMethodProvider', useValue: new AuthMethod(AuthMethodType.Password) },
|
{ provide: AuthorizationDataService, useClass: AuthorizationDataServiceStub },
|
||||||
|
{ provide: 'authMethodProvider', useValue: new AuthMethod(AuthMethodType.Password, 0) },
|
||||||
{ provide: 'isStandalonePage', useValue: true },
|
{ provide: 'isStandalonePage', useValue: true },
|
||||||
{ provide: HardRedirectService, useValue: hardRedirectService },
|
{ provide: HardRedirectService, useValue: hardRedirectService },
|
||||||
provideMockStore({ initialState }),
|
provideMockStore({ initialState }),
|
||||||
@@ -76,7 +74,7 @@ describe('LogInPasswordComponent', () => {
|
|||||||
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
// create component and test fixture
|
// create component and test fixture
|
||||||
fixture = TestBed.createComponent(LogInPasswordComponent);
|
fixture = TestBed.createComponent(LogInPasswordComponent);
|
||||||
|
|
||||||
@@ -87,10 +85,8 @@ describe('LogInPasswordComponent', () => {
|
|||||||
page = new Page(component, fixture);
|
page = new Page(component, fixture);
|
||||||
|
|
||||||
// verify the fixture is stable (no pending tasks)
|
// verify the fixture is stable (no pending tasks)
|
||||||
fixture.whenStable().then(() => {
|
await fixture.whenStable();
|
||||||
page.addPageElements();
|
page.addPageElements();
|
||||||
});
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create a FormGroup comprised of FormControls', () => {
|
it('should create a FormGroup comprised of FormControls', () => {
|
||||||
|
@@ -15,6 +15,9 @@ import { AuthMethod } from '../../../../core/auth/models/auth.method';
|
|||||||
import { AuthService } from '../../../../core/auth/auth.service';
|
import { AuthService } from '../../../../core/auth/auth.service';
|
||||||
import { HardRedirectService } from '../../../../core/services/hard-redirect.service';
|
import { HardRedirectService } from '../../../../core/services/hard-redirect.service';
|
||||||
import { CoreState } from '../../../../core/core-state.model';
|
import { CoreState } from '../../../../core/core-state.model';
|
||||||
|
import { getForgotPasswordRoute, getRegisterRoute } from '../../../../app-routing-paths';
|
||||||
|
import { FeatureID } from '../../../../core/data/feature-authorization/feature-id';
|
||||||
|
import { AuthorizationDataService } from '../../../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* /users/sign-in
|
* /users/sign-in
|
||||||
@@ -66,21 +69,18 @@ export class LogInPasswordComponent implements OnInit {
|
|||||||
public form: UntypedFormGroup;
|
public form: UntypedFormGroup;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @constructor
|
* Whether the current user (or anonymous) is authorized to register an account
|
||||||
* @param {AuthMethod} injectedAuthMethodModel
|
|
||||||
* @param {boolean} isStandalonePage
|
|
||||||
* @param {AuthService} authService
|
|
||||||
* @param {HardRedirectService} hardRedirectService
|
|
||||||
* @param {FormBuilder} formBuilder
|
|
||||||
* @param {Store<State>} store
|
|
||||||
*/
|
*/
|
||||||
|
public canRegister$: Observable<boolean>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject('authMethodProvider') public injectedAuthMethodModel: AuthMethod,
|
@Inject('authMethodProvider') public injectedAuthMethodModel: AuthMethod,
|
||||||
@Inject('isStandalonePage') public isStandalonePage: boolean,
|
@Inject('isStandalonePage') public isStandalonePage: boolean,
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private hardRedirectService: HardRedirectService,
|
private hardRedirectService: HardRedirectService,
|
||||||
private formBuilder: UntypedFormBuilder,
|
private formBuilder: UntypedFormBuilder,
|
||||||
private store: Store<CoreState>
|
protected store: Store<CoreState>,
|
||||||
|
protected authorizationService: AuthorizationDataService,
|
||||||
) {
|
) {
|
||||||
this.authMethod = injectedAuthMethodModel;
|
this.authMethod = injectedAuthMethodModel;
|
||||||
}
|
}
|
||||||
@@ -115,6 +115,15 @@ export class LogInPasswordComponent implements OnInit {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.canRegister$ = this.authorizationService.isAuthorized(FeatureID.EPersonRegistration);
|
||||||
|
}
|
||||||
|
|
||||||
|
getRegisterRoute() {
|
||||||
|
return getRegisterRoute();
|
||||||
|
}
|
||||||
|
|
||||||
|
getForgotRoute() {
|
||||||
|
return getForgotPasswordRoute();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
<div class="simple-view-element" [class.d-none]="hideIfNoTextContent && content.textContent.trim().length === 0">
|
<div class="simple-view-element" [class.d-none]="hideIfNoTextContent && content.textContent.trim().length === 0">
|
||||||
<h5 class="simple-view-element-header" *ngIf="label">{{ label }}</h5>
|
<h2 class="simple-view-element-header" *ngIf="label">{{ label }}</h2>
|
||||||
<div #content class="simple-view-element-body">
|
<div #content class="simple-view-element-body">
|
||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -2,4 +2,7 @@
|
|||||||
.simple-view-element {
|
.simple-view-element {
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
.simple-view-element-header {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -28,6 +28,7 @@ describe('SearchFormComponent', () => {
|
|||||||
const searchService = new SearchServiceStub();
|
const searchService = new SearchServiceStub();
|
||||||
const paginationService = new PaginationServiceStub();
|
const paginationService = new PaginationServiceStub();
|
||||||
const searchConfigService = { paginationID: 'test-id' };
|
const searchConfigService = { paginationID: 'test-id' };
|
||||||
|
const firstPage = { 'spc.page': 1 };
|
||||||
const dspaceObjectService = {
|
const dspaceObjectService = {
|
||||||
findById: () => createSuccessfulRemoteDataObject$(undefined),
|
findById: () => createSuccessfulRemoteDataObject$(undefined),
|
||||||
};
|
};
|
||||||
@@ -104,16 +105,16 @@ describe('SearchFormComponent', () => {
|
|||||||
const scope = 'MCU';
|
const scope = 'MCU';
|
||||||
let searchQuery = {};
|
let searchQuery = {};
|
||||||
|
|
||||||
it('should navigate to the search page even when no parameters are provided', () => {
|
it('should navigate to the search first page even when no parameters are provided', () => {
|
||||||
comp.updateSearch(searchQuery);
|
comp.updateSearch(searchQuery);
|
||||||
|
|
||||||
expect(router.navigate).toHaveBeenCalledWith(comp.getSearchLinkParts(), {
|
expect(router.navigate).toHaveBeenCalledWith(comp.getSearchLinkParts(), {
|
||||||
queryParams: searchQuery,
|
queryParams: { ...searchQuery, ...firstPage },
|
||||||
queryParamsHandling: 'merge'
|
queryParamsHandling: 'merge'
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should navigate to the search page with parameters only query if only query is provided', () => {
|
it('should navigate to the search first page with parameters only query if only query is provided', () => {
|
||||||
searchQuery = {
|
searchQuery = {
|
||||||
query: query
|
query: query
|
||||||
};
|
};
|
||||||
@@ -121,12 +122,12 @@ describe('SearchFormComponent', () => {
|
|||||||
comp.updateSearch(searchQuery);
|
comp.updateSearch(searchQuery);
|
||||||
|
|
||||||
expect(router.navigate).toHaveBeenCalledWith(comp.getSearchLinkParts(), {
|
expect(router.navigate).toHaveBeenCalledWith(comp.getSearchLinkParts(), {
|
||||||
queryParams: searchQuery,
|
queryParams: { ...searchQuery, ...firstPage },
|
||||||
queryParamsHandling: 'merge'
|
queryParamsHandling: 'merge'
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should navigate to the search page with parameters only query if only scope is provided', () => {
|
it('should navigate to the search first page with parameters only query if only scope is provided', () => {
|
||||||
searchQuery = {
|
searchQuery = {
|
||||||
scope: scope
|
scope: scope
|
||||||
};
|
};
|
||||||
@@ -134,7 +135,7 @@ describe('SearchFormComponent', () => {
|
|||||||
comp.updateSearch(searchQuery);
|
comp.updateSearch(searchQuery);
|
||||||
|
|
||||||
expect(router.navigate).toHaveBeenCalledWith(comp.getSearchLinkParts(), {
|
expect(router.navigate).toHaveBeenCalledWith(comp.getSearchLinkParts(), {
|
||||||
queryParams: searchQuery,
|
queryParams: {...searchQuery, ...firstPage},
|
||||||
queryParamsHandling: 'merge'
|
queryParamsHandling: 'merge'
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -114,7 +114,14 @@ export class SearchFormComponent implements OnChanges {
|
|||||||
* @param data Updated parameters
|
* @param data Updated parameters
|
||||||
*/
|
*/
|
||||||
updateSearch(data: any) {
|
updateSearch(data: any) {
|
||||||
const queryParams = Object.assign({}, data);
|
const goToFirstPage = { 'spc.page': 1 };
|
||||||
|
|
||||||
|
const queryParams = Object.assign(
|
||||||
|
{
|
||||||
|
...goToFirstPage
|
||||||
|
},
|
||||||
|
data
|
||||||
|
);
|
||||||
|
|
||||||
void this.router.navigate(this.getSearchLinkParts(), {
|
void this.router.navigate(this.getSearchLinkParts(), {
|
||||||
queryParams: queryParams,
|
queryParams: queryParams,
|
||||||
|
@@ -94,6 +94,19 @@ export class SearchExportCsvComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (isNotEmpty(this.searchConfig.fixedFilter)) {
|
||||||
|
const fixedFilter = this.searchConfig.fixedFilter.substring(2);
|
||||||
|
const keyAndValue = fixedFilter.split('=');
|
||||||
|
if (keyAndValue.length > 1) {
|
||||||
|
const key = keyAndValue[0];
|
||||||
|
const valueAndOperator = keyAndValue[1].split(',');
|
||||||
|
if (valueAndOperator.length > 1) {
|
||||||
|
const value = valueAndOperator[0];
|
||||||
|
const operator = valueAndOperator[1];
|
||||||
|
parameters.push({name: '-f', value: `${key},${operator}=${value}`});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.scriptDataService.invoke('metadata-export-search', parameters, []).pipe(
|
this.scriptDataService.invoke('metadata-export-search', parameters, []).pipe(
|
||||||
|
@@ -6,9 +6,9 @@
|
|||||||
[attr.aria-label]="(((collapsed$ | async) ? 'search.filters.filter.expand' : 'search.filters.filter.collapse') | translate) + ' ' + (('search.filters.filter.' + filter.name + '.head') | translate | lowercase)"
|
[attr.aria-label]="(((collapsed$ | async) ? 'search.filters.filter.expand' : 'search.filters.filter.collapse') | translate) + ' ' + (('search.filters.filter.' + filter.name + '.head') | translate | lowercase)"
|
||||||
[attr.data-test]="'filter-toggle' | dsBrowserOnly"
|
[attr.data-test]="'filter-toggle' | dsBrowserOnly"
|
||||||
>
|
>
|
||||||
<h5 class="d-inline-block mb-0">
|
<h4 class="d-inline-block mb-0">
|
||||||
{{'search.filters.filter.' + filter.name + '.head'| translate}}
|
{{'search.filters.filter.' + filter.name + '.head'| translate}}
|
||||||
</h5>
|
</h4>
|
||||||
<span class="filter-toggle flex-grow-1 fas p-auto"
|
<span class="filter-toggle flex-grow-1 fas p-auto"
|
||||||
[ngClass]="(collapsed$ | async) ? 'fa-plus' : 'fa-minus'"
|
[ngClass]="(collapsed$ | async) ? 'fa-plus' : 'fa-minus'"
|
||||||
[title]="((collapsed$ | async) ? 'search.filters.filter.expand' : 'search.filters.filter.collapse') | translate">
|
[title]="((collapsed$ | async) ? 'search.filters.filter.expand' : 'search.filters.filter.collapse') | translate">
|
||||||
|
@@ -9,8 +9,8 @@
|
|||||||
</span>
|
</span>
|
||||||
<input type="text" [(ngModel)]="range[0]" [name]="filterConfig.paramName + '.min'"
|
<input type="text" [(ngModel)]="range[0]" [name]="filterConfig.paramName + '.min'"
|
||||||
class="form-control" (blur)="onSubmit()"
|
class="form-control" (blur)="onSubmit()"
|
||||||
aria-label="Mininum value"
|
[attr.aria-label]="minLabel"
|
||||||
[placeholder]="'search.filters.filter.' + filterConfig.name + '.min.placeholder' | translate"
|
[placeholder]="minLabel"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -21,8 +21,8 @@
|
|||||||
</span>
|
</span>
|
||||||
<input type="text" [(ngModel)]="range[1]" [name]="filterConfig.paramName + '.max'"
|
<input type="text" [(ngModel)]="range[1]" [name]="filterConfig.paramName + '.max'"
|
||||||
class="form-control" (blur)="onSubmit()"
|
class="form-control" (blur)="onSubmit()"
|
||||||
aria-label="Maximum value"
|
[attr.aria-label]="maxLabel"
|
||||||
[placeholder]="'search.filters.filter.' + filterConfig.name + '.max.placeholder' | translate"
|
[placeholder]="maxLabel"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<ng-container *ngIf="shouldShowSlider()">
|
<ng-container *ngIf="shouldShowSlider()">
|
||||||
<nouislider [connect]="true" [min]="min" [max]="max" [step]="1"
|
<nouislider [connect]="true" [config]="config" [min]="min" [max]="max" [step]="1"
|
||||||
[dsDebounce]="250" (onDebounce)="onSubmit()"
|
[dsDebounce]="250" (onDebounce)="onSubmit()"
|
||||||
(keydown)="startKeyboardControl()" (keyup)="stopKeyboardControl()"
|
(keydown)="startKeyboardControl()" (keyup)="stopKeyboardControl()"
|
||||||
[(ngModel)]="range" ngDefaultControl>
|
[(ngModel)]="range" ngDefaultControl>
|
||||||
|
@@ -2,6 +2,7 @@ import { BehaviorSubject, combineLatest as observableCombineLatest, Subscription
|
|||||||
import { map, startWith } from 'rxjs/operators';
|
import { map, startWith } from 'rxjs/operators';
|
||||||
import { isPlatformBrowser } from '@angular/common';
|
import { isPlatformBrowser } from '@angular/common';
|
||||||
import { Component, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core';
|
import { Component, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { RemoteDataBuildService } from '../../../../../core/cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../../../../../core/cache/builders/remote-data-build.service';
|
||||||
import { FilterType } from '../../../models/filter-type.model';
|
import { FilterType } from '../../../models/filter-type.model';
|
||||||
import { renderFacetFor } from '../search-filter-type-decorator';
|
import { renderFacetFor } from '../search-filter-type-decorator';
|
||||||
@@ -53,11 +54,27 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple
|
|||||||
*/
|
*/
|
||||||
min = 1950;
|
min = 1950;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* i18n Label to use for minimum field
|
||||||
|
*/
|
||||||
|
minLabel: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fallback maximum for the range
|
* Fallback maximum for the range
|
||||||
*/
|
*/
|
||||||
max = new Date().getUTCFullYear();
|
max = new Date().getUTCFullYear();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* i18n Label to use for maximum field
|
||||||
|
*/
|
||||||
|
maxLabel: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base configuration for nouislider
|
||||||
|
* https://refreshless.com/nouislider/slider-options/
|
||||||
|
*/
|
||||||
|
config = {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current range of the filter
|
* The current range of the filter
|
||||||
*/
|
*/
|
||||||
@@ -78,6 +95,7 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple
|
|||||||
protected filterService: SearchFilterService,
|
protected filterService: SearchFilterService,
|
||||||
protected router: Router,
|
protected router: Router,
|
||||||
protected rdbs: RemoteDataBuildService,
|
protected rdbs: RemoteDataBuildService,
|
||||||
|
private translateService: TranslateService,
|
||||||
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService,
|
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService,
|
||||||
@Inject(IN_PLACE_SEARCH) public inPlaceSearch: boolean,
|
@Inject(IN_PLACE_SEARCH) public inPlaceSearch: boolean,
|
||||||
@Inject(FILTER_CONFIG) public filterConfig: SearchFilterConfig,
|
@Inject(FILTER_CONFIG) public filterConfig: SearchFilterConfig,
|
||||||
@@ -96,6 +114,8 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple
|
|||||||
super.ngOnInit();
|
super.ngOnInit();
|
||||||
this.min = yearFromString(this.filterConfig.minValue) || this.min;
|
this.min = yearFromString(this.filterConfig.minValue) || this.min;
|
||||||
this.max = yearFromString(this.filterConfig.maxValue) || this.max;
|
this.max = yearFromString(this.filterConfig.maxValue) || this.max;
|
||||||
|
this.minLabel = this.translateService.instant('search.filters.filter.' + this.filterConfig.name + '.min.placeholder');
|
||||||
|
this.maxLabel = this.translateService.instant('search.filters.filter.' + this.filterConfig.name + '.max.placeholder');
|
||||||
const iniMin = this.route.getQueryParameterValue(this.filterConfig.paramName + RANGE_FILTER_MIN_SUFFIX).pipe(startWith(undefined));
|
const iniMin = this.route.getQueryParameterValue(this.filterConfig.paramName + RANGE_FILTER_MIN_SUFFIX).pipe(startWith(undefined));
|
||||||
const iniMax = this.route.getQueryParameterValue(this.filterConfig.paramName + RANGE_FILTER_MAX_SUFFIX).pipe(startWith(undefined));
|
const iniMax = this.route.getQueryParameterValue(this.filterConfig.paramName + RANGE_FILTER_MAX_SUFFIX).pipe(startWith(undefined));
|
||||||
this.sub = observableCombineLatest(iniMin, iniMax).pipe(
|
this.sub = observableCombineLatest(iniMin, iniMax).pipe(
|
||||||
@@ -105,6 +125,15 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple
|
|||||||
return [minimum, maximum];
|
return [minimum, maximum];
|
||||||
})
|
})
|
||||||
).subscribe((minmax) => this.range = minmax);
|
).subscribe((minmax) => this.range = minmax);
|
||||||
|
|
||||||
|
// Default/base config for nouislider
|
||||||
|
this.config = {
|
||||||
|
// Ensure draggable handles have labels
|
||||||
|
handleAttributes: [
|
||||||
|
{ 'aria-label': this.minLabel },
|
||||||
|
{ 'aria-label': this.maxLabel },
|
||||||
|
],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
<div class="setting-option mb-3 p-3">
|
<div class="setting-option mb-3 p-3">
|
||||||
<h5><label for="{{id}}">{{label | translate}}</label></h5>
|
<h4><label for="{{id}}">{{label | translate}}</label></h4>
|
||||||
<select id="{{id}}" class="form-control" (change)="change.emit($event)">
|
<select id="{{id}}" class="form-control" (change)="change.emit($event)">
|
||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
</select>
|
</select>
|
||||||
|
@@ -6,10 +6,11 @@ import { EPerson } from '../../core/eperson/models/eperson.model';
|
|||||||
import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils';
|
||||||
import { AuthMethod } from '../../core/auth/models/auth.method';
|
import { AuthMethod } from '../../core/auth/models/auth.method';
|
||||||
import { hasValue } from '../empty.util';
|
import { hasValue } from '../empty.util';
|
||||||
|
import { AuthMethodType } from '../../core/auth/models/auth.method-type';
|
||||||
|
|
||||||
export const authMethodsMock = [
|
export const authMethodsMock: AuthMethod[] = [
|
||||||
new AuthMethod('password'),
|
new AuthMethod(AuthMethodType.Password, 0),
|
||||||
new AuthMethod('shibboleth', 'dspace.test/shibboleth')
|
new AuthMethod(AuthMethodType.Shibboleth, 1, 'dspace.test/shibboleth'),
|
||||||
];
|
];
|
||||||
|
|
||||||
export class AuthServiceStub {
|
export class AuthServiceStub {
|
||||||
|
@@ -29,7 +29,7 @@ export const themeStateSelector = createFeatureSelector<ThemeState>('theme');
|
|||||||
|
|
||||||
export const currentThemeSelector = createSelector(
|
export const currentThemeSelector = createSelector(
|
||||||
themeStateSelector,
|
themeStateSelector,
|
||||||
(state: ThemeState): string => hasValue(state) ? state.currentTheme : undefined
|
(state: ThemeState): string => hasValue(state) ? state.currentTheme : BASE_THEME_NAME,
|
||||||
);
|
);
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
@@ -240,14 +240,7 @@ export class ThemeService {
|
|||||||
if (hasValue(parentThemeName)) {
|
if (hasValue(parentThemeName)) {
|
||||||
// inherit the head tags of the parent theme
|
// inherit the head tags of the parent theme
|
||||||
return this.createHeadTags(parentThemeName);
|
return this.createHeadTags(parentThemeName);
|
||||||
}
|
} else {
|
||||||
const defaultThemeConfig = getDefaultThemeConfig();
|
|
||||||
const defaultThemeName = defaultThemeConfig.name;
|
|
||||||
if (
|
|
||||||
hasNoValue(defaultThemeName) ||
|
|
||||||
themeName === defaultThemeName ||
|
|
||||||
themeName === BASE_THEME_NAME
|
|
||||||
) {
|
|
||||||
// last resort, use fallback favicon.ico
|
// last resort, use fallback favicon.ico
|
||||||
return [
|
return [
|
||||||
this.createHeadTag({
|
this.createHeadTag({
|
||||||
@@ -260,9 +253,6 @@ export class ThemeService {
|
|||||||
})
|
})
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
// inherit the head tags of the default theme
|
|
||||||
return this.createHeadTags(defaultThemeConfig.name);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return headTagConfigs.map(this.createHeadTag.bind(this));
|
return headTagConfigs.map(this.createHeadTag.bind(this));
|
||||||
@@ -425,9 +415,10 @@ export class ThemeService {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private getActionForMatch(newTheme: Theme, currentThemeName: string): SetThemeAction | NoOpAction {
|
private getActionForMatch(newTheme: Theme, currentThemeName: string): SetThemeAction | NoOpAction {
|
||||||
if (hasValue(newTheme) && newTheme.config.name !== currentThemeName) {
|
const newThemeName: string = newTheme?.config.name ?? BASE_THEME_NAME;
|
||||||
|
if (newThemeName !== currentThemeName) {
|
||||||
// If we have a match, and it isn't already the active theme, set it as the new theme
|
// If we have a match, and it isn't already the active theme, set it as the new theme
|
||||||
return new SetThemeAction(newTheme.config.name);
|
return new SetThemeAction(newThemeName);
|
||||||
} else {
|
} else {
|
||||||
// Otherwise, do nothing
|
// Otherwise, do nothing
|
||||||
return new NoOpAction();
|
return new NoOpAction();
|
||||||
|
@@ -1,10 +1,10 @@
|
|||||||
import {
|
import {
|
||||||
|
AfterViewInit,
|
||||||
Component,
|
Component,
|
||||||
ViewChild,
|
ViewChild,
|
||||||
ViewContainerRef,
|
ViewContainerRef,
|
||||||
ComponentRef,
|
ComponentRef,
|
||||||
SimpleChanges,
|
SimpleChanges,
|
||||||
OnInit,
|
|
||||||
OnDestroy,
|
OnDestroy,
|
||||||
ComponentFactoryResolver,
|
ComponentFactoryResolver,
|
||||||
ChangeDetectorRef,
|
ChangeDetectorRef,
|
||||||
@@ -24,7 +24,7 @@ import { BASE_THEME_NAME } from './theme.constants';
|
|||||||
styleUrls: ['./themed.component.scss'],
|
styleUrls: ['./themed.component.scss'],
|
||||||
templateUrl: './themed.component.html',
|
templateUrl: './themed.component.html',
|
||||||
})
|
})
|
||||||
export abstract class ThemedComponent<T> implements OnInit, OnDestroy, OnChanges {
|
export abstract class ThemedComponent<T> implements AfterViewInit, OnDestroy, OnChanges {
|
||||||
@ViewChild('vcr', { read: ViewContainerRef }) vcr: ViewContainerRef;
|
@ViewChild('vcr', { read: ViewContainerRef }) vcr: ViewContainerRef;
|
||||||
@ViewChild('content') themedElementContent: ElementRef;
|
@ViewChild('content') themedElementContent: ElementRef;
|
||||||
protected compRef: ComponentRef<T>;
|
protected compRef: ComponentRef<T>;
|
||||||
@@ -74,8 +74,7 @@ export abstract class ThemedComponent<T> implements OnInit, OnDestroy, OnChanges
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngAfterViewInit(): void {
|
||||||
this.destroyComponentInstance();
|
|
||||||
this.initComponentInstance();
|
this.initComponentInstance();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,8 +95,6 @@ export abstract class ThemedComponent<T> implements OnInit, OnDestroy, OnChanges
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (hasNoValue(this.lazyLoadObs)) {
|
if (hasNoValue(this.lazyLoadObs)) {
|
||||||
this.destroyComponentInstance();
|
|
||||||
|
|
||||||
this.lazyLoadObs = combineLatest([
|
this.lazyLoadObs = combineLatest([
|
||||||
observableOf(changes),
|
observableOf(changes),
|
||||||
this.resolveThemedComponent(this.themeService.getThemeName()).pipe(
|
this.resolveThemedComponent(this.themeService.getThemeName()).pipe(
|
||||||
@@ -120,6 +117,7 @@ export abstract class ThemedComponent<T> implements OnInit, OnDestroy, OnChanges
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.lazyLoadSub = this.lazyLoadObs.subscribe(([simpleChanges, constructor]: [SimpleChanges, GenericConstructor<T>]) => {
|
this.lazyLoadSub = this.lazyLoadObs.subscribe(([simpleChanges, constructor]: [SimpleChanges, GenericConstructor<T>]) => {
|
||||||
|
this.destroyComponentInstance();
|
||||||
const factory = this.resolver.resolveComponentFactory(constructor);
|
const factory = this.resolver.resolveComponentFactory(constructor);
|
||||||
this.compRef = this.vcr.createComponent(factory, undefined, undefined, [this.themedElementContent.nativeElement.childNodes]);
|
this.compRef = this.vcr.createComponent(factory, undefined, undefined, [this.themedElementContent.nativeElement.childNodes]);
|
||||||
if (hasValue(simpleChanges)) {
|
if (hasValue(simpleChanges)) {
|
||||||
|
@@ -67,7 +67,9 @@ export class MarkdownPipe implements PipeTransform {
|
|||||||
// sanitize-html doesn't let through SVG by default, so we extend its allowlists to cover MathJax SVG
|
// sanitize-html doesn't let through SVG by default, so we extend its allowlists to cover MathJax SVG
|
||||||
allowedTags: [
|
allowedTags: [
|
||||||
...sanitizeHtml.defaults.allowedTags,
|
...sanitizeHtml.defaults.allowedTags,
|
||||||
'mjx-container', 'svg', 'g', 'path', 'rect', 'text'
|
'mjx-container', 'svg', 'g', 'path', 'rect', 'text',
|
||||||
|
// Also let the mjx-assistive-mml tag (and it's children) through (for screen readers)
|
||||||
|
'mjx-assistive-mml', 'math', 'mrow', 'mi',
|
||||||
],
|
],
|
||||||
allowedAttributes: {
|
allowedAttributes: {
|
||||||
...sanitizeHtml.defaults.allowedAttributes,
|
...sanitizeHtml.defaults.allowedAttributes,
|
||||||
@@ -88,7 +90,16 @@ export class MarkdownPipe implements PipeTransform {
|
|||||||
],
|
],
|
||||||
text: [
|
text: [
|
||||||
'transform', 'font-size'
|
'transform', 'font-size'
|
||||||
]
|
],
|
||||||
|
'mjx-assistive-mml': [
|
||||||
|
'unselectable', 'display', 'style',
|
||||||
|
],
|
||||||
|
math: [
|
||||||
|
'xmlns',
|
||||||
|
],
|
||||||
|
mrow: [
|
||||||
|
'data-mjx-texclass',
|
||||||
|
],
|
||||||
},
|
},
|
||||||
parser: {
|
parser: {
|
||||||
lowerCaseAttributeNames: false,
|
lowerCaseAttributeNames: false,
|
||||||
|
@@ -3,7 +3,7 @@ import { Component, Injector, Input, OnInit, ViewChild } from '@angular/core';
|
|||||||
import { SectionsDirective } from '../sections.directive';
|
import { SectionsDirective } from '../sections.directive';
|
||||||
import { SectionDataObject } from '../models/section-data.model';
|
import { SectionDataObject } from '../models/section-data.model';
|
||||||
import { rendersSectionType } from '../sections-decorator';
|
import { rendersSectionType } from '../sections-decorator';
|
||||||
import { AlertType } from '../../../shared/alert/aletr-type';
|
import { AlertType } from '../../../shared/alert/alert-type';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component represents a section that contains the submission license form.
|
* This component represents a section that contains the submission license form.
|
||||||
|
@@ -7,7 +7,7 @@ import { SectionModelComponent } from '../models/section.model';
|
|||||||
import { renderSectionFor } from '../sections-decorator';
|
import { renderSectionFor } from '../sections-decorator';
|
||||||
import { SectionDataObject } from '../models/section-data.model';
|
import { SectionDataObject } from '../models/section-data.model';
|
||||||
import { SubmissionService } from '../../submission.service';
|
import { SubmissionService } from '../../submission.service';
|
||||||
import { AlertType } from '../../../shared/alert/aletr-type';
|
import { AlertType } from '../../../shared/alert/alert-type';
|
||||||
import { SectionsService } from '../sections.service';
|
import { SectionsService } from '../sections.service';
|
||||||
import { WorkspaceitemSectionIdentifiersObject } from '../../../core/submission/models/workspaceitem-section-identifiers.model';
|
import { WorkspaceitemSectionIdentifiersObject } from '../../../core/submission/models/workspaceitem-section-identifiers.model';
|
||||||
|
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { Component, Input } from '@angular/core';
|
import { Component, Input } from '@angular/core';
|
||||||
|
|
||||||
import { Policy } from '../../../../core/submission/models/sherpa-policies-details.model';
|
import { Policy } from '../../../../core/submission/models/sherpa-policies-details.model';
|
||||||
import { AlertType } from '../../../../shared/alert/aletr-type';
|
import { AlertType } from '../../../../shared/alert/alert-type';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component represents a section that contains the publisher policy informations.
|
* This component represents a section that contains the publisher policy informations.
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { AlertType } from '../../../shared/alert/aletr-type';
|
import { AlertType } from '../../../shared/alert/alert-type';
|
||||||
import { Component, Inject } from '@angular/core';
|
import { Component, Inject } from '@angular/core';
|
||||||
|
|
||||||
import { BehaviorSubject, Observable, of, Subscription } from 'rxjs';
|
import { BehaviorSubject, Observable, of, Subscription } from 'rxjs';
|
||||||
|
@@ -1,16 +1,13 @@
|
|||||||
<ng-container *ngIf="fileData">
|
<ng-container *ngIf="fileData">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-2">
|
<div class="col-md-12">
|
||||||
<!--ds-themed-thumbnail [thumbnail]="bitstreamsList[bitstreamKey].url | async"></ds-themed-thumbnail-->
|
|
||||||
<ds-themed-thumbnail [thumbnail]="fileData?.thumbnail"></ds-themed-thumbnail>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-10">
|
|
||||||
<div class="float-left w-75">
|
<div class="float-left w-75">
|
||||||
<h3>{{fileName}} <span class="text-muted">({{fileData?.sizeBytes | dsFileSize}})</span></h3>
|
<h3>{{fileName}} <span class="text-muted">({{fileData?.sizeBytes | dsFileSize}})</span></h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="float-right w-15">
|
<div class="float-right w-15">
|
||||||
<ng-container>
|
<ng-container>
|
||||||
<ds-themed-file-download-link [cssClasses]="'btn btn-link-focus'" [isBlank]="true" [bitstream]="getBitstream()" [enableRequestACopy]="false">
|
<ds-themed-file-download-link [cssClasses]="'btn btn-link-focus'" [isBlank]="true"
|
||||||
|
[bitstream]="getBitstream()" [enableRequestACopy]="false">
|
||||||
<i class="fa fa-download fa-2x text-normal" aria-hidden="true"></i>
|
<i class="fa fa-download fa-2x text-normal" aria-hidden="true"></i>
|
||||||
</ds-themed-file-download-link>
|
</ds-themed-file-download-link>
|
||||||
<button class="btn btn-link-focus"
|
<button class="btn btn-link-focus"
|
||||||
@@ -46,7 +43,9 @@
|
|||||||
<p>{{ 'submission.sections.upload.delete.confirm.info' | translate }}</p>
|
<p>{{ 'submission.sections.upload.delete.confirm.info' | translate }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary" (click)="c('cancel')">{{ 'submission.sections.upload.delete.confirm.cancel' | translate }}</button>
|
<button type="button" class="btn btn-secondary"
|
||||||
<button type="button" class="btn btn-danger" (click)="c('ok')">{{ 'submission.sections.upload.delete.confirm.submit' | translate }}</button>
|
(click)="c('cancel')">{{ 'submission.sections.upload.delete.confirm.cancel' | translate }}</button>
|
||||||
|
<button type="button" class="btn btn-danger"
|
||||||
|
(click)="c('ok')">{{ 'submission.sections.upload.delete.confirm.submit' | translate }}</button>
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
@@ -15,15 +15,23 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngFor="let entry of getAllMetadataValue(fileDescrKey)">
|
<ng-container *ngFor="let entry of getAllMetadataValue(fileDescrKey)">
|
||||||
<ng-container *ngIf="entry.value !== ''">
|
<ng-container *ngIf="entry.value !== ''">
|
||||||
{{entry.value | dsTruncate:['150']}}
|
{{entry.value | dsTruncate:['150']}}
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="entry.value === ''">
|
<ng-container *ngIf="entry.value === ''">
|
||||||
<span *ngIf="metadata[fileDescrKey].indexOf(entry) === 0" class="text-muted">{{'submission.sections.upload.no-entry' | translate}} {{fileDescrKey}}</span>
|
<span *ngIf="metadata[fileDescrKey].indexOf(entry) === 0"
|
||||||
|
class="text-muted">{{'submission.sections.upload.no-entry' | translate}} {{fileDescrKey}}</span>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<span class="clearfix"></span>
|
<span class="clearfix"></span>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="mt-1" *ngIf="fileFormat">
|
||||||
|
{{'admin.registries.bitstream-formats.edit.head' | translate:{format: fileFormat} }}
|
||||||
|
</div>
|
||||||
|
<div class="mt-1" *ngIf="fileCheckSum">
|
||||||
|
Checksum {{fileCheckSum.checkSumAlgorithm}}: {{fileCheckSum.value}}
|
||||||
|
</div>
|
||||||
<span class="clearfix"></span>
|
<span class="clearfix"></span>
|
||||||
<ds-submission-section-upload-access-conditions [accessConditions]="fileData.accessConditions"></ds-submission-section-upload-access-conditions>
|
<ds-submission-section-upload-access-conditions [accessConditions]="fileData.accessConditions"></ds-submission-section-upload-access-conditions>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -38,6 +38,13 @@ export class SubmissionSectionUploadFileViewComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
public fileDescrKey = 'Description';
|
public fileDescrKey = 'Description';
|
||||||
|
|
||||||
|
public fileFormat!: string;
|
||||||
|
|
||||||
|
public fileCheckSum!: {
|
||||||
|
checkSumAlgorithm: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize instance variables
|
* Initialize instance variables
|
||||||
*/
|
*/
|
||||||
@@ -46,6 +53,8 @@ export class SubmissionSectionUploadFileViewComponent implements OnInit {
|
|||||||
this.metadata[this.fileTitleKey] = Metadata.all(this.fileData.metadata, 'dc.title');
|
this.metadata[this.fileTitleKey] = Metadata.all(this.fileData.metadata, 'dc.title');
|
||||||
this.metadata[this.fileDescrKey] = Metadata.all(this.fileData.metadata, 'dc.description');
|
this.metadata[this.fileDescrKey] = Metadata.all(this.fileData.metadata, 'dc.description');
|
||||||
}
|
}
|
||||||
|
this.fileCheckSum = this.fileData.checkSum;
|
||||||
|
this.fileFormat = this.fileData.format.shortDescription;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user