mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
Merge remote-tracking branch 'upstream/main' into retrieve-name-with-dsonameservice-7.4
# Conflicts: # src/app/access-control/epeople-registry/epeople-registry.component.html # src/app/access-control/epeople-registry/eperson-form/eperson-form.component.ts # src/app/access-control/group-registry/group-form/group-form.component.spec.ts # src/app/access-control/group-registry/group-form/group-form.component.ts # src/app/access-control/group-registry/group-form/members-list/members-list.component.html # src/app/access-control/group-registry/group-form/members-list/members-list.component.ts # src/app/collection-page/edit-item-template-page/edit-item-template-page.component.html # src/app/item-page/full/field-components/file-section/full-file-section.component.ts # src/app/item-page/media-viewer/media-viewer-video/media-viewer-video.component.ts # src/app/item-page/simple/field-components/file-section/file-section.component.html # src/app/item-page/simple/field-components/file-section/file-section.component.ts # src/app/item-page/versions/item-versions.component.ts # src/app/shared/auth-nav-menu/user-menu/user-menu.component.html # src/app/shared/auth-nav-menu/user-menu/user-menu.component.ts # src/app/shared/object-collection/shared/mydspace-item-submitter/item-submitter.component.html
This commit is contained in:
@@ -6,7 +6,8 @@
|
|||||||
"eslint-plugin-import",
|
"eslint-plugin-import",
|
||||||
"eslint-plugin-jsdoc",
|
"eslint-plugin-jsdoc",
|
||||||
"eslint-plugin-deprecation",
|
"eslint-plugin-deprecation",
|
||||||
"eslint-plugin-unused-imports"
|
"unused-imports",
|
||||||
|
"eslint-plugin-lodash"
|
||||||
],
|
],
|
||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
@@ -202,7 +203,13 @@
|
|||||||
"deprecation/deprecation": "warn",
|
"deprecation/deprecation": "warn",
|
||||||
|
|
||||||
"import/order": "off",
|
"import/order": "off",
|
||||||
"import/no-deprecated": "warn"
|
"import/no-deprecated": "warn",
|
||||||
|
"import/no-namespace": "error",
|
||||||
|
"unused-imports/no-unused-imports": "error",
|
||||||
|
"lodash/import-scope": [
|
||||||
|
"error",
|
||||||
|
"method"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
12
.github/pull_request_template.md
vendored
12
.github/pull_request_template.md
vendored
@@ -1,7 +1,7 @@
|
|||||||
## References
|
## References
|
||||||
_Add references/links to any related issues or PRs. These may include:_
|
_Add references/links to any related issues or PRs. These may include:_
|
||||||
* Fixes #[issue-number]
|
* Fixes #`issue-number` (if this fixes an issue ticket)
|
||||||
* Requires DSpace/DSpace#[pr-number] (if a REST API PR is required to test this)
|
* Requires DSpace/DSpace#`pr-number` (if a REST API PR is required to test this)
|
||||||
|
|
||||||
## Description
|
## Description
|
||||||
Short summary of changes (1-2 sentences).
|
Short summary of changes (1-2 sentences).
|
||||||
@@ -19,8 +19,10 @@ List of changes in this PR:
|
|||||||
_This checklist provides a reminder of what we are going to look for when reviewing your PR. You need not complete this checklist prior to creating your PR (draft PRs are always welcome). If you are unsure about an item in the checklist, don't hesitate to ask. We're here to help!_
|
_This checklist provides a reminder of what we are going to look for when reviewing your PR. You need not complete this checklist prior to creating your PR (draft PRs are always welcome). If you are unsure about an item in the checklist, don't hesitate to ask. We're here to help!_
|
||||||
|
|
||||||
- [ ] My PR is small in size (e.g. less than 1,000 lines of code, not including comments & specs/tests), or I have provided reasons as to why that's not possible.
|
- [ ] My PR is small in size (e.g. less than 1,000 lines of code, not including comments & specs/tests), or I have provided reasons as to why that's not possible.
|
||||||
- [ ] My PR passes [TSLint](https://palantir.github.io/tslint/) validation using `yarn run lint`
|
- [ ] My PR passes [ESLint](https://eslint.org/) validation using `yarn lint`
|
||||||
- [ ] My PR doesn't introduce circular dependencies
|
- [ ] My PR doesn't introduce circular dependencies (verified via `yarn check-circ-deps`)
|
||||||
- [ ] My PR includes [TypeDoc](https://typedoc.org/) comments for _all new (or modified) public methods and classes_. It also includes TypeDoc for large or complex private methods.
|
- [ ] My PR includes [TypeDoc](https://typedoc.org/) comments for _all new (or modified) public methods and classes_. It also includes TypeDoc for large or complex private methods.
|
||||||
- [ ] My PR passes all specs/tests and includes new/updated specs or tests based on the [Code Testing Guide](https://wiki.lyrasis.org/display/DSPACE/Code+Testing+Guide).
|
- [ ] My PR passes all specs/tests and includes new/updated specs or tests based on the [Code Testing Guide](https://wiki.lyrasis.org/display/DSPACE/Code+Testing+Guide).
|
||||||
- [ ] If my PR includes new, third-party dependencies (in `package.json`), I've made sure their licenses align with the [DSpace BSD License](https://github.com/DSpace/DSpace/blob/main/LICENSE) based on the [Licensing of Contributions](https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines#CodeContributionGuidelines-LicensingofContributions) documentation.
|
- [ ] If my PR includes new libraries/dependencies (in `package.json`), I've made sure their licenses align with the [DSpace BSD License](https://github.com/DSpace/DSpace/blob/main/LICENSE) based on the [Licensing of Contributions](https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines#CodeContributionGuidelines-LicensingofContributions) documentation.
|
||||||
|
- [ ] If my PR includes new features or configurations, I've provided basic technical documentation in the PR itself.
|
||||||
|
- [ ] If my PR fixes an issue ticket, I've [linked them together](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue).
|
||||||
|
29
.github/workflows/build.yml
vendored
29
.github/workflows/build.yml
vendored
@@ -6,34 +6,39 @@ name: Build
|
|||||||
# Run this Build for all pushes / PRs to current branch
|
# Run this Build for all pushes / PRs to current branch
|
||||||
on: [push, pull_request]
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read # to fetch code (actions/checkout)
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
tests:
|
tests:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
# The ci step will test the dspace-angular code against DSpace REST.
|
# The ci step will test the dspace-angular code against DSpace REST.
|
||||||
# Direct that step to utilize a DSpace REST service that has been started in docker.
|
# Direct that step to utilize a DSpace REST service that has been started in docker.
|
||||||
DSPACE_REST_HOST: localhost
|
DSPACE_REST_HOST: 127.0.0.1
|
||||||
DSPACE_REST_PORT: 8080
|
DSPACE_REST_PORT: 8080
|
||||||
DSPACE_REST_NAMESPACE: '/server'
|
DSPACE_REST_NAMESPACE: '/server'
|
||||||
DSPACE_REST_SSL: false
|
DSPACE_REST_SSL: false
|
||||||
|
# Spin up UI on 127.0.0.1 to avoid host resolution issues in e2e tests with Node 18+
|
||||||
|
DSPACE_UI_HOST: 127.0.0.1
|
||||||
# When Chrome version is specified, we pin to a specific version of Chrome
|
# When Chrome version is specified, we pin to a specific version of Chrome
|
||||||
# Comment this out to use the latest release
|
# Comment this out to use the latest release
|
||||||
#CHROME_VERSION: "90.0.4430.212-1"
|
#CHROME_VERSION: "90.0.4430.212-1"
|
||||||
strategy:
|
strategy:
|
||||||
# Create a matrix of Node versions to test against (in parallel)
|
# Create a matrix of Node versions to test against (in parallel)
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [14.x, 16.x]
|
node-version: [16.x, 18.x]
|
||||||
# Do NOT exit immediately if one matrix job fails
|
# Do NOT exit immediately if one matrix job fails
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
# These are the actual CI steps to perform per job
|
# These are the actual CI steps to perform per job
|
||||||
steps:
|
steps:
|
||||||
# https://github.com/actions/checkout
|
# https://github.com/actions/checkout
|
||||||
- name: Checkout codebase
|
- name: Checkout codebase
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
# https://github.com/actions/setup-node
|
# https://github.com/actions/setup-node
|
||||||
- name: Install Node.js ${{ matrix.node-version }}
|
- name: Install Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
|
|
||||||
@@ -58,7 +63,7 @@ jobs:
|
|||||||
id: yarn-cache-dir-path
|
id: yarn-cache-dir-path
|
||||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||||
- name: Cache Yarn dependencies
|
- name: Cache Yarn dependencies
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
# Cache entire Yarn cache directory (see previous step)
|
# Cache entire Yarn cache directory (see previous step)
|
||||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||||
@@ -85,7 +90,7 @@ jobs:
|
|||||||
# Upload coverage reports to Codecov (for one version of Node only)
|
# Upload coverage reports to Codecov (for one version of Node only)
|
||||||
# https://github.com/codecov/codecov-action
|
# https://github.com/codecov/codecov-action
|
||||||
- name: Upload coverage to Codecov.io
|
- name: Upload coverage to Codecov.io
|
||||||
uses: codecov/codecov-action@v2
|
uses: codecov/codecov-action@v3
|
||||||
if: matrix.node-version == '16.x'
|
if: matrix.node-version == '16.x'
|
||||||
|
|
||||||
# Using docker-compose start backend using CI configuration
|
# Using docker-compose start backend using CI configuration
|
||||||
@@ -100,7 +105,7 @@ jobs:
|
|||||||
# https://github.com/cypress-io/github-action
|
# https://github.com/cypress-io/github-action
|
||||||
# (NOTE: to run these e2e tests locally, just use 'ng e2e')
|
# (NOTE: to run these e2e tests locally, just use 'ng e2e')
|
||||||
- name: Run e2e tests (integration tests)
|
- name: Run e2e tests (integration tests)
|
||||||
uses: cypress-io/github-action@v2
|
uses: cypress-io/github-action@v4
|
||||||
with:
|
with:
|
||||||
# Run tests in Chrome, headless mode
|
# Run tests in Chrome, headless mode
|
||||||
browser: chrome
|
browser: chrome
|
||||||
@@ -109,14 +114,14 @@ jobs:
|
|||||||
start: yarn run serve:ssr
|
start: yarn run serve:ssr
|
||||||
# Wait for backend & frontend to be available
|
# Wait for backend & frontend to be available
|
||||||
# NOTE: We use the 'sites' REST endpoint to also ensure the database is ready
|
# NOTE: We use the 'sites' REST endpoint to also ensure the database is ready
|
||||||
wait-on: http://localhost:8080/server/api/core/sites, http://localhost:4000
|
wait-on: http://127.0.0.1:8080/server/api/core/sites, http://127.0.0.1:4000
|
||||||
# Wait for 2 mins max for everything to respond
|
# Wait for 2 mins max for everything to respond
|
||||||
wait-on-timeout: 120
|
wait-on-timeout: 120
|
||||||
|
|
||||||
# Cypress always creates a video of all e2e tests (whether they succeeded or failed)
|
# Cypress always creates a video of all e2e tests (whether they succeeded or failed)
|
||||||
# Save those in an Artifact
|
# Save those in an Artifact
|
||||||
- name: Upload e2e test videos to Artifacts
|
- name: Upload e2e test videos to Artifacts
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v3
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
name: e2e-test-videos
|
name: e2e-test-videos
|
||||||
@@ -125,7 +130,7 @@ jobs:
|
|||||||
# If e2e tests fail, Cypress creates a screenshot of what happened
|
# If e2e tests fail, Cypress creates a screenshot of what happened
|
||||||
# Save those in an Artifact
|
# Save those in an Artifact
|
||||||
- name: Upload e2e test failure screenshots to Artifacts
|
- name: Upload e2e test failure screenshots to Artifacts
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v3
|
||||||
if: failure()
|
if: failure()
|
||||||
with:
|
with:
|
||||||
name: e2e-test-screenshots
|
name: e2e-test-screenshots
|
||||||
@@ -144,7 +149,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
nohup yarn run serve:ssr &
|
nohup yarn run serve:ssr &
|
||||||
printf 'Waiting for app to start'
|
printf 'Waiting for app to start'
|
||||||
until curl --output /dev/null --silent --head --fail http://localhost:4000/home; do
|
until curl --output /dev/null --silent --head --fail http://127.0.0.1:4000/home; do
|
||||||
printf '.'
|
printf '.'
|
||||||
sleep 2
|
sleep 2
|
||||||
done
|
done
|
||||||
@@ -155,7 +160,7 @@ jobs:
|
|||||||
# This step also prints entire HTML of homepage for easier debugging if grep fails.
|
# This step also prints entire HTML of homepage for easier debugging if grep fails.
|
||||||
- name: Verify SSR (server-side rendering)
|
- name: Verify SSR (server-side rendering)
|
||||||
run: |
|
run: |
|
||||||
result=$(wget -O- -q http://localhost:4000/home)
|
result=$(wget -O- -q http://127.0.0.1:4000/home)
|
||||||
echo "$result"
|
echo "$result"
|
||||||
echo "$result" | grep -oE "<meta name=\"title\" [^>]*>" | grep DSpace
|
echo "$result" | grep -oE "<meta name=\"title\" [^>]*>" | grep DSpace
|
||||||
|
|
||||||
|
49
.github/workflows/codescan.yml
vendored
Normal file
49
.github/workflows/codescan.yml
vendored
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# DSpace CodeQL code scanning configuration for GitHub
|
||||||
|
# https://docs.github.com/en/code-security/code-scanning
|
||||||
|
#
|
||||||
|
# NOTE: Code scanning must be run separate from our default build.yml
|
||||||
|
# because CodeQL requires a fresh build with all tests *disabled*.
|
||||||
|
name: "Code Scanning"
|
||||||
|
|
||||||
|
# Run this code scan for all pushes / PRs to main branch. Also run once a week.
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main ]
|
||||||
|
# Don't run if PR is only updating static documentation
|
||||||
|
paths-ignore:
|
||||||
|
- '**/*.md'
|
||||||
|
- '**/*.txt'
|
||||||
|
schedule:
|
||||||
|
- cron: "37 0 * * 1"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
name: Analyze Code
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
# Limit permissions of this GitHub action. Can only write to security-events
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
contents: read
|
||||||
|
security-events: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
# https://github.com/actions/checkout
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
# Initializes the CodeQL tools for scanning.
|
||||||
|
# https://github.com/github/codeql-action
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v2
|
||||||
|
with:
|
||||||
|
languages: javascript
|
||||||
|
|
||||||
|
# Autobuild attempts to build any compiled languages
|
||||||
|
- name: Autobuild
|
||||||
|
uses: github/codeql-action/autobuild@v2
|
||||||
|
|
||||||
|
# Perform GitHub Code Scanning.
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v2
|
13
.github/workflows/docker.yml
vendored
13
.github/workflows/docker.yml
vendored
@@ -12,6 +12,9 @@ on:
|
|||||||
- 'dspace-**'
|
- 'dspace-**'
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read # to fetch code (actions/checkout)
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
docker:
|
docker:
|
||||||
# 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'
|
||||||
@@ -39,11 +42,11 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
# https://github.com/actions/checkout
|
# https://github.com/actions/checkout
|
||||||
- name: Checkout codebase
|
- name: Checkout codebase
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
# https://github.com/docker/setup-buildx-action
|
# https://github.com/docker/setup-buildx-action
|
||||||
- name: Setup Docker Buildx
|
- name: Setup Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v1
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
# https://github.com/docker/setup-qemu-action
|
# https://github.com/docker/setup-qemu-action
|
||||||
- name: Set up QEMU emulation to build for multiple architectures
|
- name: Set up QEMU emulation to build for multiple architectures
|
||||||
@@ -53,7 +56,7 @@ jobs:
|
|||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
# Only login if not a PR, as PRs only trigger a Docker build and not a push
|
# Only login if not a PR, as PRs only trigger a Docker build and not a push
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
|
password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
|
||||||
@@ -65,7 +68,7 @@ jobs:
|
|||||||
# 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
|
||||||
id: meta_build
|
id: meta_build
|
||||||
uses: docker/metadata-action@v3
|
uses: docker/metadata-action@v4
|
||||||
with:
|
with:
|
||||||
images: dspace/dspace-angular
|
images: dspace/dspace-angular
|
||||||
tags: ${{ env.IMAGE_TAGS }}
|
tags: ${{ env.IMAGE_TAGS }}
|
||||||
@@ -74,7 +77,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@v2
|
uses: docker/build-push-action@v3
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
|
17
.github/workflows/issue_opened.yml
vendored
17
.github/workflows/issue_opened.yml
vendored
@@ -5,25 +5,22 @@ on:
|
|||||||
issues:
|
issues:
|
||||||
types: [opened]
|
types: [opened]
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
jobs:
|
jobs:
|
||||||
automation:
|
automation:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
# Add the new issue to a project board, if it needs triage
|
# Add the new issue to a project board, if it needs triage
|
||||||
# See https://github.com/marketplace/actions/create-project-card-action
|
# See https://github.com/actions/add-to-project
|
||||||
- name: Add issue to project board
|
- name: Add issue to triage board
|
||||||
# Only add to project board if issue is flagged as "needs triage" or has no labels
|
# Only add to project board if issue is flagged as "needs triage" or has no labels
|
||||||
# NOTE: By default we flag new issues as "needs triage" in our issue template
|
# NOTE: By default we flag new issues as "needs triage" in our issue template
|
||||||
if: (contains(github.event.issue.labels.*.name, 'needs triage') || join(github.event.issue.labels.*.name) == '')
|
if: (contains(github.event.issue.labels.*.name, 'needs triage') || join(github.event.issue.labels.*.name) == '')
|
||||||
uses: technote-space/create-project-card-action@v1
|
uses: actions/add-to-project@v0.3.0
|
||||||
# Note, the authentication token below is an ORG level Secret.
|
# Note, the authentication token below is an ORG level Secret.
|
||||||
# It must be created/recreated manually via a personal access token with "public_repo" and "admin:org" permissions
|
# It must be created/recreated manually via a personal access token with admin:org, project, public_repo permissions
|
||||||
# See: https://docs.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token#permissions-for-the-github_token
|
# See: https://docs.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token#permissions-for-the-github_token
|
||||||
# This is necessary because the "DSpace Backlog" project is an org level project (i.e. not repo specific)
|
# This is necessary because the "DSpace Backlog" project is an org level project (i.e. not repo specific)
|
||||||
with:
|
with:
|
||||||
GITHUB_TOKEN: ${{ secrets.ORG_PROJECT_TOKEN }}
|
github-token: ${{ secrets.TRIAGE_PROJECT_TOKEN }}
|
||||||
PROJECT: DSpace Backlog
|
project-url: https://github.com/orgs/DSpace/projects/24
|
||||||
COLUMN: Triage
|
|
||||||
CHECK_ORG_PROJECT: true
|
|
||||||
# Ignore errors
|
|
||||||
continue-on-error: true
|
|
||||||
|
27
.github/workflows/label_merge_conflicts.yml
vendored
27
.github/workflows/label_merge_conflicts.yml
vendored
@@ -5,21 +5,32 @@ name: Check for merge conflicts
|
|||||||
# NOTE: This means merge conflicts are only checked for when a PR is merged to main.
|
# NOTE: This means merge conflicts are only checked for when a PR is merged to main.
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches: [ main ]
|
||||||
- main
|
# So that the `conflict_label_name` is removed if conflicts are resolved,
|
||||||
|
# we allow this to run for `pull_request_target` so that github secrets are available.
|
||||||
|
pull_request_target:
|
||||||
|
types: [ synchronize ]
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
triage:
|
triage:
|
||||||
|
# Ensure this job never runs on forked repos. It's only executed for 'dspace/dspace-angular'
|
||||||
|
if: github.repository == 'dspace/dspace-angular'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
# See: https://github.com/mschilde/auto-label-merge-conflicts/
|
# See: https://github.com/prince-chrismc/label-merge-conflicts-action
|
||||||
- name: Auto-label PRs with merge conflicts
|
- name: Auto-label PRs with merge conflicts
|
||||||
uses: mschilde/auto-label-merge-conflicts@v2.0
|
uses: prince-chrismc/label-merge-conflicts-action@v2
|
||||||
# Add "merge conflict" label if a merge conflict is detected. Remove it when resolved.
|
# 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
|
||||||
with:
|
with:
|
||||||
CONFLICT_LABEL_NAME: 'merge conflict'
|
conflict_label_name: 'merge conflict'
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
# Ignore errors
|
conflict_comment: |
|
||||||
continue-on-error: true
|
Hi @${author},
|
||||||
|
Conflicts have been detected against the base branch.
|
||||||
|
Please [resolve these conflicts](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/addressing-merge-conflicts/about-merge-conflicts) as soon as you can. Thanks!
|
46
CONTRIBUTING.md
Normal file
46
CONTRIBUTING.md
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# How to Contribute
|
||||||
|
|
||||||
|
DSpace is a community built and supported project. We do not have a centralized development or support team, but have a dedicated group of volunteers who help us improve the software, documentation, resources, etc.
|
||||||
|
|
||||||
|
* [Contribute new code via a Pull Request](#contribute-new-code-via-a-pull-request)
|
||||||
|
* [Contribute documentation](#contribute-documentation)
|
||||||
|
* [Help others on mailing lists or Slack](#help-others-on-mailing-lists-or-slack)
|
||||||
|
* [Join a working or interest group](#join-a-working-or-interest-group)
|
||||||
|
|
||||||
|
## Contribute new code via a Pull Request
|
||||||
|
|
||||||
|
We accept [GitHub Pull Requests (PRs)](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork) at any time from anyone.
|
||||||
|
Contributors to each release are recognized in our [Release Notes](https://wiki.lyrasis.org/display/DSDOC7x/Release+Notes).
|
||||||
|
|
||||||
|
Code Contribution Checklist
|
||||||
|
- [ ] PRs _should_ be smaller in size (ideally less than 1,000 lines of code, not including comments & tests)
|
||||||
|
- [ ] PRs **must** pass [ESLint](https://eslint.org/) validation using `yarn lint`
|
||||||
|
- [ ] PRs **must** not introduce circular dependencies (verified via `yarn check-circ-deps`)
|
||||||
|
- [ ] PRs **must** include [TypeDoc](https://typedoc.org/) comments for _all new (or modified) public methods and classes_. Large or complex private methods should also have TypeDoc.
|
||||||
|
- [ ] PRs **must** pass all automated pecs/tests and includes new/updated specs or tests based on the [Code Testing Guide](https://wiki.lyrasis.org/display/DSPACE/Code+Testing+Guide).
|
||||||
|
- [ ] If a PR includes new libraries/dependencies (in `package.json`), then their software licenses **must** align with the [DSpace BSD License](https://github.com/DSpace/dspace-angular/blob/main/LICENSE) based on the [Licensing of Contributions](https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines#CodeContributionGuidelines-LicensingofContributions) documentation.
|
||||||
|
- [ ] Basic technical documentation _should_ be provided for any new features or configuration, either in the PR itself or in the DSpace Wiki documentation.
|
||||||
|
- [ ] If a PR fixes an issue ticket, please [link them together](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue).
|
||||||
|
|
||||||
|
Additional details on the code contribution process can be found in our [Code Contribution Guidelines](https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines)
|
||||||
|
|
||||||
|
## Contribute documentation
|
||||||
|
|
||||||
|
DSpace Documentation is a collaborative effort in a shared Wiki. The latest documentation is at https://wiki.lyrasis.org/display/DSDOC7x
|
||||||
|
|
||||||
|
If you find areas of the DSpace Documentation which you wish to improve, please request a Wiki account by emailing wikihelp@lyrasis.org.
|
||||||
|
Once you have an account setup, contact @tdonohue (via [Slack](https://wiki.lyrasis.org/display/DSPACE/Slack) or email) for access to edit our Documentation.
|
||||||
|
|
||||||
|
## Help others on mailing lists or Slack
|
||||||
|
|
||||||
|
DSpace has our own [Slack](https://wiki.lyrasis.org/display/DSPACE/Slack) community and [Mailing Lists](https://wiki.lyrasis.org/display/DSPACE/Mailing+Lists) where discussions take place and questions are answered.
|
||||||
|
Anyone is welcome to join and help others. We just ask you to follow our [Code of Conduct](https://www.lyrasis.org/about/Pages/Code-of-Conduct.aspx) (adopted via LYRASIS).
|
||||||
|
|
||||||
|
## Join a working or interest group
|
||||||
|
|
||||||
|
Most of the work in building/improving DSpace comes via [Working Groups](https://wiki.lyrasis.org/display/DSPACE/DSpace+Working+Groups) or [Interest Groups](https://wiki.lyrasis.org/display/DSPACE/DSpace+Interest+Groups).
|
||||||
|
|
||||||
|
All working/interest groups are open to anyone to join and participate. A few key groups to be aware of include:
|
||||||
|
|
||||||
|
* [DSpace 7 Working Group](https://wiki.lyrasis.org/display/DSPACE/DSpace+7+Working+Group) - This is the main (mostly volunteer) development team. We meet weekly to review our current development [project board](https://github.com/orgs/DSpace/projects), assigning tickets and/or PRs.
|
||||||
|
* [DSpace Community Advisory Team (DCAT)](https://wiki.lyrasis.org/display/cmtygp/DSpace+Community+Advisory+Team) - This is an interest group for repository managers/administrators. We meet monthly to discuss DSpace, share tips & provide feedback back to developers.
|
@@ -1,11 +1,15 @@
|
|||||||
# This image will be published as dspace/dspace-angular
|
# This image will be published as dspace/dspace-angular
|
||||||
# See https://github.com/DSpace/dspace-angular/tree/main/docker for usage details
|
# See https://github.com/DSpace/dspace-angular/tree/main/docker for usage details
|
||||||
|
|
||||||
FROM node:14-alpine
|
FROM node:18-alpine
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
ADD . /app/
|
ADD . /app/
|
||||||
EXPOSE 4000
|
EXPOSE 4000
|
||||||
|
|
||||||
|
# Ensure Python and other build tools are available
|
||||||
|
# These are needed to install some node modules, especially on linux/arm64
|
||||||
|
RUN apk add --update python3 make g++ && rm -rf /var/cache/apk/*
|
||||||
|
|
||||||
# We run yarn install with an increased network timeout (5min) to avoid "ESOCKETTIMEDOUT" errors from hub.docker.com
|
# We run yarn install with an increased network timeout (5min) to avoid "ESOCKETTIMEDOUT" errors from hub.docker.com
|
||||||
# See, for example https://github.com/yarnpkg/yarn/issues/5540
|
# See, for example https://github.com/yarnpkg/yarn/issues/5540
|
||||||
RUN yarn install --network-timeout 300000
|
RUN yarn install --network-timeout 300000
|
||||||
|
10
README.md
10
README.md
@@ -35,7 +35,7 @@ https://wiki.lyrasis.org/display/DSDOC7x/Installing+DSpace
|
|||||||
Quick start
|
Quick start
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
**Ensure you're running [Node](https://nodejs.org) `v14.x` or `v16.x`, [npm](https://www.npmjs.com/) >= `v5.x` and [yarn](https://yarnpkg.com) == `v1.x`**
|
**Ensure you're running [Node](https://nodejs.org) `v16.x` or `v18.x`, [npm](https://www.npmjs.com/) >= `v5.x` and [yarn](https://yarnpkg.com) == `v1.x`**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# clone the repo
|
# clone the repo
|
||||||
@@ -90,7 +90,7 @@ Requirements
|
|||||||
------------
|
------------
|
||||||
|
|
||||||
- [Node.js](https://nodejs.org) and [yarn](https://yarnpkg.com)
|
- [Node.js](https://nodejs.org) and [yarn](https://yarnpkg.com)
|
||||||
- Ensure you're running node `v14.x` or `v16.x` and yarn == `v1.x`
|
- Ensure you're running node `v16.x` or `v18.x` and yarn == `v1.x`
|
||||||
|
|
||||||
If you have [`nvm`](https://github.com/creationix/nvm#install-script) or [`nvm-windows`](https://github.com/coreybutler/nvm-windows) installed, which is highly recommended, you can run `nvm install --lts && nvm use` to install and start using the latest Node LTS.
|
If you have [`nvm`](https://github.com/creationix/nvm#install-script) or [`nvm-windows`](https://github.com/coreybutler/nvm-windows) installed, which is highly recommended, you can run `nvm install --lts && nvm use` to install and start using the latest Node LTS.
|
||||||
|
|
||||||
@@ -351,7 +351,7 @@ Documentation
|
|||||||
|
|
||||||
Official DSpace documentation is available in the DSpace wiki at https://wiki.lyrasis.org/display/DSDOC7x/
|
Official DSpace documentation is available in the DSpace wiki at https://wiki.lyrasis.org/display/DSDOC7x/
|
||||||
|
|
||||||
Some UI specific configuration documentation is also found in the [`./docs`](docs) folder of htis codebase.
|
Some UI specific configuration documentation is also found in the [`./docs`](docs) folder of this codebase.
|
||||||
|
|
||||||
### Building code documentation
|
### Building code documentation
|
||||||
|
|
||||||
@@ -379,10 +379,10 @@ To get the most out of TypeScript, you'll need a TypeScript-aware editor. We've
|
|||||||
- [Sublime Text](http://www.sublimetext.com/3)
|
- [Sublime Text](http://www.sublimetext.com/3)
|
||||||
- [Typescript-Sublime-Plugin](https://github.com/Microsoft/Typescript-Sublime-plugin#installation)
|
- [Typescript-Sublime-Plugin](https://github.com/Microsoft/Typescript-Sublime-plugin#installation)
|
||||||
|
|
||||||
Collaborating
|
Contributing
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
See [the guide on the wiki](https://wiki.lyrasis.org/display/DSPACE/DSpace+7+-+Angular+UI+Development#DSpace7-AngularUIDevelopment-Howtocontribute)
|
See [Contributing documentation](CONTRIBUTING.md)
|
||||||
|
|
||||||
File Structure
|
File Structure
|
||||||
--------------
|
--------------
|
||||||
|
@@ -25,12 +25,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"allowedCommonJsDependencies": [
|
"allowedCommonJsDependencies": [
|
||||||
"angular2-text-mask",
|
|
||||||
"cerialize",
|
"cerialize",
|
||||||
"core-js",
|
"core-js",
|
||||||
"lodash",
|
"lodash",
|
||||||
"jwt-decode",
|
"jwt-decode",
|
||||||
"url-parse",
|
|
||||||
"uuid",
|
"uuid",
|
||||||
"webfontloader",
|
"webfontloader",
|
||||||
"zone.js"
|
"zone.js"
|
||||||
|
@@ -32,12 +32,60 @@ cache:
|
|||||||
# NOTE: how long should objects be cached for by default
|
# NOTE: how long should objects be cached for by default
|
||||||
msToLive:
|
msToLive:
|
||||||
default: 900000 # 15 minutes
|
default: 900000 # 15 minutes
|
||||||
control: max-age=60 # revalidate browser
|
# Default 'Cache-Control' HTTP Header to set for all static content (including compiled *.js files)
|
||||||
|
# Defaults to max-age=604,800 seconds (one week). This lets a user's browser know that it can cache these
|
||||||
|
# files for one week, after which they will be "stale" and need to be redownloaded.
|
||||||
|
# NOTE: When updates are made to compiled *.js files, it will automatically bypass this browser cache, because
|
||||||
|
# all compiled *.js files include a unique hash in their name which updates when content is modified.
|
||||||
|
control: max-age=604800 # revalidate browser
|
||||||
autoSync:
|
autoSync:
|
||||||
defaultTime: 0
|
defaultTime: 0
|
||||||
maxBufferSize: 100
|
maxBufferSize: 100
|
||||||
timePerMethod:
|
timePerMethod:
|
||||||
PATCH: 3 # time in seconds
|
PATCH: 3 # time in seconds
|
||||||
|
# In-memory cache(s) of server-side rendered pages. These caches will store the most recently accessed public pages.
|
||||||
|
# Pages are automatically added/dropped from these caches based on how recently they have been used.
|
||||||
|
# Restarting the app clears all page caches.
|
||||||
|
# NOTE: To control the cache size, use the "max" setting. Keep in mind, individual cached pages are usually small (<100KB).
|
||||||
|
# Enabling *both* caches will mean that a page may be cached twice, once in each cache (but may expire at different times via timeToLive).
|
||||||
|
serverSide:
|
||||||
|
# Set to true to see all cache hits/misses/refreshes in your console logs. Useful for debugging SSR caching issues.
|
||||||
|
debug: false
|
||||||
|
# When enabled (i.e. max > 0), known bots will be sent pages from a server side cache specific for bots.
|
||||||
|
# (Keep in mind, bot detection cannot be guarranteed. It is possible some bots will bypass this cache.)
|
||||||
|
botCache:
|
||||||
|
# Maximum number of pages to cache for known bots. Set to zero (0) to disable server side caching for bots.
|
||||||
|
# Default is 1000, which means the 1000 most recently accessed public pages will be cached.
|
||||||
|
# As all pages are cached in server memory, increasing this value will increase memory needs.
|
||||||
|
# Individual cached pages are usually small (<100KB), so max=1000 should only require ~100MB of memory.
|
||||||
|
max: 1000
|
||||||
|
# Amount of time after which cached pages are considered stale (in ms). After becoming stale, the cached
|
||||||
|
# copy is automatically refreshed on the next request.
|
||||||
|
# NOTE: For the bot cache, this setting may impact how quickly search engine bots will index new content on your site.
|
||||||
|
# For example, setting this to one week may mean that search engine bots may not find all new content for one week.
|
||||||
|
timeToLive: 86400000 # 1 day
|
||||||
|
# When set to true, after timeToLive expires, the next request will receive the *cached* page & then re-render the page
|
||||||
|
# behind the scenes to update the cache. This ensures users primarily interact with the cache, but may receive stale pages (older than timeToLive).
|
||||||
|
# When set to false, after timeToLive expires, the next request will wait on SSR to complete & receive a fresh page (which is then saved to cache).
|
||||||
|
# This ensures stale pages (older than timeToLive) are never returned from the cache, but some users will wait on SSR.
|
||||||
|
allowStale: true
|
||||||
|
# When enabled (i.e. max > 0), all anonymous users will be sent pages from a server side cache.
|
||||||
|
# This allows anonymous users to interact more quickly with the site, but also means they may see slightly
|
||||||
|
# outdated content (based on timeToLive)
|
||||||
|
anonymousCache:
|
||||||
|
# Maximum number of pages to cache. Default is zero (0) which means anonymous user cache is disabled.
|
||||||
|
# As all pages are cached in server memory, increasing this value will increase memory needs.
|
||||||
|
# Individual cached pages are usually small (<100KB), so a value of max=1000 would only require ~100MB of memory.
|
||||||
|
max: 0
|
||||||
|
# Amount of time after which cached pages are considered stale (in ms). After becoming stale, the cached
|
||||||
|
# copy is automatically refreshed on the next request.
|
||||||
|
# NOTE: For the anonymous cache, it is recommended to keep this value low to avoid anonymous users seeing outdated content.
|
||||||
|
timeToLive: 10000 # 10 seconds
|
||||||
|
# When set to true, after timeToLive expires, the next request will receive the *cached* page & then re-render the page
|
||||||
|
# behind the scenes to update the cache. This ensures users primarily interact with the cache, but may receive stale pages (older than timeToLive).
|
||||||
|
# When set to false, after timeToLive expires, the next request will wait on SSR to complete & receive a fresh page (which is then saved to cache).
|
||||||
|
# This ensures stale pages (older than timeToLive) are never returned from the cache, but some users will wait on SSR.
|
||||||
|
allowStale: true
|
||||||
|
|
||||||
# Authentication settings
|
# Authentication settings
|
||||||
auth:
|
auth:
|
||||||
@@ -55,6 +103,8 @@ auth:
|
|||||||
|
|
||||||
# Form settings
|
# Form settings
|
||||||
form:
|
form:
|
||||||
|
# Sets the spellcheck textarea attribute value
|
||||||
|
spellCheck: true
|
||||||
# NOTE: Map server-side validators to comparative Angular form validators
|
# NOTE: Map server-side validators to comparative Angular form validators
|
||||||
validatorMap:
|
validatorMap:
|
||||||
required: required
|
required: required
|
||||||
@@ -119,6 +169,9 @@ languages:
|
|||||||
- code: en
|
- code: en
|
||||||
label: English
|
label: English
|
||||||
active: true
|
active: true
|
||||||
|
- code: ca
|
||||||
|
label: Català
|
||||||
|
active: true
|
||||||
- code: cs
|
- code: cs
|
||||||
label: Čeština
|
label: Čeština
|
||||||
active: true
|
active: true
|
||||||
@@ -143,6 +196,9 @@ languages:
|
|||||||
- code: nl
|
- code: nl
|
||||||
label: Nederlands
|
label: Nederlands
|
||||||
active: true
|
active: true
|
||||||
|
- code: pl
|
||||||
|
label: Polski
|
||||||
|
active: true
|
||||||
- code: pt-PT
|
- code: pt-PT
|
||||||
label: Português
|
label: Português
|
||||||
active: true
|
active: true
|
||||||
@@ -170,6 +226,10 @@ languages:
|
|||||||
- code: el
|
- code: el
|
||||||
label: Ελληνικά
|
label: Ελληνικά
|
||||||
active: true
|
active: true
|
||||||
|
- code: uk
|
||||||
|
label: Yкраї́нська
|
||||||
|
active: true
|
||||||
|
|
||||||
|
|
||||||
# Browse-By Pages
|
# Browse-By Pages
|
||||||
browseBy:
|
browseBy:
|
||||||
@@ -207,6 +267,11 @@ item:
|
|||||||
undoTimeout: 10000 # 10 seconds
|
undoTimeout: 10000 # 10 seconds
|
||||||
# Show the item access status label in items lists
|
# Show the item access status label in items lists
|
||||||
showAccessStatuses: false
|
showAccessStatuses: false
|
||||||
|
bitstream:
|
||||||
|
# Number of entries in the bitstream list in the item view page.
|
||||||
|
# Rounded to the nearest size in the list of selectable sizes on the
|
||||||
|
# settings menu. See pageSizeOptions in 'pagination-component-options.model.ts'.
|
||||||
|
pageSize: 5
|
||||||
|
|
||||||
# Collection Page Config
|
# Collection Page Config
|
||||||
collection:
|
collection:
|
||||||
@@ -296,3 +361,16 @@ info:
|
|||||||
markdown:
|
markdown:
|
||||||
enabled: false
|
enabled: false
|
||||||
mathjax: false
|
mathjax: false
|
||||||
|
|
||||||
|
# Which vocabularies should be used for which search filters
|
||||||
|
# and whether to show the filter in the search sidebar
|
||||||
|
# Take a look at the filter-vocabulary-config.ts file for documentation on how the options are obtained
|
||||||
|
vocabularies:
|
||||||
|
- filter: 'subject'
|
||||||
|
vocabulary: 'srsc'
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
# Default collection/community sorting order at Advanced search, Create/update community and collection when there are not a query.
|
||||||
|
comcolSelectionSort:
|
||||||
|
sortField: 'dc.title'
|
||||||
|
sortDirection: 'ASC'
|
@@ -5,7 +5,7 @@
|
|||||||
"screenshotsFolder": "cypress/screenshots",
|
"screenshotsFolder": "cypress/screenshots",
|
||||||
"pluginsFile": "cypress/plugins/index.ts",
|
"pluginsFile": "cypress/plugins/index.ts",
|
||||||
"fixturesFolder": "cypress/fixtures",
|
"fixturesFolder": "cypress/fixtures",
|
||||||
"baseUrl": "http://localhost:4000",
|
"baseUrl": "http://127.0.0.1:4000",
|
||||||
"retries": {
|
"retries": {
|
||||||
"runMode": 2,
|
"runMode": 2,
|
||||||
"openMode": 0
|
"openMode": 0
|
||||||
|
@@ -4,10 +4,11 @@ import { testA11y } from 'cypress/support/utils';
|
|||||||
|
|
||||||
describe('My DSpace page', () => {
|
describe('My DSpace page', () => {
|
||||||
it('should display recent submissions and pass accessibility tests', () => {
|
it('should display recent submissions and pass accessibility tests', () => {
|
||||||
cy.login(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
|
|
||||||
|
|
||||||
cy.visit('/mydspace');
|
cy.visit('/mydspace');
|
||||||
|
|
||||||
|
// This page is restricted, so we will be shown the login form. Fill it out & submit.
|
||||||
|
cy.loginViaForm(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
|
||||||
|
|
||||||
cy.get('ds-my-dspace-page').should('exist');
|
cy.get('ds-my-dspace-page').should('exist');
|
||||||
|
|
||||||
// At least one recent submission should be displayed
|
// At least one recent submission should be displayed
|
||||||
@@ -36,10 +37,11 @@ describe('My DSpace page', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should have a working detailed view that passes accessibility tests', () => {
|
it('should have a working detailed view that passes accessibility tests', () => {
|
||||||
cy.login(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
|
|
||||||
|
|
||||||
cy.visit('/mydspace');
|
cy.visit('/mydspace');
|
||||||
|
|
||||||
|
// This page is restricted, so we will be shown the login form. Fill it out & submit.
|
||||||
|
cy.loginViaForm(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
|
||||||
|
|
||||||
cy.get('ds-my-dspace-page').should('exist');
|
cy.get('ds-my-dspace-page').should('exist');
|
||||||
|
|
||||||
// Click button in sidebar to display detailed view
|
// Click button in sidebar to display detailed view
|
||||||
@@ -61,9 +63,11 @@ describe('My DSpace page', () => {
|
|||||||
|
|
||||||
// NOTE: Deleting existing submissions is exercised by submission.spec.ts
|
// NOTE: Deleting existing submissions is exercised by submission.spec.ts
|
||||||
it('should let you start a new submission & edit in-progress submissions', () => {
|
it('should let you start a new submission & edit in-progress submissions', () => {
|
||||||
cy.login(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
|
|
||||||
cy.visit('/mydspace');
|
cy.visit('/mydspace');
|
||||||
|
|
||||||
|
// This page is restricted, so we will be shown the login form. Fill it out & submit.
|
||||||
|
cy.loginViaForm(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
|
||||||
|
|
||||||
// Open the New Submission dropdown
|
// Open the New Submission dropdown
|
||||||
cy.get('button[data-test="submission-dropdown"]').click();
|
cy.get('button[data-test="submission-dropdown"]').click();
|
||||||
// Click on the "Item" type in that dropdown
|
// Click on the "Item" type in that dropdown
|
||||||
@@ -131,9 +135,11 @@ describe('My DSpace page', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should let you import from external sources', () => {
|
it('should let you import from external sources', () => {
|
||||||
cy.login(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
|
|
||||||
cy.visit('/mydspace');
|
cy.visit('/mydspace');
|
||||||
|
|
||||||
|
// This page is restricted, so we will be shown the login form. Fill it out & submit.
|
||||||
|
cy.loginViaForm(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
|
||||||
|
|
||||||
// Open the New Import dropdown
|
// Open the New Import dropdown
|
||||||
cy.get('button[data-test="import-dropdown"]').click();
|
cy.get('button[data-test="import-dropdown"]').click();
|
||||||
// Click on the "Item" type in that dropdown
|
// Click on the "Item" type in that dropdown
|
||||||
|
@@ -6,11 +6,12 @@ describe('New Submission page', () => {
|
|||||||
// NOTE: We already test that new submissions can be started from MyDSpace in my-dspace.spec.ts
|
// NOTE: We already test that new submissions can be started from MyDSpace in my-dspace.spec.ts
|
||||||
|
|
||||||
it('should create a new submission when using /submit path & pass accessibility', () => {
|
it('should create a new submission when using /submit path & pass accessibility', () => {
|
||||||
cy.login(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
|
|
||||||
|
|
||||||
// Test that calling /submit with collection & entityType will create a new submission
|
// Test that calling /submit with collection & entityType will create a new submission
|
||||||
cy.visit('/submit?collection=' + TEST_SUBMIT_COLLECTION_UUID + '&entityType=none');
|
cy.visit('/submit?collection=' + TEST_SUBMIT_COLLECTION_UUID + '&entityType=none');
|
||||||
|
|
||||||
|
// This page is restricted, so we will be shown the login form. Fill it out & submit.
|
||||||
|
cy.loginViaForm(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
|
||||||
|
|
||||||
// Should redirect to /workspaceitems, as we've started a new submission
|
// Should redirect to /workspaceitems, as we've started a new submission
|
||||||
cy.url().should('include', '/workspaceitems');
|
cy.url().should('include', '/workspaceitems');
|
||||||
|
|
||||||
@@ -33,11 +34,12 @@ describe('New Submission page', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should block submission & show errors if required fields are missing', () => {
|
it('should block submission & show errors if required fields are missing', () => {
|
||||||
cy.login(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
|
|
||||||
|
|
||||||
// Create a new submission
|
// Create a new submission
|
||||||
cy.visit('/submit?collection=' + TEST_SUBMIT_COLLECTION_UUID + '&entityType=none');
|
cy.visit('/submit?collection=' + TEST_SUBMIT_COLLECTION_UUID + '&entityType=none');
|
||||||
|
|
||||||
|
// This page is restricted, so we will be shown the login form. Fill it out & submit.
|
||||||
|
cy.loginViaForm(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
|
||||||
|
|
||||||
// Attempt an immediate deposit without filling out any fields
|
// Attempt an immediate deposit without filling out any fields
|
||||||
cy.get('button#deposit').click();
|
cy.get('button#deposit').click();
|
||||||
|
|
||||||
@@ -92,11 +94,12 @@ describe('New Submission page', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should allow for deposit if all required fields completed & file uploaded', () => {
|
it('should allow for deposit if all required fields completed & file uploaded', () => {
|
||||||
cy.login(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
|
|
||||||
|
|
||||||
// Create a new submission
|
// Create a new submission
|
||||||
cy.visit('/submit?collection=' + TEST_SUBMIT_COLLECTION_UUID + '&entityType=none');
|
cy.visit('/submit?collection=' + TEST_SUBMIT_COLLECTION_UUID + '&entityType=none');
|
||||||
|
|
||||||
|
// This page is restricted, so we will be shown the login form. Fill it out & submit.
|
||||||
|
cy.loginViaForm(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
|
||||||
|
|
||||||
// Fill out all required fields (Title, Date)
|
// Fill out all required fields (Title, Date)
|
||||||
cy.get('input#dc_title').type('DSpace logo uploaded via e2e tests');
|
cy.get('input#dc_title').type('DSpace logo uploaded via e2e tests');
|
||||||
cy.get('input#dc_date_issued_year').type('2022');
|
cy.get('input#dc_date_issued_year').type('2022');
|
||||||
|
@@ -19,6 +19,14 @@ declare global {
|
|||||||
* @param password password to login as
|
* @param password password to login as
|
||||||
*/
|
*/
|
||||||
login(email: string, password: string): typeof login;
|
login(email: string, password: string): typeof login;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Login via form before accessing the next page. Useful to fill out login
|
||||||
|
* form when a cy.visit() call is to an a page which requires authentication.
|
||||||
|
* @param email email to login as
|
||||||
|
* @param password password to login as
|
||||||
|
*/
|
||||||
|
loginViaForm(email: string, password: string): typeof loginViaForm;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -26,6 +34,8 @@ declare global {
|
|||||||
/**
|
/**
|
||||||
* Login user via REST API directly, and pass authentication token to UI via
|
* Login user via REST API directly, and pass authentication token to UI via
|
||||||
* the UI's dsAuthInfo cookie.
|
* the UI's dsAuthInfo cookie.
|
||||||
|
* WARNING: WHILE THIS METHOD WORKS, OCCASIONALLY RANDOM AUTHENTICATION ERRORS OCCUR.
|
||||||
|
* At this time "loginViaForm()" seems more consistent/stable.
|
||||||
* @param email email to login as
|
* @param email email to login as
|
||||||
* @param password password to login as
|
* @param password password to login as
|
||||||
*/
|
*/
|
||||||
@@ -81,3 +91,20 @@ function login(email: string, password: string): void {
|
|||||||
}
|
}
|
||||||
// Add as a Cypress command (i.e. assign to 'cy.login')
|
// Add as a Cypress command (i.e. assign to 'cy.login')
|
||||||
Cypress.Commands.add('login', login);
|
Cypress.Commands.add('login', login);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Login user via displayed login form
|
||||||
|
* @param email email to login as
|
||||||
|
* @param password password to login as
|
||||||
|
*/
|
||||||
|
function loginViaForm(email: string, password: string): void {
|
||||||
|
// Enter email
|
||||||
|
cy.get('ds-log-in [data-test="email"]').type(email);
|
||||||
|
// Enter password
|
||||||
|
cy.get('ds-log-in [data-test="password"]').type(password);
|
||||||
|
// Click login button
|
||||||
|
cy.get('ds-log-in [data-test="login-button"]').click();
|
||||||
|
}
|
||||||
|
// Add as a Cypress command (i.e. assign to 'cy.loginViaForm')
|
||||||
|
Cypress.Commands.add('loginViaForm', loginViaForm);
|
@@ -24,8 +24,8 @@ services:
|
|||||||
# __D__ => "-" (e.g. google__D__metadata => google-metadata)
|
# __D__ => "-" (e.g. google__D__metadata => google-metadata)
|
||||||
# dspace.dir, dspace.server.url and dspace.ui.url
|
# dspace.dir, dspace.server.url and dspace.ui.url
|
||||||
dspace__P__dir: /dspace
|
dspace__P__dir: /dspace
|
||||||
dspace__P__server__P__url: http://localhost:8080/server
|
dspace__P__server__P__url: http://127.0.0.1:8080/server
|
||||||
dspace__P__ui__P__url: http://localhost:4000
|
dspace__P__ui__P__url: http://127.0.0.1:4000
|
||||||
# db.url: Ensure we are using the 'dspacedb' image for our database
|
# db.url: Ensure we are using the 'dspacedb' image for our database
|
||||||
db__P__url: 'jdbc:postgresql://dspacedb:5432/dspace'
|
db__P__url: 'jdbc:postgresql://dspacedb:5432/dspace'
|
||||||
# solr.server: Ensure we are using the 'dspacesolr' image for Solr
|
# solr.server: Ensure we are using the 'dspacesolr' image for Solr
|
||||||
|
92
package.json
92
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dspace-angular",
|
"name": "dspace-angular",
|
||||||
"version": "7.4.0",
|
"version": "7.6.0-next",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
"config:watch": "nodemon",
|
"config:watch": "nodemon",
|
||||||
@@ -30,8 +30,9 @@
|
|||||||
"clean:log": "rimraf *.log*",
|
"clean:log": "rimraf *.log*",
|
||||||
"clean:json": "rimraf *.records.json",
|
"clean:json": "rimraf *.records.json",
|
||||||
"clean:node": "rimraf node_modules",
|
"clean:node": "rimraf node_modules",
|
||||||
|
"clean:cli": "rimraf .angular/cache",
|
||||||
"clean:prod": "yarn run clean:dist && yarn run clean:log && yarn run clean:doc && yarn run clean:coverage && yarn run clean:json",
|
"clean:prod": "yarn run clean:dist && yarn run clean:log && yarn run clean:doc && yarn run clean:coverage && yarn run clean:json",
|
||||||
"clean": "yarn run clean:prod && yarn run clean:dev:config && yarn run clean:node",
|
"clean": "yarn run clean:prod && yarn run clean:dev:config && yarn run clean:cli && yarn run clean:node",
|
||||||
"sync-i18n": "ts-node --project ./tsconfig.ts-node.json scripts/sync-i18n-files.ts",
|
"sync-i18n": "ts-node --project ./tsconfig.ts-node.json scripts/sync-i18n-files.ts",
|
||||||
"build:mirador": "webpack --config webpack/webpack.mirador.config.ts",
|
"build:mirador": "webpack --config webpack/webpack.mirador.config.ts",
|
||||||
"merge-i18n": "ts-node --project ./tsconfig.ts-node.json scripts/merge-i18n-files.ts",
|
"merge-i18n": "ts-node --project ./tsconfig.ts-node.json scripts/merge-i18n-files.ts",
|
||||||
@@ -54,18 +55,18 @@
|
|||||||
"ts-node": "10.2.1"
|
"ts-node": "10.2.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "~13.2.6",
|
"@angular/animations": "~13.3.12",
|
||||||
"@angular/cdk": "^13.2.6",
|
"@angular/cdk": "^13.2.6",
|
||||||
"@angular/common": "~13.2.6",
|
"@angular/common": "~13.3.12",
|
||||||
"@angular/compiler": "~13.2.6",
|
"@angular/compiler": "~13.3.12",
|
||||||
"@angular/core": "~13.2.6",
|
"@angular/core": "~13.3.12",
|
||||||
"@angular/forms": "~13.2.6",
|
"@angular/forms": "~13.3.12",
|
||||||
"@angular/localize": "13.2.6",
|
"@angular/localize": "13.3.12",
|
||||||
"@angular/platform-browser": "~13.2.6",
|
"@angular/platform-browser": "~13.3.12",
|
||||||
"@angular/platform-browser-dynamic": "~13.2.6",
|
"@angular/platform-browser-dynamic": "~13.3.12",
|
||||||
"@angular/platform-server": "~13.2.6",
|
"@angular/platform-server": "~13.3.12",
|
||||||
"@angular/router": "~13.2.6",
|
"@angular/router": "~13.3.12",
|
||||||
"@babel/runtime": "^7.17.2",
|
"@babel/runtime": "7.17.2",
|
||||||
"@kolkov/ngx-gallery": "^2.0.1",
|
"@kolkov/ngx-gallery": "^2.0.1",
|
||||||
"@material-ui/core": "^4.11.0",
|
"@material-ui/core": "^4.11.0",
|
||||||
"@material-ui/icons": "^4.9.1",
|
"@material-ui/icons": "^4.9.1",
|
||||||
@@ -77,114 +78,103 @@
|
|||||||
"@ngrx/store": "^13.0.2",
|
"@ngrx/store": "^13.0.2",
|
||||||
"@nguniversal/express-engine": "^13.0.2",
|
"@nguniversal/express-engine": "^13.0.2",
|
||||||
"@ngx-translate/core": "^13.0.0",
|
"@ngx-translate/core": "^13.0.0",
|
||||||
"@nicky-lenaers/ngx-scroll-to": "^9.0.0",
|
"@nicky-lenaers/ngx-scroll-to": "^13.0.0",
|
||||||
"@types/grecaptcha": "^3.0.4",
|
"@types/grecaptcha": "^3.0.4",
|
||||||
"angular-idle-preload": "3.0.0",
|
"angular-idle-preload": "3.0.0",
|
||||||
"angulartics2": "^12.0.0",
|
"angulartics2": "^12.0.0",
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
"bootstrap": "4.3.1",
|
"bootstrap": "^4.6.1",
|
||||||
"caniuse-lite": "^1.0.30001165",
|
|
||||||
"cerialize": "0.1.18",
|
"cerialize": "0.1.18",
|
||||||
"cli-progress": "^3.8.0",
|
"cli-progress": "^3.8.0",
|
||||||
|
"colors": "^1.4.0",
|
||||||
"compression": "^1.7.4",
|
"compression": "^1.7.4",
|
||||||
"cookie-parser": "1.4.5",
|
"cookie-parser": "1.4.5",
|
||||||
"core-js": "^3.7.0",
|
"core-js": "^3.7.0",
|
||||||
|
"date-fns": "^2.29.3",
|
||||||
|
"date-fns-tz": "^1.3.7",
|
||||||
"deepmerge": "^4.2.2",
|
"deepmerge": "^4.2.2",
|
||||||
|
"ejs": "^3.1.8",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"express-rate-limit": "^5.1.3",
|
"express-rate-limit": "^5.1.3",
|
||||||
"fast-json-patch": "^3.0.0-1",
|
"fast-json-patch": "^3.0.0-1",
|
||||||
"file-saver": "^2.0.5",
|
|
||||||
"filesize": "^6.1.0",
|
"filesize": "^6.1.0",
|
||||||
"font-awesome": "4.7.0",
|
|
||||||
"http-proxy-middleware": "^1.0.5",
|
"http-proxy-middleware": "^1.0.5",
|
||||||
"https": "1.0.0",
|
"isbot": "^3.6.5",
|
||||||
"js-cookie": "2.2.1",
|
"js-cookie": "2.2.1",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"json5": "^2.1.3",
|
"json5": "^2.2.2",
|
||||||
"jsonschema": "1.4.0",
|
"jsonschema": "1.4.0",
|
||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
"klaro": "^0.7.10",
|
"klaro": "^0.7.18",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
"lru-cache": "^7.14.1",
|
||||||
"markdown-it": "^13.0.1",
|
"markdown-it": "^13.0.1",
|
||||||
"markdown-it-mathjax3": "^4.3.1",
|
"markdown-it-mathjax3": "^4.3.1",
|
||||||
"mirador": "^3.3.0",
|
"mirador": "^3.3.0",
|
||||||
"mirador-dl-plugin": "^0.13.0",
|
"mirador-dl-plugin": "^0.13.0",
|
||||||
"mirador-share-plugin": "^0.11.0",
|
"mirador-share-plugin": "^0.11.0",
|
||||||
"moment": "^2.29.4",
|
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"ng-mocks": "^13.1.1",
|
"ng-mocks": "^13.1.1",
|
||||||
"ng2-file-upload": "1.4.0",
|
"ng2-file-upload": "1.4.0",
|
||||||
"ng2-nouislider": "^1.8.3",
|
"ng2-nouislider": "^1.8.3",
|
||||||
"ngx-infinite-scroll": "^10.0.1",
|
"ngx-infinite-scroll": "^10.0.1",
|
||||||
"ngx-moment": "^5.0.0",
|
|
||||||
"ngx-pagination": "5.0.0",
|
"ngx-pagination": "5.0.0",
|
||||||
"ngx-sortablejs": "^11.1.0",
|
"ngx-sortablejs": "^11.1.0",
|
||||||
"ngx-ui-switch": "^11.0.1",
|
"ngx-ui-switch": "^13.0.2",
|
||||||
"nouislider": "^14.6.3",
|
"nouislider": "^14.6.3",
|
||||||
"pem": "1.14.4",
|
"pem": "1.14.4",
|
||||||
"postcss-cli": "^9.1.0",
|
|
||||||
"prop-types": "^15.7.2",
|
"prop-types": "^15.7.2",
|
||||||
"react-copy-to-clipboard": "^5.0.1",
|
"react-copy-to-clipboard": "^5.0.1",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"rxjs": "^7.5.5",
|
"rxjs": "^7.5.5",
|
||||||
"sanitize-html": "^2.7.2",
|
"sanitize-html": "^2.7.2",
|
||||||
"sortablejs": "1.13.0",
|
"sortablejs": "1.13.0",
|
||||||
"tslib": "^2.0.0",
|
|
||||||
"url-parse": "^1.5.6",
|
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
"webfontloader": "1.6.28",
|
"webfontloader": "1.6.28",
|
||||||
"zone.js": "~0.11.5"
|
"zone.js": "~0.11.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-builders/custom-webpack": "~13.1.0",
|
"@angular-builders/custom-webpack": "~13.1.0",
|
||||||
"@angular-devkit/build-angular": "~13.2.6",
|
"@angular-devkit/build-angular": "~13.3.10",
|
||||||
"@angular-eslint/builder": "13.1.0",
|
"@angular-eslint/builder": "13.1.0",
|
||||||
"@angular-eslint/eslint-plugin": "13.1.0",
|
"@angular-eslint/eslint-plugin": "13.1.0",
|
||||||
"@angular-eslint/eslint-plugin-template": "13.1.0",
|
"@angular-eslint/eslint-plugin-template": "13.1.0",
|
||||||
"@angular-eslint/schematics": "13.1.0",
|
"@angular-eslint/schematics": "13.1.0",
|
||||||
"@angular-eslint/template-parser": "13.1.0",
|
"@angular-eslint/template-parser": "13.1.0",
|
||||||
"@angular/cli": "~13.2.6",
|
"@angular/cli": "~13.3.10",
|
||||||
"@angular/compiler-cli": "~13.2.6",
|
"@angular/compiler-cli": "~13.3.12",
|
||||||
"@angular/language-service": "~13.2.6",
|
"@angular/language-service": "~13.3.12",
|
||||||
"@cypress/schematic": "^1.5.0",
|
"@cypress/schematic": "^1.5.0",
|
||||||
"@fortawesome/fontawesome-free": "^5.5.0",
|
"@fortawesome/fontawesome-free": "^6.2.1",
|
||||||
"@ngrx/store-devtools": "^13.0.2",
|
"@ngrx/store-devtools": "^13.0.2",
|
||||||
"@ngtools/webpack": "^13.2.6",
|
"@ngtools/webpack": "^13.2.6",
|
||||||
"@nguniversal/builders": "^13.0.2",
|
"@nguniversal/builders": "^13.1.1",
|
||||||
"@types/deep-freeze": "0.1.2",
|
"@types/deep-freeze": "0.1.2",
|
||||||
|
"@types/ejs": "^3.1.1",
|
||||||
"@types/express": "^4.17.9",
|
"@types/express": "^4.17.9",
|
||||||
"@types/file-saver": "^2.0.1",
|
|
||||||
"@types/jasmine": "~3.6.0",
|
"@types/jasmine": "~3.6.0",
|
||||||
"@types/jasminewd2": "~2.0.8",
|
|
||||||
"@types/js-cookie": "2.2.6",
|
"@types/js-cookie": "2.2.6",
|
||||||
"@types/lodash": "^4.14.165",
|
"@types/lodash": "^4.14.165",
|
||||||
"@types/node": "^14.14.9",
|
"@types/node": "^14.14.9",
|
||||||
"@types/sanitize-html": "^2.6.2",
|
"@types/sanitize-html": "^2.6.2",
|
||||||
"@typescript-eslint/eslint-plugin": "5.11.0",
|
"@typescript-eslint/eslint-plugin": "5.11.0",
|
||||||
"@typescript-eslint/parser": "5.11.0",
|
"@typescript-eslint/parser": "5.11.0",
|
||||||
"axe-core": "^4.3.3",
|
"axe-core": "^4.4.3",
|
||||||
"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",
|
||||||
"css-loader": "^6.2.0",
|
"cypress": "9.7.0",
|
||||||
"css-minimizer-webpack-plugin": "^3.4.1",
|
|
||||||
"cssnano": "^5.0.6",
|
|
||||||
"cypress": "9.5.1",
|
|
||||||
"cypress-axe": "^0.14.0",
|
"cypress-axe": "^0.14.0",
|
||||||
"debug-loader": "^0.0.1",
|
|
||||||
"deep-freeze": "0.0.1",
|
"deep-freeze": "0.0.1",
|
||||||
"dotenv": "^8.2.0",
|
|
||||||
"eslint": "^8.2.0",
|
"eslint": "^8.2.0",
|
||||||
"eslint-plugin-deprecation": "^1.3.2",
|
"eslint-plugin-deprecation": "^1.3.2",
|
||||||
"eslint-plugin-import": "^2.25.4",
|
"eslint-plugin-import": "^2.25.4",
|
||||||
"eslint-plugin-jsdoc": "^38.0.6",
|
"eslint-plugin-jsdoc": "^39.6.4",
|
||||||
|
"eslint-plugin-lodash": "^7.4.0",
|
||||||
"eslint-plugin-unused-imports": "^2.0.0",
|
"eslint-plugin-unused-imports": "^2.0.0",
|
||||||
"express-static-gzip": "^2.1.5",
|
"express-static-gzip": "^2.1.5",
|
||||||
"fork-ts-checker-webpack-plugin": "^6.0.3",
|
|
||||||
"html-loader": "^1.3.2",
|
|
||||||
"jasmine-core": "^3.8.0",
|
"jasmine-core": "^3.8.0",
|
||||||
"jasmine-marbles": "0.9.2",
|
"jasmine-marbles": "0.9.2",
|
||||||
"jasmine-spec-reporter": "~5.0.0",
|
|
||||||
"karma": "^6.3.14",
|
"karma": "^6.3.14",
|
||||||
"karma-chrome-launcher": "~3.1.0",
|
"karma-chrome-launcher": "~3.1.0",
|
||||||
"karma-coverage-istanbul-reporter": "~3.0.2",
|
"karma-coverage-istanbul-reporter": "~3.0.2",
|
||||||
@@ -192,26 +182,20 @@
|
|||||||
"karma-jasmine-html-reporter": "^1.5.0",
|
"karma-jasmine-html-reporter": "^1.5.0",
|
||||||
"karma-mocha-reporter": "2.2.5",
|
"karma-mocha-reporter": "2.2.5",
|
||||||
"ngx-mask": "^13.1.7",
|
"ngx-mask": "^13.1.7",
|
||||||
"nodemon": "^2.0.15",
|
"nodemon": "^2.0.20",
|
||||||
"postcss": "^8.1",
|
"postcss": "^8.1",
|
||||||
"postcss-apply": "0.12.0",
|
"postcss-apply": "0.12.0",
|
||||||
"postcss-import": "^14.0.0",
|
"postcss-import": "^14.0.0",
|
||||||
"postcss-loader": "^4.0.3",
|
"postcss-loader": "^4.0.3",
|
||||||
"postcss-preset-env": "^7.4.2",
|
"postcss-preset-env": "^7.4.2",
|
||||||
"postcss-responsive-type": "1.0.0",
|
"postcss-responsive-type": "1.0.0",
|
||||||
"protractor": "^7.0.0",
|
|
||||||
"protractor-istanbul-plugin": "2.0.0",
|
|
||||||
"raw-loader": "0.5.1",
|
|
||||||
"react": "^16.14.0",
|
"react": "^16.14.0",
|
||||||
"react-dom": "^16.14.0",
|
"react-dom": "^16.14.0",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"rxjs-spy": "^8.0.2",
|
"rxjs-spy": "^8.0.2",
|
||||||
"sass": "~1.32.6",
|
"sass": "~1.33.0",
|
||||||
"sass-loader": "^12.6.0",
|
"sass-loader": "^12.6.0",
|
||||||
"sass-resources-loader": "^2.1.1",
|
"sass-resources-loader": "^2.1.1",
|
||||||
"string-replace-loader": "^3.1.0",
|
|
||||||
"terser-webpack-plugin": "^2.3.1",
|
|
||||||
"ts-loader": "^5.2.0",
|
|
||||||
"ts-node": "^8.10.2",
|
"ts-node": "^8.10.2",
|
||||||
"typescript": "~4.5.5",
|
"typescript": "~4.5.5",
|
||||||
"webpack": "^5.69.1",
|
"webpack": "^5.69.1",
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import * as fs from 'fs';
|
import { existsSync, writeFileSync } from 'fs';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
|
||||||
import { AppConfig } from '../src/config/app-config.interface';
|
import { AppConfig } from '../src/config/app-config.interface';
|
||||||
@@ -16,7 +16,7 @@ const appConfig: AppConfig = buildAppConfig();
|
|||||||
|
|
||||||
const angularJsonPath = join(process.cwd(), 'angular.json');
|
const angularJsonPath = join(process.cwd(), 'angular.json');
|
||||||
|
|
||||||
if (!fs.existsSync(angularJsonPath)) {
|
if (!existsSync(angularJsonPath)) {
|
||||||
console.error(`Error:\n${angularJsonPath} does not exist\n`);
|
console.error(`Error:\n${angularJsonPath} does not exist\n`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
@@ -30,7 +30,7 @@ try {
|
|||||||
|
|
||||||
angularJson.projects['dspace-angular'].architect.build.options.baseHref = baseHref;
|
angularJson.projects['dspace-angular'].architect.build.options.baseHref = baseHref;
|
||||||
|
|
||||||
fs.writeFileSync(angularJsonPath, JSON.stringify(angularJson, null, 2) + '\n');
|
writeFileSync(angularJsonPath, JSON.stringify(angularJson, null, 2) + '\n');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import * as fs from 'fs';
|
import { existsSync, writeFileSync } from 'fs';
|
||||||
import * as yaml from 'js-yaml';
|
import { dump } from 'js-yaml';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -18,7 +18,7 @@ if (args[0] === undefined) {
|
|||||||
|
|
||||||
const envFullPath = join(process.cwd(), args[0]);
|
const envFullPath = join(process.cwd(), args[0]);
|
||||||
|
|
||||||
if (!fs.existsSync(envFullPath)) {
|
if (!existsSync(envFullPath)) {
|
||||||
console.error(`Error:\n${envFullPath} does not exist\n`);
|
console.error(`Error:\n${envFullPath} does not exist\n`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
@@ -26,10 +26,10 @@ if (!fs.existsSync(envFullPath)) {
|
|||||||
try {
|
try {
|
||||||
const env = require(envFullPath).environment;
|
const env = require(envFullPath).environment;
|
||||||
|
|
||||||
const config = yaml.dump(env);
|
const config = dump(env);
|
||||||
if (args[1]) {
|
if (args[1]) {
|
||||||
const ymlFullPath = join(process.cwd(), args[1]);
|
const ymlFullPath = join(process.cwd(), args[1]);
|
||||||
fs.writeFileSync(ymlFullPath, config);
|
writeFileSync(ymlFullPath, config);
|
||||||
} else {
|
} else {
|
||||||
console.log(config);
|
console.log(config);
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import * as child from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
|
|
||||||
import { AppConfig } from '../src/config/app-config.interface';
|
import { AppConfig } from '../src/config/app-config.interface';
|
||||||
import { buildAppConfig } from '../src/config/config.server';
|
import { buildAppConfig } from '../src/config/config.server';
|
||||||
@@ -9,7 +9,7 @@ const appConfig: AppConfig = buildAppConfig();
|
|||||||
* Calls `ng serve` with the following arguments configured for the UI in the app config: host, port, nameSpace, ssl
|
* Calls `ng serve` with the following arguments configured for the UI in the app config: host, port, nameSpace, ssl
|
||||||
* Any CLI arguments given to this script are patched through to `ng serve` as well.
|
* Any CLI arguments given to this script are patched through to `ng serve` as well.
|
||||||
*/
|
*/
|
||||||
child.spawn(
|
spawn(
|
||||||
`ng serve --host ${appConfig.ui.host} --port ${appConfig.ui.port} --serve-path ${appConfig.ui.nameSpace} --ssl ${appConfig.ui.ssl} ${process.argv.slice(2).join(' ')} --configuration development`,
|
`ng serve --host ${appConfig.ui.host} --port ${appConfig.ui.port} --serve-path ${appConfig.ui.nameSpace} --ssl ${appConfig.ui.ssl} ${process.argv.slice(2).join(' ')} --configuration development`,
|
||||||
{ stdio: 'inherit', shell: true }
|
{ stdio: 'inherit', shell: true }
|
||||||
);
|
);
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import * as http from 'http';
|
import { request } from 'http';
|
||||||
import * as https from 'https';
|
import { request as https_request } from 'https';
|
||||||
|
|
||||||
import { AppConfig } from '../src/config/app-config.interface';
|
import { AppConfig } from '../src/config/app-config.interface';
|
||||||
import { buildAppConfig } from '../src/config/config.server';
|
import { buildAppConfig } from '../src/config/config.server';
|
||||||
@@ -20,7 +20,7 @@ console.log(`...Testing connection to REST API at ${restUrl}...\n`);
|
|||||||
|
|
||||||
// If SSL enabled, test via HTTPS, else via HTTP
|
// If SSL enabled, test via HTTPS, else via HTTP
|
||||||
if (appConfig.rest.ssl) {
|
if (appConfig.rest.ssl) {
|
||||||
const req = https.request(restUrl, (res) => {
|
const req = https_request(restUrl, (res) => {
|
||||||
console.log(`RESPONSE: ${res.statusCode} ${res.statusMessage} \n`);
|
console.log(`RESPONSE: ${res.statusCode} ${res.statusMessage} \n`);
|
||||||
// We will keep reading data until the 'end' event fires.
|
// We will keep reading data until the 'end' event fires.
|
||||||
// This ensures we don't just read the first chunk.
|
// This ensures we don't just read the first chunk.
|
||||||
@@ -39,7 +39,7 @@ if (appConfig.rest.ssl) {
|
|||||||
|
|
||||||
req.end();
|
req.end();
|
||||||
} else {
|
} else {
|
||||||
const req = http.request(restUrl, (res) => {
|
const req = request(restUrl, (res) => {
|
||||||
console.log(`RESPONSE: ${res.statusCode} ${res.statusMessage} \n`);
|
console.log(`RESPONSE: ${res.statusCode} ${res.statusMessage} \n`);
|
||||||
// We will keep reading data until the 'end' event fires.
|
// We will keep reading data until the 'end' event fires.
|
||||||
// This ensures we don't just read the first chunk.
|
// This ensures we don't just read the first chunk.
|
||||||
|
293
server.ts
293
server.ts
@@ -19,19 +19,24 @@ import 'zone.js/node';
|
|||||||
import 'reflect-metadata';
|
import 'reflect-metadata';
|
||||||
import 'rxjs';
|
import 'rxjs';
|
||||||
|
|
||||||
import axios from 'axios';
|
/* eslint-disable import/no-namespace */
|
||||||
import * as pem from 'pem';
|
|
||||||
import * as https from 'https';
|
|
||||||
import * as morgan from 'morgan';
|
import * as morgan from 'morgan';
|
||||||
import * as express from 'express';
|
import * as express from 'express';
|
||||||
import * as bodyParser from 'body-parser';
|
import * as ejs from 'ejs';
|
||||||
import * as compression from 'compression';
|
import * as compression from 'compression';
|
||||||
import * as expressStaticGzip from 'express-static-gzip';
|
import * as expressStaticGzip from 'express-static-gzip';
|
||||||
|
/* eslint-enable import/no-namespace */
|
||||||
|
|
||||||
|
import axios from 'axios';
|
||||||
|
import LRU from 'lru-cache';
|
||||||
|
import isbot from 'isbot';
|
||||||
|
import { createCertificate } from 'pem';
|
||||||
|
import { createServer } from 'https';
|
||||||
|
import { json } from 'body-parser';
|
||||||
|
|
||||||
import { existsSync, readFileSync } from 'fs';
|
import { existsSync, readFileSync } from 'fs';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
|
||||||
import { APP_BASE_HREF } from '@angular/common';
|
|
||||||
import { enableProdMode } from '@angular/core';
|
import { enableProdMode } from '@angular/core';
|
||||||
|
|
||||||
import { ngExpressEngine } from '@nguniversal/express-engine';
|
import { ngExpressEngine } from '@nguniversal/express-engine';
|
||||||
@@ -49,6 +54,8 @@ import { buildAppConfig } from './src/config/config.server';
|
|||||||
import { APP_CONFIG, AppConfig } from './src/config/app-config.interface';
|
import { APP_CONFIG, AppConfig } from './src/config/app-config.interface';
|
||||||
import { extendEnvironmentWithAppConfig } from './src/config/config.util';
|
import { extendEnvironmentWithAppConfig } from './src/config/config.util';
|
||||||
import { logStartupMessage } from './startup-message';
|
import { logStartupMessage } from './startup-message';
|
||||||
|
import { TOKENITEM } from 'src/app/core/auth/models/auth-token-info.model';
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Set path for the browser application's dist folder
|
* Set path for the browser application's dist folder
|
||||||
@@ -57,12 +64,18 @@ const DIST_FOLDER = join(process.cwd(), 'dist/browser');
|
|||||||
// Set path fir IIIF viewer.
|
// Set path fir IIIF viewer.
|
||||||
const IIIF_VIEWER = join(process.cwd(), 'dist/iiif');
|
const IIIF_VIEWER = join(process.cwd(), 'dist/iiif');
|
||||||
|
|
||||||
const indexHtml = existsSync(join(DIST_FOLDER, 'index.html')) ? 'index.html' : 'index';
|
const indexHtml = join(DIST_FOLDER, 'index.html');
|
||||||
|
|
||||||
const cookieParser = require('cookie-parser');
|
const cookieParser = require('cookie-parser');
|
||||||
|
|
||||||
const appConfig: AppConfig = buildAppConfig(join(DIST_FOLDER, 'assets/config.json'));
|
const appConfig: AppConfig = buildAppConfig(join(DIST_FOLDER, 'assets/config.json'));
|
||||||
|
|
||||||
|
// cache of SSR pages for known bots, only enabled in production mode
|
||||||
|
let botCache: LRU<string, any>;
|
||||||
|
|
||||||
|
// cache of SSR pages for anonymous users. Disabled by default, and only available in production mode
|
||||||
|
let anonymousCache: LRU<string, any>;
|
||||||
|
|
||||||
// extend environment with app config for server
|
// extend environment with app config for server
|
||||||
extendEnvironmentWithAppConfig(environment, appConfig);
|
extendEnvironmentWithAppConfig(environment, appConfig);
|
||||||
|
|
||||||
@@ -83,10 +96,12 @@ export function app() {
|
|||||||
/*
|
/*
|
||||||
* If production mode is enabled in the environment file:
|
* If production mode is enabled in the environment file:
|
||||||
* - Enable Angular's production mode
|
* - Enable Angular's production mode
|
||||||
|
* - Initialize caching of SSR rendered pages (if enabled in config.yml)
|
||||||
* - Enable compression for SSR reponses. See [compression](https://github.com/expressjs/compression)
|
* - Enable compression for SSR reponses. See [compression](https://github.com/expressjs/compression)
|
||||||
*/
|
*/
|
||||||
if (environment.production) {
|
if (environment.production) {
|
||||||
enableProdMode();
|
enableProdMode();
|
||||||
|
initCache();
|
||||||
server.use(compression({
|
server.use(compression({
|
||||||
// only compress responses we've marked as SSR
|
// only compress responses we've marked as SSR
|
||||||
// otherwise, this middleware may compress files we've chosen not to compress via compression-webpack-plugin
|
// otherwise, this middleware may compress files we've chosen not to compress via compression-webpack-plugin
|
||||||
@@ -102,15 +117,15 @@ export function app() {
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* Add cookie parser middleware
|
* Add cookie parser middleware
|
||||||
* See [morgan](https://github.com/expressjs/cookie-parser)
|
* See [cookie-parser](https://github.com/expressjs/cookie-parser)
|
||||||
*/
|
*/
|
||||||
server.use(cookieParser());
|
server.use(cookieParser());
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Add parser for request bodies
|
* Add JSON parser for request bodies
|
||||||
* See [morgan](https://github.com/expressjs/body-parser)
|
* See [body-parser](https://github.com/expressjs/body-parser)
|
||||||
*/
|
*/
|
||||||
server.use(bodyParser.json());
|
server.use(json());
|
||||||
|
|
||||||
// Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine)
|
// Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine)
|
||||||
server.engine('html', (_, options, callback) =>
|
server.engine('html', (_, options, callback) =>
|
||||||
@@ -133,10 +148,23 @@ export function app() {
|
|||||||
})(_, (options as any), callback)
|
})(_, (options as any), callback)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
server.engine('ejs', ejs.renderFile);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Register the view engines for html and ejs
|
* Register the view engines for html and ejs
|
||||||
*/
|
*/
|
||||||
server.set('view engine', 'html');
|
server.set('view engine', 'html');
|
||||||
|
server.set('view engine', 'ejs');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serve the robots.txt ejs template, filling in the origin variable
|
||||||
|
*/
|
||||||
|
server.get('/robots.txt', (req, res) => {
|
||||||
|
res.setHeader('content-type', 'text/plain');
|
||||||
|
res.render('assets/robots.txt.ejs', {
|
||||||
|
'origin': req.protocol + '://' + req.headers.host
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Set views folder path to directory where template files are stored
|
* Set views folder path to directory where template files are stored
|
||||||
@@ -169,7 +197,7 @@ export function app() {
|
|||||||
* Serve static resources (images, i18n messages, …)
|
* Serve static resources (images, i18n messages, …)
|
||||||
* Handle pre-compressed files with [express-static-gzip](https://github.com/tkoenig89/express-static-gzip)
|
* Handle pre-compressed files with [express-static-gzip](https://github.com/tkoenig89/express-static-gzip)
|
||||||
*/
|
*/
|
||||||
router.get('*.*', cacheControl, expressStaticGzip(DIST_FOLDER, {
|
router.get('*.*', addCacheControl, expressStaticGzip(DIST_FOLDER, {
|
||||||
index: false,
|
index: false,
|
||||||
enableBrotli: true,
|
enableBrotli: true,
|
||||||
orderPreference: ['br', 'gzip'],
|
orderPreference: ['br', 'gzip'],
|
||||||
@@ -185,8 +213,11 @@ export function app() {
|
|||||||
*/
|
*/
|
||||||
server.get('/app/health', healthCheck);
|
server.get('/app/health', healthCheck);
|
||||||
|
|
||||||
// Register the ngApp callback function to handle incoming requests
|
/**
|
||||||
router.get('*', ngApp);
|
* Default sending all incoming requests to ngApp() function, after first checking for a cached
|
||||||
|
* copy of the page (see cacheCheck())
|
||||||
|
*/
|
||||||
|
router.get('*', cacheCheck, ngApp);
|
||||||
|
|
||||||
server.use(environment.ui.nameSpace, router);
|
server.use(environment.ui.nameSpace, router);
|
||||||
|
|
||||||
@@ -198,6 +229,25 @@ export function app() {
|
|||||||
*/
|
*/
|
||||||
function ngApp(req, res) {
|
function ngApp(req, res) {
|
||||||
if (environment.universal.preboot) {
|
if (environment.universal.preboot) {
|
||||||
|
// Render the page to user via SSR (server side rendering)
|
||||||
|
serverSideRender(req, res);
|
||||||
|
} else {
|
||||||
|
// If preboot is disabled, just serve the client
|
||||||
|
console.log('Universal off, serving for direct client-side rendering (CSR)');
|
||||||
|
clientSideRender(req, res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render page content on server side using Angular SSR. By default this page content is
|
||||||
|
* returned to the user.
|
||||||
|
* @param req current request
|
||||||
|
* @param res current response
|
||||||
|
* @param sendToUser if true (default), send the rendered content to the user.
|
||||||
|
* If false, then only save this rendered content to the in-memory cache (to refresh cache).
|
||||||
|
*/
|
||||||
|
function serverSideRender(req, res, sendToUser: boolean = true) {
|
||||||
|
// Render the page via SSR (server side rendering)
|
||||||
res.render(indexHtml, {
|
res.render(indexHtml, {
|
||||||
req,
|
req,
|
||||||
res,
|
res,
|
||||||
@@ -207,52 +257,215 @@ function ngApp(req, res) {
|
|||||||
baseUrl: environment.ui.nameSpace,
|
baseUrl: environment.ui.nameSpace,
|
||||||
originUrl: environment.ui.baseUrl,
|
originUrl: environment.ui.baseUrl,
|
||||||
requestUrl: req.originalUrl,
|
requestUrl: req.originalUrl,
|
||||||
providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }]
|
|
||||||
}, (err, data) => {
|
}, (err, data) => {
|
||||||
if (hasNoValue(err) && hasValue(data)) {
|
if (hasNoValue(err) && hasValue(data)) {
|
||||||
res.locals.ssr = true; // mark response as SSR
|
// save server side rendered page to cache (if any are enabled)
|
||||||
|
saveToCache(req, data);
|
||||||
|
if (sendToUser) {
|
||||||
|
res.locals.ssr = true; // mark response as SSR (enables text compression)
|
||||||
|
// send rendered page to user
|
||||||
res.send(data);
|
res.send(data);
|
||||||
|
}
|
||||||
} else if (hasValue(err) && err.code === 'ERR_HTTP_HEADERS_SENT') {
|
} else if (hasValue(err) && err.code === 'ERR_HTTP_HEADERS_SENT') {
|
||||||
// When this error occurs we can't fall back to CSR because the response has already been
|
// When this error occurs we can't fall back to CSR because the response has already been
|
||||||
// sent. These errors occur for various reasons in universal, not all of which are in our
|
// sent. These errors occur for various reasons in universal, not all of which are in our
|
||||||
// control to solve.
|
// control to solve.
|
||||||
console.warn('Warning [ERR_HTTP_HEADERS_SENT]: Tried to set headers after they were sent to the client');
|
console.warn('Warning [ERR_HTTP_HEADERS_SENT]: Tried to set headers after they were sent to the client');
|
||||||
} else {
|
} else {
|
||||||
console.warn('Error in SSR, serving for direct CSR.');
|
console.warn('Error in server-side rendering (SSR)');
|
||||||
if (hasValue(err)) {
|
if (hasValue(err)) {
|
||||||
console.warn('Error details : ', err);
|
console.warn('Error details : ', err);
|
||||||
}
|
}
|
||||||
res.render(indexHtml, {
|
if (sendToUser) {
|
||||||
req,
|
console.warn('Falling back to serving direct client-side rendering (CSR).');
|
||||||
providers: [{
|
clientSideRender(req, res);
|
||||||
provide: APP_BASE_HREF,
|
}
|
||||||
useValue: req.baseUrl
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
}
|
||||||
// If preboot is disabled, just serve the client
|
|
||||||
console.log('Universal off, serving for direct CSR');
|
/**
|
||||||
res.render(indexHtml, {
|
* Send back response to user to trigger direct client-side rendering (CSR)
|
||||||
req,
|
* @param req current request
|
||||||
providers: [{
|
* @param res current response
|
||||||
provide: APP_BASE_HREF,
|
*/
|
||||||
useValue: req.baseUrl
|
function clientSideRender(req, res) {
|
||||||
}]
|
res.sendFile(indexHtml);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Adds a Cache-Control HTTP header to the response.
|
||||||
|
* The cache control value can be configured in the config.*.yml file
|
||||||
|
* Defaults to max-age=604,800 seconds (1 week)
|
||||||
|
*/
|
||||||
|
function addCacheControl(req, res, next) {
|
||||||
|
// instruct browser to revalidate
|
||||||
|
res.header('Cache-Control', environment.cache.control || 'max-age=604800');
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Initialize server-side caching of pages rendered via SSR.
|
||||||
|
*/
|
||||||
|
function initCache() {
|
||||||
|
if (botCacheEnabled()) {
|
||||||
|
// Initialize a new "least-recently-used" item cache (where least recently used pages are removed first)
|
||||||
|
// See https://www.npmjs.com/package/lru-cache
|
||||||
|
// When enabled, each page defaults to expiring after 1 day
|
||||||
|
botCache = new LRU( {
|
||||||
|
max: environment.cache.serverSide.botCache.max,
|
||||||
|
ttl: environment.cache.serverSide.botCache.timeToLive || 24 * 60 * 60 * 1000, // 1 day
|
||||||
|
allowStale: environment.cache.serverSide.botCache.allowStale ?? true // if object is stale, return stale value before deleting
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (anonymousCacheEnabled()) {
|
||||||
|
// NOTE: While caches may share SSR pages, this cache must be kept separately because the timeToLive
|
||||||
|
// may expire pages more frequently.
|
||||||
|
// When enabled, each page defaults to expiring after 10 seconds (to minimize anonymous users seeing out-of-date content)
|
||||||
|
anonymousCache = new LRU( {
|
||||||
|
max: environment.cache.serverSide.anonymousCache.max,
|
||||||
|
ttl: environment.cache.serverSide.anonymousCache.timeToLive || 10 * 1000, // 10 seconds
|
||||||
|
allowStale: environment.cache.serverSide.anonymousCache.allowStale ?? true // if object is stale, return stale value before deleting
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/**
|
||||||
* Adds a cache control header to the response
|
* Return whether bot-specific server side caching is enabled in configuration.
|
||||||
* The cache control value can be configured in the environments file and defaults to max-age=60
|
|
||||||
*/
|
*/
|
||||||
function cacheControl(req, res, next) {
|
function botCacheEnabled(): boolean {
|
||||||
// instruct browser to revalidate
|
// Caching is only enabled if SSR is enabled AND
|
||||||
res.header('Cache-Control', environment.cache.control || 'max-age=60');
|
// "max" pages to cache is greater than zero
|
||||||
|
return environment.universal.preboot && environment.cache.serverSide.botCache.max && (environment.cache.serverSide.botCache.max > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return whether anonymous user server side caching is enabled in configuration.
|
||||||
|
*/
|
||||||
|
function anonymousCacheEnabled(): boolean {
|
||||||
|
// Caching is only enabled if SSR is enabled AND
|
||||||
|
// "max" pages to cache is greater than zero
|
||||||
|
return environment.universal.preboot && environment.cache.serverSide.anonymousCache.max && (environment.cache.serverSide.anonymousCache.max > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the currently requested page is in our server-side, in-memory cache.
|
||||||
|
* Caching is ONLY done for SSR requests. Pages are cached base on their path (e.g. /home or /search?query=test)
|
||||||
|
*/
|
||||||
|
function cacheCheck(req, res, next) {
|
||||||
|
// Cached copy of page (if found)
|
||||||
|
let cachedCopy;
|
||||||
|
|
||||||
|
// If the bot cache is enabled and this request looks like a bot, check the bot cache for a cached page.
|
||||||
|
if (botCacheEnabled() && isbot(req.get('user-agent'))) {
|
||||||
|
cachedCopy = checkCacheForRequest('bot', botCache, req, res);
|
||||||
|
} else if (anonymousCacheEnabled() && !isUserAuthenticated(req)) {
|
||||||
|
cachedCopy = checkCacheForRequest('anonymous', anonymousCache, req, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If cached copy exists, return it to the user.
|
||||||
|
if (cachedCopy) {
|
||||||
|
res.locals.ssr = true; // mark response as SSR-generated (enables text compression)
|
||||||
|
res.send(cachedCopy);
|
||||||
|
|
||||||
|
// Tell Express to skip all other handlers for this path
|
||||||
|
// This ensures we don't try to re-render the page since we've already returned the cached copy
|
||||||
|
next('router');
|
||||||
|
} else {
|
||||||
|
// If nothing found in cache, just continue with next handler
|
||||||
|
// (This should send the request on to the handler that rerenders the page via SSR
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the current request (i.e. page) is found in the given cache. If it is found,
|
||||||
|
* the cached copy is returned. When found, this method also triggers a re-render via
|
||||||
|
* SSR if the cached copy is now expired (i.e. timeToLive has passed for this cached copy).
|
||||||
|
* @param cacheName name of cache (just useful for debug logging)
|
||||||
|
* @param cache LRU cache to check
|
||||||
|
* @param req current request to look for in the cache
|
||||||
|
* @param res current response
|
||||||
|
* @returns cached copy (if found) or undefined (if not found)
|
||||||
|
*/
|
||||||
|
function checkCacheForRequest(cacheName: string, cache: LRU<string, any>, req, res): any {
|
||||||
|
// Get the cache key for this request
|
||||||
|
const key = getCacheKey(req);
|
||||||
|
|
||||||
|
// Check if this page is in our cache
|
||||||
|
let cachedCopy = cache.get(key);
|
||||||
|
if (cachedCopy) {
|
||||||
|
if (environment.cache.serverSide.debug) { console.log(`CACHE HIT FOR ${key} in ${cacheName} cache`); }
|
||||||
|
|
||||||
|
// Check if cached copy is expired (If expired, the key will now be gone from cache)
|
||||||
|
// NOTE: This will only occur when "allowStale=true", as it means the "get(key)" above returned a stale value.
|
||||||
|
if (!cache.has(key)) {
|
||||||
|
if (environment.cache.serverSide.debug) { console.log(`CACHE EXPIRED FOR ${key} in ${cacheName} cache. Re-rendering...`); }
|
||||||
|
// Update cached copy by rerendering server-side
|
||||||
|
// NOTE: In this scenario the currently cached copy will be returned to the current user.
|
||||||
|
// This re-render is peformed behind the scenes to update cached copy for next user.
|
||||||
|
serverSideRender(req, res, false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (environment.cache.serverSide.debug) { console.log(`CACHE MISS FOR ${key} in ${cacheName} cache.`); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// return page from cache
|
||||||
|
return cachedCopy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a cache key from the current request.
|
||||||
|
* The cache key is the URL path (NOTE: this key will also include any querystring params).
|
||||||
|
* E.g. "/home" or "/search?query=test"
|
||||||
|
* @param req current request
|
||||||
|
* @returns cache key to use for this page
|
||||||
|
*/
|
||||||
|
function getCacheKey(req): string {
|
||||||
|
// NOTE: this will return the URL path *without* any baseUrl
|
||||||
|
return req.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save page to server side cache(s), if enabled. If caching is not enabled or a user is authenticated, this is a noop
|
||||||
|
* If multiple caches are enabled, the page will be saved to any caches where it does not yet exist (or is expired).
|
||||||
|
* (This minimizes the number of times we need to run SSR on the same page.)
|
||||||
|
* @param req current page request
|
||||||
|
* @param page page data to save to cache
|
||||||
|
*/
|
||||||
|
function saveToCache(req, page: any) {
|
||||||
|
// Only cache if no one is currently authenticated. This means ONLY public pages can be cached.
|
||||||
|
// NOTE: It's not safe to save page data to the cache when a user is authenticated. In that situation,
|
||||||
|
// the page may include sensitive or user-specific materials. As the cache is shared across all users, it can only contain public info.
|
||||||
|
if (!isUserAuthenticated(req)) {
|
||||||
|
const key = getCacheKey(req);
|
||||||
|
// Avoid caching "/reload/[random]" paths (these are hard refreshes after logout)
|
||||||
|
if (key.startsWith('/reload')) { return; }
|
||||||
|
|
||||||
|
// If bot cache is enabled, save it to that cache if it doesn't exist or is expired
|
||||||
|
// (NOTE: has() will return false if page is expired in cache)
|
||||||
|
if (botCacheEnabled() && !botCache.has(key)) {
|
||||||
|
botCache.set(key, page);
|
||||||
|
if (environment.cache.serverSide.debug) { console.log(`CACHE SAVE FOR ${key} in bot cache.`); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// If anonymous cache is enabled, save it to that cache if it doesn't exist or is expired
|
||||||
|
if (anonymousCacheEnabled() && !anonymousCache.has(key)) {
|
||||||
|
anonymousCache.set(key, page);
|
||||||
|
if (environment.cache.serverSide.debug) { console.log(`CACHE SAVE FOR ${key} in anonymous cache.`); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether a user is authenticated or not
|
||||||
|
*/
|
||||||
|
function isUserAuthenticated(req): boolean {
|
||||||
|
// Check whether our DSpace authentication Cookie exists or not
|
||||||
|
return req.cookies[TOKENITEM];
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Callback function for when the server has started
|
* Callback function for when the server has started
|
||||||
@@ -266,7 +479,7 @@ function serverStarted() {
|
|||||||
* @param keys SSL credentials
|
* @param keys SSL credentials
|
||||||
*/
|
*/
|
||||||
function createHttpsServer(keys) {
|
function createHttpsServer(keys) {
|
||||||
https.createServer({
|
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, () => {
|
||||||
@@ -320,7 +533,7 @@ function start() {
|
|||||||
|
|
||||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; // lgtm[js/disabling-certificate-validation]
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; // lgtm[js/disabling-certificate-validation]
|
||||||
|
|
||||||
pem.createCertificate({
|
createCertificate({
|
||||||
days: 1,
|
days: 1,
|
||||||
selfSigned: true
|
selfSigned: true
|
||||||
}, (error, keys) => {
|
}, (error, keys) => {
|
||||||
|
@@ -10,6 +10,16 @@ import { MembersListComponent } from './group-registry/group-form/members-list/m
|
|||||||
import { SubgroupsListComponent } from './group-registry/group-form/subgroup-list/subgroups-list.component';
|
import { SubgroupsListComponent } from './group-registry/group-form/subgroup-list/subgroups-list.component';
|
||||||
import { GroupsRegistryComponent } from './group-registry/groups-registry.component';
|
import { GroupsRegistryComponent } from './group-registry/groups-registry.component';
|
||||||
import { FormModule } from '../shared/form/form.module';
|
import { FormModule } from '../shared/form/form.module';
|
||||||
|
import { DYNAMIC_ERROR_MESSAGES_MATCHER, DynamicErrorMessagesMatcher } from '@ng-dynamic-forms/core';
|
||||||
|
import { AbstractControl } from '@angular/forms';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Condition for displaying error messages on email form field
|
||||||
|
*/
|
||||||
|
export const ValidateEmailErrorStateMatcher: DynamicErrorMessagesMatcher =
|
||||||
|
(control: AbstractControl, model: any, hasFocus: boolean) => {
|
||||||
|
return (control.touched && !hasFocus) || (control.errors?.emailTaken && hasFocus);
|
||||||
|
};
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -17,7 +27,10 @@ import { FormModule } from '../shared/form/form.module';
|
|||||||
SharedModule,
|
SharedModule,
|
||||||
RouterModule,
|
RouterModule,
|
||||||
AccessControlRoutingModule,
|
AccessControlRoutingModule,
|
||||||
FormModule
|
FormModule,
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
MembersListComponent,
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
EPeopleRegistryComponent,
|
EPeopleRegistryComponent,
|
||||||
@@ -25,7 +38,13 @@ import { FormModule } from '../shared/form/form.module';
|
|||||||
GroupsRegistryComponent,
|
GroupsRegistryComponent,
|
||||||
GroupFormComponent,
|
GroupFormComponent,
|
||||||
SubgroupsListComponent,
|
SubgroupsListComponent,
|
||||||
MembersListComponent
|
MembersListComponent,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: DYNAMIC_ERROR_MESSAGES_MATCHER,
|
||||||
|
useValue: ValidateEmailErrorStateMatcher
|
||||||
|
},
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
/**
|
/**
|
||||||
|
@@ -8,7 +8,7 @@
|
|||||||
<button class="mr-auto btn btn-success addEPerson-button"
|
<button class="mr-auto btn btn-success addEPerson-button"
|
||||||
(click)="isEPersonFormShown = true">
|
(click)="isEPersonFormShown = true">
|
||||||
<i class="fas fa-plus"></i>
|
<i class="fas fa-plus"></i>
|
||||||
<span class="d-none d-sm-inline">{{labelPrefix + 'button.add' | translate}}</span>
|
<span class="d-none d-sm-inline ml-1">{{labelPrefix + 'button.add' | translate}}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
<div class="flex-grow-1 mr-3 ml-3">
|
<div class="flex-grow-1 mr-3 ml-3">
|
||||||
<div class="form-group input-group">
|
<div class="form-group input-group">
|
||||||
<input type="text" name="query" id="query" formControlName="query"
|
<input type="text" name="query" id="query" formControlName="query"
|
||||||
class="form-control" attr.aria-label="{{labelPrefix + 'search.placeholder' | translate}}"
|
class="form-control" [attr.aria-label]="labelPrefix + 'search.placeholder' | translate"
|
||||||
[placeholder]="(labelPrefix + 'search.placeholder' | translate)">
|
[placeholder]="(labelPrefix + 'search.placeholder' | translate)">
|
||||||
<span class="input-group-append">
|
<span class="input-group-append">
|
||||||
<button type="submit" class="search-button btn btn-primary">
|
<button type="submit" class="search-button btn btn-primary">
|
||||||
@@ -72,13 +72,13 @@
|
|||||||
<td>{{epersonDto.eperson.email}}</td>
|
<td>{{epersonDto.eperson.email}}</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="btn-group edit-field">
|
<div class="btn-group edit-field">
|
||||||
<button class="delete-button" (click)="toggleEditEPerson(epersonDto.eperson)"
|
<button (click)="toggleEditEPerson(epersonDto.eperson)"
|
||||||
class="btn btn-outline-primary btn-sm access-control-editEPersonButton"
|
class="btn btn-outline-primary btn-sm access-control-editEPersonButton"
|
||||||
title="{{labelPrefix + 'table.edit.buttons.edit' | translate: { name: dsoNameService.getName(epersonDto.eperson) } }}">
|
title="{{labelPrefix + 'table.edit.buttons.edit' | translate: { name: dsoNameService.getName(epersonDto.eperson) } }}">
|
||||||
<i class="fas fa-edit fa-fw"></i>
|
<i class="fas fa-edit fa-fw"></i>
|
||||||
</button>
|
</button>
|
||||||
<button [disabled]="!epersonDto.ableToDelete" (click)="deleteEPerson(epersonDto.eperson)"
|
<button [disabled]="!epersonDto.ableToDelete" (click)="deleteEPerson(epersonDto.eperson)"
|
||||||
class="btn btn-outline-danger btn-sm access-control-deleteEPersonButton"
|
class="delete-button btn btn-outline-danger btn-sm access-control-deleteEPersonButton"
|
||||||
title="{{labelPrefix + 'table.edit.buttons.remove' | translate: { name: dsoNameService.getName(epersonDto.eperson) } }}">
|
title="{{labelPrefix + 'table.edit.buttons.remove' | translate: { name: dsoNameService.getName(epersonDto.eperson) } }}">
|
||||||
<i class="fas fa-trash-alt fa-fw"></i>
|
<i class="fas fa-trash-alt fa-fw"></i>
|
||||||
</button>
|
</button>
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { Observable, of as observableOf } from 'rxjs';
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing';
|
import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing';
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { BrowserModule, By } from '@angular/platform-browser';
|
import { BrowserModule, By } from '@angular/platform-browser';
|
||||||
@@ -42,6 +42,7 @@ describe('EPeopleRegistryComponent', () => {
|
|||||||
let paginationService;
|
let paginationService;
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
|
jasmine.getEnv().allowRespy(true);
|
||||||
mockEPeople = [EPersonMock, EPersonMock2];
|
mockEPeople = [EPersonMock, EPersonMock2];
|
||||||
ePersonDataServiceStub = {
|
ePersonDataServiceStub = {
|
||||||
activeEPerson: null,
|
activeEPerson: null,
|
||||||
@@ -260,17 +261,16 @@ describe('EPeopleRegistryComponent', () => {
|
|||||||
describe('delete EPerson button when the isAuthorized returns false', () => {
|
describe('delete EPerson button when the isAuthorized returns false', () => {
|
||||||
let ePeopleDeleteButton;
|
let ePeopleDeleteButton;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
authorizationService = jasmine.createSpyObj('authorizationService', {
|
spyOn(authorizationService, 'isAuthorized').and.returnValue(observableOf(false));
|
||||||
isAuthorized: observableOf(false)
|
component.initialisePage();
|
||||||
});
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be disabled', () => {
|
it('should be disabled', () => {
|
||||||
ePeopleDeleteButton = fixture.debugElement.queryAll(By.css('#epeople tr td div button.delete-button'));
|
ePeopleDeleteButton = fixture.debugElement.queryAll(By.css('#epeople tr td div button.delete-button'));
|
||||||
ePeopleDeleteButton.forEach((deleteButton) => {
|
ePeopleDeleteButton.forEach((deleteButton: DebugElement) => {
|
||||||
expect(deleteButton.nativeElement.disabled).toBe(true);
|
expect(deleteButton.nativeElement.disabled).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -537,7 +537,7 @@ describe('EPersonFormComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should call epersonRegistrationService.registerEmail', () => {
|
it('should call epersonRegistrationService.registerEmail', () => {
|
||||||
expect(epersonRegistrationService.registerEmail).toHaveBeenCalledWith(ePersonEmail);
|
expect(epersonRegistrationService.registerEmail).toHaveBeenCalledWith(ePersonEmail, null, 'forgot');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -36,6 +36,7 @@ import { followLink } from '../../../shared/utils/follow-link-config.model';
|
|||||||
import { ValidateEmailNotTaken } from './validators/email-taken.validator';
|
import { ValidateEmailNotTaken } from './validators/email-taken.validator';
|
||||||
import { Registration } from '../../../core/shared/registration.model';
|
import { Registration } from '../../../core/shared/registration.model';
|
||||||
import { EpersonRegistrationService } from '../../../core/data/eperson-registration.service';
|
import { EpersonRegistrationService } from '../../../core/data/eperson-registration.service';
|
||||||
|
import { TYPE_REQUEST_FORGOT } from '../../../register-email-form/register-email-form.component';
|
||||||
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
|
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -493,7 +494,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
resetPassword() {
|
resetPassword() {
|
||||||
if (hasValue(this.epersonInitial.email)) {
|
if (hasValue(this.epersonInitial.email)) {
|
||||||
this.epersonRegistrationService.registerEmail(this.epersonInitial.email).pipe(getFirstCompletedRemoteData())
|
this.epersonRegistrationService.registerEmail(this.epersonInitial.email, null, TYPE_REQUEST_FORGOT).pipe(getFirstCompletedRemoteData())
|
||||||
.subscribe((response: RemoteData<Registration>) => {
|
.subscribe((response: RemoteData<Registration>) => {
|
||||||
if (response.hasSucceeded) {
|
if (response.hasSucceeded) {
|
||||||
this.notificationsService.success(this.translateService.get('admin.access-control.epeople.actions.reset'),
|
this.notificationsService.success(this.translateService.get('admin.access-control.epeople.actions.reset'),
|
||||||
|
@@ -9,7 +9,18 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ng-template #editheader>
|
<ng-template #editheader>
|
||||||
<h2 class="border-bottom pb-2">{{messagePrefix + '.head.edit' | translate}}</h2>
|
<h2 class="border-bottom pb-2">
|
||||||
|
<span
|
||||||
|
*dsContextHelp="{
|
||||||
|
content: 'admin.access-control.groups.form.tooltip.editGroupPage',
|
||||||
|
id: 'edit-group-page',
|
||||||
|
iconPlacement: 'right',
|
||||||
|
tooltipPlacement: ['right', 'bottom']
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{messagePrefix + '.head.edit' | translate}}
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ds-alert *ngIf="groupBeingEdited?.permanent" [type]="AlertTypeEnum.Warning"
|
<ds-alert *ngIf="groupBeingEdited?.permanent" [type]="AlertTypeEnum.Warning"
|
||||||
|
@@ -269,6 +269,43 @@ describe('GroupFormComponent', () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should edit with name and description operations', () => {
|
||||||
|
const operations = [{
|
||||||
|
op: 'add',
|
||||||
|
path: '/metadata/dc.description',
|
||||||
|
value: 'testDescription'
|
||||||
|
}, {
|
||||||
|
op: 'replace',
|
||||||
|
path: '/name',
|
||||||
|
value: 'newGroupName'
|
||||||
|
}];
|
||||||
|
expect(groupsDataServiceStub.patch).toHaveBeenCalledWith(expected, operations);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should edit with description operations', () => {
|
||||||
|
component.groupName.value = null;
|
||||||
|
component.onSubmit();
|
||||||
|
fixture.detectChanges();
|
||||||
|
const operations = [{
|
||||||
|
op: 'add',
|
||||||
|
path: '/metadata/dc.description',
|
||||||
|
value: 'testDescription'
|
||||||
|
}];
|
||||||
|
expect(groupsDataServiceStub.patch).toHaveBeenCalledWith(expected, operations);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should edit with name operations', () => {
|
||||||
|
component.groupDescription.value = null;
|
||||||
|
component.onSubmit();
|
||||||
|
fixture.detectChanges();
|
||||||
|
const operations = [{
|
||||||
|
op: 'replace',
|
||||||
|
path: '/name',
|
||||||
|
value: 'newGroupName'
|
||||||
|
}];
|
||||||
|
expect(groupsDataServiceStub.patch).toHaveBeenCalledWith(expected, operations);
|
||||||
|
});
|
||||||
|
|
||||||
it('should emit the existing group using the correct new values', (async () => {
|
it('should emit the existing group using the correct new values', (async () => {
|
||||||
await fixture.whenStable().then(() => {
|
await fixture.whenStable().then(() => {
|
||||||
expect(component.submitForm.emit).toHaveBeenCalledWith(expected2);
|
expect(component.submitForm.emit).toHaveBeenCalledWith(expected2);
|
||||||
|
@@ -47,6 +47,7 @@ import { NoContent } from '../../../core/shared/NoContent.model';
|
|||||||
import { Operation } from 'fast-json-patch';
|
import { Operation } from 'fast-json-patch';
|
||||||
import { ValidateGroupExists } from './validators/group-exists.validator';
|
import { ValidateGroupExists } from './validators/group-exists.validator';
|
||||||
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
|
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
|
||||||
|
import { environment } from '../../../../environments/environment';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-group-form',
|
selector: 'ds-group-form',
|
||||||
@@ -198,6 +199,7 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
label: groupDescription,
|
label: groupDescription,
|
||||||
name: 'groupDescription',
|
name: 'groupDescription',
|
||||||
required: false,
|
required: false,
|
||||||
|
spellCheck: environment.form.spellCheck,
|
||||||
});
|
});
|
||||||
this.formModel = [
|
this.formModel = [
|
||||||
this.groupName,
|
this.groupName,
|
||||||
@@ -348,8 +350,8 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
if (hasValue(this.groupDescription.value)) {
|
if (hasValue(this.groupDescription.value)) {
|
||||||
operations = [...operations, {
|
operations = [...operations, {
|
||||||
op: 'replace',
|
op: 'add',
|
||||||
path: '/metadata/dc.description/0/value',
|
path: '/metadata/dc.description',
|
||||||
value: this.groupDescription.value
|
value: this.groupDescription.value
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
@@ -1,9 +1,19 @@
|
|||||||
<ng-container>
|
<ng-container>
|
||||||
<h3 class="border-bottom pb-2">{{messagePrefix + '.head' | translate}}</h3>
|
<h3 class="border-bottom pb-2">{{messagePrefix + '.head' | translate}}</h3>
|
||||||
|
|
||||||
<h4 id="search" class="border-bottom pb-2">{{messagePrefix + '.search.head' | translate}}
|
<h4 id="search" class="border-bottom pb-2">
|
||||||
|
<span
|
||||||
|
*dsContextHelp="{
|
||||||
|
content: 'admin.access-control.groups.form.tooltip.editGroup.addEpeople',
|
||||||
|
id: 'edit-group-add-epeople',
|
||||||
|
iconPlacement: 'right',
|
||||||
|
tooltipPlacement: ['top', 'right', 'bottom']
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{messagePrefix + '.search.head' | translate}}
|
||||||
|
</span>
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
<form [formGroup]="searchForm" (ngSubmit)="search(searchForm.value)" class="d-flex justify-content-between">
|
<form [formGroup]="searchForm" (ngSubmit)="search(searchForm.value)" class="d-flex justify-content-between">
|
||||||
<div>
|
<div>
|
||||||
<select name="scope" id="scope" formControlName="scope" class="form-control" aria-label="Search scope">
|
<select name="scope" id="scope" formControlName="scope" class="form-control" aria-label="Search scope">
|
||||||
@@ -59,18 +69,20 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="align-middle">
|
<td class="align-middle">
|
||||||
<div class="btn-group edit-field">
|
<div class="btn-group edit-field">
|
||||||
<button *ngIf="(ePerson.memberOfGroup)"
|
<button *ngIf="ePerson.memberOfGroup"
|
||||||
(click)="deleteMemberFromGroup(ePerson)"
|
(click)="deleteMemberFromGroup(ePerson)"
|
||||||
class="btn btn-outline-danger btn-sm"
|
[disabled]="actionConfig.remove.disabled"
|
||||||
|
[ngClass]="['btn btn-sm', actionConfig.remove.css]"
|
||||||
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: { name: dsoNameService.getName(ePerson.eperson) } }}">
|
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: { name: dsoNameService.getName(ePerson.eperson) } }}">
|
||||||
<i class="fas fa-trash-alt fa-fw"></i>
|
<i [ngClass]="actionConfig.remove.icon"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button *ngIf="!(ePerson.memberOfGroup)"
|
<button *ngIf="!ePerson.memberOfGroup"
|
||||||
(click)="addMemberToGroup(ePerson)"
|
(click)="addMemberToGroup(ePerson)"
|
||||||
class="btn btn-outline-primary btn-sm"
|
[disabled]="actionConfig.add.disabled"
|
||||||
|
[ngClass]="['btn btn-sm', actionConfig.add.css]"
|
||||||
title="{{messagePrefix + '.table.edit.buttons.add' | translate: { name: dsoNameService.getName(ePerson.eperson) } }}">
|
title="{{messagePrefix + '.table.edit.buttons.add' | translate: { name: dsoNameService.getName(ePerson.eperson) } }}">
|
||||||
<i class="fas fa-plus fa-fw"></i>
|
<i [ngClass]="actionConfig.add.icon"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -121,10 +133,19 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="align-middle">
|
<td class="align-middle">
|
||||||
<div class="btn-group edit-field">
|
<div class="btn-group edit-field">
|
||||||
<button (click)="deleteMemberFromGroup(ePerson)"
|
<button *ngIf="ePerson.memberOfGroup"
|
||||||
class="btn btn-outline-danger btn-sm"
|
(click)="deleteMemberFromGroup(ePerson)"
|
||||||
|
[disabled]="actionConfig.remove.disabled"
|
||||||
|
[ngClass]="['btn btn-sm', actionConfig.remove.css]"
|
||||||
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: { name: dsoNameService.getName(ePerson.eperson) } }}">
|
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: { name: dsoNameService.getName(ePerson.eperson) } }}">
|
||||||
<i class="fas fa-trash-alt fa-fw"></i>
|
<i [ngClass]="actionConfig.remove.icon"></i>
|
||||||
|
</button>
|
||||||
|
<button *ngIf="!ePerson.memberOfGroup"
|
||||||
|
(click)="addMemberToGroup(ePerson)"
|
||||||
|
[disabled]="actionConfig.add.disabled"
|
||||||
|
[ngClass]="['btn btn-sm', actionConfig.add.css]"
|
||||||
|
title="{{messagePrefix + '.table.edit.buttons.add' | translate: { name: dsoNameService.getName(ePerson.eperson) } }}">
|
||||||
|
<i [ngClass]="actionConfig.add.icon"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
import { ComponentFixture, fakeAsync, flush, inject, TestBed, tick, waitForAsync } from '@angular/core/testing';
|
import { ComponentFixture, fakeAsync, flush, inject, TestBed, tick, waitForAsync } from '@angular/core/testing';
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { BrowserModule, By } from '@angular/platform-browser';
|
import { BrowserModule, By } from '@angular/platform-browser';
|
||||||
@@ -39,10 +39,10 @@ describe('MembersListComponent', () => {
|
|||||||
let ePersonDataServiceStub: any;
|
let ePersonDataServiceStub: any;
|
||||||
let groupsDataServiceStub: any;
|
let groupsDataServiceStub: any;
|
||||||
let activeGroup;
|
let activeGroup;
|
||||||
let allEPersons;
|
let allEPersons: EPerson[];
|
||||||
let allGroups;
|
let allGroups: Group[];
|
||||||
let epersonMembers;
|
let epersonMembers: EPerson[];
|
||||||
let subgroupMembers;
|
let subgroupMembers: Group[];
|
||||||
let paginationService;
|
let paginationService;
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
@@ -55,7 +55,7 @@ describe('MembersListComponent', () => {
|
|||||||
activeGroup: activeGroup,
|
activeGroup: activeGroup,
|
||||||
epersonMembers: epersonMembers,
|
epersonMembers: epersonMembers,
|
||||||
subgroupMembers: subgroupMembers,
|
subgroupMembers: subgroupMembers,
|
||||||
findListByHref(href: string): Observable<RemoteData<PaginatedList<EPerson>>> {
|
findListByHref(_href: string): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||||
return createSuccessfulRemoteDataObject$(buildPaginatedList<EPerson>(new PageInfo(), groupsDataServiceStub.getEPersonMembers()));
|
return createSuccessfulRemoteDataObject$(buildPaginatedList<EPerson>(new PageInfo(), groupsDataServiceStub.getEPersonMembers()));
|
||||||
},
|
},
|
||||||
searchByScope(scope: string, query: string): Observable<RemoteData<PaginatedList<EPerson>>> {
|
searchByScope(scope: string, query: string): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||||
@@ -150,8 +150,10 @@ describe('MembersListComponent', () => {
|
|||||||
});
|
});
|
||||||
afterEach(fakeAsync(() => {
|
afterEach(fakeAsync(() => {
|
||||||
fixture.destroy();
|
fixture.destroy();
|
||||||
|
fixture.debugElement.nativeElement.remove();
|
||||||
flush();
|
flush();
|
||||||
component = null;
|
component = null;
|
||||||
|
fixture.debugElement.nativeElement.remove();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should create MembersListComponent', inject([MembersListComponent], (comp: MembersListComponent) => {
|
it('should create MembersListComponent', inject([MembersListComponent], (comp: MembersListComponent) => {
|
||||||
@@ -170,12 +172,19 @@ describe('MembersListComponent', () => {
|
|||||||
|
|
||||||
describe('search', () => {
|
describe('search', () => {
|
||||||
describe('when searching without query', () => {
|
describe('when searching without query', () => {
|
||||||
let epersonsFound;
|
let epersonsFound: DebugElement[];
|
||||||
beforeEach(fakeAsync(() => {
|
beforeEach(fakeAsync(() => {
|
||||||
|
spyOn(component, 'isMemberOfGroup').and.callFake((ePerson: EPerson) => {
|
||||||
|
return observableOf(activeGroup.epersons.includes(ePerson));
|
||||||
|
});
|
||||||
component.search({ scope: 'metadata', query: '' });
|
component.search({ scope: 'metadata', query: '' });
|
||||||
tick();
|
tick();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
epersonsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr'));
|
epersonsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr'));
|
||||||
|
// Stop using the fake spy function (because otherwise the clicking on the buttons will not change anything
|
||||||
|
// because they don't change the value of activeGroup.epersons)
|
||||||
|
jasmine.getEnv().allowRespy(true);
|
||||||
|
spyOn(component, 'isMemberOfGroup').and.callThrough();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should display all epersons', () => {
|
it('should display all epersons', () => {
|
||||||
@@ -184,62 +193,56 @@ describe('MembersListComponent', () => {
|
|||||||
|
|
||||||
describe('if eperson is already a eperson', () => {
|
describe('if eperson is already a eperson', () => {
|
||||||
it('should have delete button, else it should have add button', () => {
|
it('should have delete button, else it should have add button', () => {
|
||||||
activeGroup.epersons.map((eperson: EPerson) => {
|
const memberIds: string[] = activeGroup.epersons.map((ePerson: EPerson) => ePerson.id);
|
||||||
epersonsFound.map((foundEPersonRowElement) => {
|
epersonsFound.map((foundEPersonRowElement: DebugElement) => {
|
||||||
if (foundEPersonRowElement.debugElement !== undefined) {
|
const epersonId: DebugElement = foundEPersonRowElement.query(By.css('td:first-child'));
|
||||||
const epersonId = foundEPersonRowElement.debugElement.query(By.css('td:first-child'));
|
const addButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-plus'));
|
||||||
const addButton = foundEPersonRowElement.debugElement.query(By.css('td:last-child .fa-plus'));
|
const deleteButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-trash-alt'));
|
||||||
const deleteButton = foundEPersonRowElement.debugElement.query(By.css('td:last-child .fa-trash-alt'));
|
if (memberIds.includes(epersonId.nativeElement.textContent)) {
|
||||||
if (epersonId.nativeElement.textContent === eperson.id) {
|
expect(addButton).toBeNull();
|
||||||
expect(addButton).toBeUndefined();
|
expect(deleteButton).not.toBeNull();
|
||||||
expect(deleteButton).toBeDefined();
|
|
||||||
} else {
|
} else {
|
||||||
expect(deleteButton).toBeUndefined();
|
expect(deleteButton).toBeNull();
|
||||||
expect(addButton).toBeDefined();
|
expect(addButton).not.toBeNull();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('if first add button is pressed', () => {
|
describe('if first add button is pressed', () => {
|
||||||
beforeEach(fakeAsync(() => {
|
beforeEach(fakeAsync(() => {
|
||||||
const addButton = fixture.debugElement.query(By.css('#epersonsSearch tbody .fa-plus'));
|
const addButton: DebugElement = fixture.debugElement.query(By.css('#epersonsSearch tbody .fa-plus'));
|
||||||
addButton.nativeElement.click();
|
addButton.nativeElement.click();
|
||||||
tick();
|
tick();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
}));
|
}));
|
||||||
it('all groups in search member of selected group', () => {
|
it('then all the ePersons are member of the active group', () => {
|
||||||
epersonsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr'));
|
epersonsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr'));
|
||||||
expect(epersonsFound.length).toEqual(2);
|
expect(epersonsFound.length).toEqual(2);
|
||||||
epersonsFound.map((foundEPersonRowElement) => {
|
epersonsFound.map((foundEPersonRowElement: DebugElement) => {
|
||||||
if (foundEPersonRowElement.debugElement !== undefined) {
|
const addButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-plus'));
|
||||||
const addButton = foundEPersonRowElement.debugElement.query(By.css('td:last-child .fa-plus'));
|
const deleteButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-trash-alt'));
|
||||||
const deleteButton = foundEPersonRowElement.debugElement.query(By.css('td:last-child .fa-trash-alt'));
|
expect(addButton).toBeNull();
|
||||||
expect(addButton).toBeUndefined();
|
expect(deleteButton).not.toBeNull();
|
||||||
expect(deleteButton).toBeDefined();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('if first delete button is pressed', () => {
|
describe('if first delete button is pressed', () => {
|
||||||
beforeEach(fakeAsync(() => {
|
beforeEach(fakeAsync(() => {
|
||||||
const addButton = fixture.debugElement.query(By.css('#epersonsSearch tbody .fa-trash-alt'));
|
const deleteButton: DebugElement = fixture.debugElement.query(By.css('#epersonsSearch tbody .fa-trash-alt'));
|
||||||
addButton.nativeElement.click();
|
deleteButton.nativeElement.click();
|
||||||
tick();
|
tick();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
}));
|
}));
|
||||||
it('first eperson in search delete button, because now member', () => {
|
it('then no ePerson is member of the active group', () => {
|
||||||
epersonsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr'));
|
epersonsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr'));
|
||||||
epersonsFound.map((foundEPersonRowElement) => {
|
expect(epersonsFound.length).toEqual(2);
|
||||||
if (foundEPersonRowElement.debugElement !== undefined) {
|
epersonsFound.map((foundEPersonRowElement: DebugElement) => {
|
||||||
const addButton = foundEPersonRowElement.debugElement.query(By.css('td:last-child .fa-plus'));
|
const addButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-plus'));
|
||||||
const deleteButton = foundEPersonRowElement.debugElement.query(By.css('td:last-child .fa-trash-alt'));
|
const deleteButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-trash-alt'));
|
||||||
expect(deleteButton).toBeUndefined();
|
expect(deleteButton).toBeNull();
|
||||||
expect(addButton).toBeDefined();
|
expect(addButton).not.toBeNull();
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -38,6 +38,35 @@ enum SubKey {
|
|||||||
SearchResultsDTO,
|
SearchResultsDTO,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The layout config of the buttons in the last column
|
||||||
|
*/
|
||||||
|
export interface EPersonActionConfig {
|
||||||
|
/**
|
||||||
|
* The css classes that should be added to the button
|
||||||
|
*/
|
||||||
|
css?: string;
|
||||||
|
/**
|
||||||
|
* Whether the button should be disabled
|
||||||
|
*/
|
||||||
|
disabled: boolean;
|
||||||
|
/**
|
||||||
|
* The Font Awesome icon that should be used
|
||||||
|
*/
|
||||||
|
icon: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link EPersonActionConfig} that should be used to display the button. The remove config will be used when the
|
||||||
|
* {@link EPerson} is already a member of the {@link Group} and the remove config will be used otherwise.
|
||||||
|
*
|
||||||
|
* *See {@link actionConfig} for an example*
|
||||||
|
*/
|
||||||
|
export interface EPersonListActionConfig {
|
||||||
|
add: EPersonActionConfig;
|
||||||
|
remove: EPersonActionConfig;
|
||||||
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-members-list',
|
selector: 'ds-members-list',
|
||||||
templateUrl: './members-list.component.html'
|
templateUrl: './members-list.component.html'
|
||||||
@@ -50,6 +79,20 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
|||||||
@Input()
|
@Input()
|
||||||
messagePrefix: string;
|
messagePrefix: string;
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
actionConfig: EPersonListActionConfig = {
|
||||||
|
add: {
|
||||||
|
css: 'btn-outline-primary',
|
||||||
|
disabled: false,
|
||||||
|
icon: 'fas fa-plus fa-fw',
|
||||||
|
},
|
||||||
|
remove: {
|
||||||
|
css: 'btn-outline-danger',
|
||||||
|
disabled: false,
|
||||||
|
icon: 'fas fa-trash-alt fa-fw'
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* EPeople being displayed in search result, initially all members, after search result of search
|
* EPeople being displayed in search result, initially all members, after search result of search
|
||||||
*/
|
*/
|
||||||
@@ -94,23 +137,21 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
|||||||
// current active group being edited
|
// current active group being edited
|
||||||
groupBeingEdited: Group;
|
groupBeingEdited: Group;
|
||||||
|
|
||||||
paginationSub: Subscription;
|
constructor(
|
||||||
|
protected groupDataService: GroupDataService,
|
||||||
|
|
||||||
constructor(private groupDataService: GroupDataService,
|
|
||||||
public ePersonDataService: EPersonDataService,
|
public ePersonDataService: EPersonDataService,
|
||||||
private translateService: TranslateService,
|
protected translateService: TranslateService,
|
||||||
private notificationsService: NotificationsService,
|
protected notificationsService: NotificationsService,
|
||||||
private formBuilder: FormBuilder,
|
protected formBuilder: FormBuilder,
|
||||||
private paginationService: PaginationService,
|
protected paginationService: PaginationService,
|
||||||
private router: Router,
|
protected router: Router,
|
||||||
public dsoNameService: DSONameService,
|
public dsoNameService: DSONameService,
|
||||||
) {
|
) {
|
||||||
this.currentSearchQuery = '';
|
this.currentSearchQuery = '';
|
||||||
this.currentSearchScope = 'metadata';
|
this.currentSearchScope = 'metadata';
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit(): void {
|
||||||
this.searchForm = this.formBuilder.group(({
|
this.searchForm = this.formBuilder.group(({
|
||||||
scope: 'metadata',
|
scope: 'metadata',
|
||||||
query: '',
|
query: '',
|
||||||
@@ -129,7 +170,7 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
|||||||
* @param page the number of the page to retrieve
|
* @param page the number of the page to retrieve
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private retrieveMembers(page: number) {
|
retrieveMembers(page: number): void {
|
||||||
this.unsubFrom(SubKey.MembersDTO);
|
this.unsubFrom(SubKey.MembersDTO);
|
||||||
this.subs.set(SubKey.MembersDTO,
|
this.subs.set(SubKey.MembersDTO,
|
||||||
this.paginationService.getCurrentPagination(this.config.id, this.config).pipe(
|
this.paginationService.getCurrentPagination(this.config.id, this.config).pipe(
|
||||||
@@ -169,7 +210,7 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether or not the given ePerson is a member of the group currently being edited
|
* Whether the given ePerson is a member of the group currently being edited
|
||||||
* @param possibleMember EPerson that is a possible member (being tested) of the group currently being edited
|
* @param possibleMember EPerson that is a possible member (being tested) of the group currently being edited
|
||||||
*/
|
*/
|
||||||
isMemberOfGroup(possibleMember: EPerson): Observable<boolean> {
|
isMemberOfGroup(possibleMember: EPerson): Observable<boolean> {
|
||||||
@@ -198,7 +239,7 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
|||||||
* @param key The key of the subscription to unsubscribe from
|
* @param key The key of the subscription to unsubscribe from
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private unsubFrom(key: SubKey) {
|
protected unsubFrom(key: SubKey) {
|
||||||
if (this.subs.has(key)) {
|
if (this.subs.has(key)) {
|
||||||
this.subs.get(key).unsubscribe();
|
this.subs.get(key).unsubscribe();
|
||||||
this.subs.delete(key);
|
this.subs.delete(key);
|
||||||
@@ -210,6 +251,7 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
|||||||
* @param ePerson EPerson we want to delete as member from group that is currently being edited
|
* @param ePerson EPerson we want to delete as member from group that is currently being edited
|
||||||
*/
|
*/
|
||||||
deleteMemberFromGroup(ePerson: EpersonDtoModel) {
|
deleteMemberFromGroup(ePerson: EpersonDtoModel) {
|
||||||
|
ePerson.memberOfGroup = false;
|
||||||
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => {
|
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => {
|
||||||
if (activeGroup != null) {
|
if (activeGroup != null) {
|
||||||
const response = this.groupDataService.deleteMemberFromGroup(activeGroup, ePerson.eperson);
|
const response = this.groupDataService.deleteMemberFromGroup(activeGroup, ePerson.eperson);
|
||||||
|
@@ -1,7 +1,16 @@
|
|||||||
<ng-container>
|
<ng-container>
|
||||||
<h3 class="border-bottom pb-2">{{messagePrefix + '.head' | translate}}</h3>
|
<h3 class="border-bottom pb-2">{{messagePrefix + '.head' | translate}}</h3>
|
||||||
|
|
||||||
<h4 id="search" class="border-bottom pb-2">{{messagePrefix + '.search.head' | translate}}
|
<h4 id="search" class="border-bottom pb-2">
|
||||||
|
<span *dsContextHelp="{
|
||||||
|
content: 'admin.access-control.groups.form.tooltip.editGroup.addSubgroups',
|
||||||
|
id: 'edit-group-add-subgroups',
|
||||||
|
iconPlacement: 'right',
|
||||||
|
tooltipPlacement: ['top', 'right', 'bottom']
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{messagePrefix + '.search.head' | translate}}
|
||||||
|
</span>
|
||||||
|
|
||||||
</h4>
|
</h4>
|
||||||
<form [formGroup]="searchForm" (ngSubmit)="search(searchForm.value)" class="d-flex justify-content-between">
|
<form [formGroup]="searchForm" (ngSubmit)="search(searchForm.value)" class="d-flex justify-content-between">
|
||||||
@@ -60,7 +69,7 @@
|
|||||||
<i class="fas fa-trash-alt fa-fw"></i>
|
<i class="fas fa-trash-alt fa-fw"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<p *ngIf="(isActiveGroup(group) | async)">{{ messagePrefix + '.table.edit.currentGroup' | translate }}</p>
|
<span *ngIf="(isActiveGroup(group) | async)">{{ messagePrefix + '.table.edit.currentGroup' | translate }}</span>
|
||||||
|
|
||||||
<button *ngIf="!(isSubgroupOfGroup(group) | async) && !(isActiveGroup(group) | async)"
|
<button *ngIf="!(isSubgroupOfGroup(group) | async) && !(isActiveGroup(group) | async)"
|
||||||
(click)="addSubgroupToGroup(group)"
|
(click)="addSubgroupToGroup(group)"
|
||||||
|
@@ -1,14 +1,6 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
import { NO_ERRORS_SCHEMA, DebugElement } from '@angular/core';
|
||||||
import {
|
import { ComponentFixture, fakeAsync, flush, inject, TestBed, tick, waitForAsync } from '@angular/core/testing';
|
||||||
ComponentFixture,
|
|
||||||
fakeAsync,
|
|
||||||
flush,
|
|
||||||
inject,
|
|
||||||
TestBed,
|
|
||||||
tick,
|
|
||||||
waitForAsync
|
|
||||||
} from '@angular/core/testing';
|
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { BrowserModule, By } from '@angular/platform-browser';
|
import { BrowserModule, By } from '@angular/platform-browser';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
@@ -48,8 +40,8 @@ describe('SubgroupsListComponent', () => {
|
|||||||
let ePersonDataServiceStub: any;
|
let ePersonDataServiceStub: any;
|
||||||
let groupsDataServiceStub: any;
|
let groupsDataServiceStub: any;
|
||||||
let activeGroup;
|
let activeGroup;
|
||||||
let subgroups;
|
let subgroups: Group[];
|
||||||
let allGroups;
|
let allGroups: Group[];
|
||||||
let routerStub;
|
let routerStub;
|
||||||
let paginationService;
|
let paginationService;
|
||||||
|
|
||||||
@@ -67,7 +59,7 @@ describe('SubgroupsListComponent', () => {
|
|||||||
getSubgroups(): Group {
|
getSubgroups(): Group {
|
||||||
return this.activeGroup;
|
return this.activeGroup;
|
||||||
},
|
},
|
||||||
findListByHref(href: string): Observable<RemoteData<PaginatedList<Group>>> {
|
findListByHref(_href: string): Observable<RemoteData<PaginatedList<Group>>> {
|
||||||
return this.subgroups$.pipe(
|
return this.subgroups$.pipe(
|
||||||
map((currentGroups: Group[]) => {
|
map((currentGroups: Group[]) => {
|
||||||
return createSuccessfulRemoteDataObject(buildPaginatedList<Group>(new PageInfo(), currentGroups));
|
return createSuccessfulRemoteDataObject(buildPaginatedList<Group>(new PageInfo(), currentGroups));
|
||||||
@@ -136,6 +128,7 @@ describe('SubgroupsListComponent', () => {
|
|||||||
});
|
});
|
||||||
afterEach(fakeAsync(() => {
|
afterEach(fakeAsync(() => {
|
||||||
fixture.destroy();
|
fixture.destroy();
|
||||||
|
fixture.debugElement.nativeElement.remove();
|
||||||
flush();
|
flush();
|
||||||
component = null;
|
component = null;
|
||||||
}));
|
}));
|
||||||
@@ -155,7 +148,7 @@ describe('SubgroupsListComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('if first group delete button is pressed', () => {
|
describe('if first group delete button is pressed', () => {
|
||||||
let groupsFound;
|
let groupsFound: DebugElement[];
|
||||||
beforeEach(fakeAsync(() => {
|
beforeEach(fakeAsync(() => {
|
||||||
const addButton = fixture.debugElement.query(By.css('#subgroupsOfGroup tbody .deleteButton'));
|
const addButton = fixture.debugElement.query(By.css('#subgroupsOfGroup tbody .deleteButton'));
|
||||||
addButton.triggerEventHandler('click', {
|
addButton.triggerEventHandler('click', {
|
||||||
@@ -173,7 +166,7 @@ describe('SubgroupsListComponent', () => {
|
|||||||
|
|
||||||
describe('search', () => {
|
describe('search', () => {
|
||||||
describe('when searching with empty query', () => {
|
describe('when searching with empty query', () => {
|
||||||
let groupsFound;
|
let groupsFound: DebugElement[];
|
||||||
beforeEach(fakeAsync(() => {
|
beforeEach(fakeAsync(() => {
|
||||||
component.search({ query: '' });
|
component.search({ query: '' });
|
||||||
groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr'));
|
groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr'));
|
||||||
@@ -184,9 +177,9 @@ describe('SubgroupsListComponent', () => {
|
|||||||
groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr'));
|
groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr'));
|
||||||
expect(groupsFound.length).toEqual(2);
|
expect(groupsFound.length).toEqual(2);
|
||||||
groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr'));
|
groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr'));
|
||||||
const groupIdsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr td:first-child'));
|
const groupIdsFound: DebugElement[] = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr td:first-child'));
|
||||||
allGroups.map((group: Group) => {
|
allGroups.map((group: Group) => {
|
||||||
expect(groupIdsFound.find((foundEl) => {
|
expect(groupIdsFound.find((foundEl: DebugElement) => {
|
||||||
return (foundEl.nativeElement.textContent.trim() === group.uuid);
|
return (foundEl.nativeElement.textContent.trim() === group.uuid);
|
||||||
})).toBeTruthy();
|
})).toBeTruthy();
|
||||||
});
|
});
|
||||||
@@ -198,30 +191,30 @@ describe('SubgroupsListComponent', () => {
|
|||||||
groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr'));
|
groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr'));
|
||||||
const getSubgroups = groupsDataServiceStub.getSubgroups().subgroups;
|
const getSubgroups = groupsDataServiceStub.getSubgroups().subgroups;
|
||||||
if (getSubgroups !== undefined && getSubgroups.length > 0) {
|
if (getSubgroups !== undefined && getSubgroups.length > 0) {
|
||||||
groupsFound.map((foundGroupRowElement) => {
|
groupsFound.map((foundGroupRowElement: DebugElement) => {
|
||||||
if (foundGroupRowElement.debugElement !== undefined) {
|
const groupId: DebugElement = foundGroupRowElement.query(By.css('td:first-child'));
|
||||||
const addButton = foundGroupRowElement.debugElement.query(By.css('td:last-child .fa-plus'));
|
const addButton: DebugElement = foundGroupRowElement.query(By.css('td:last-child .fa-plus'));
|
||||||
const deleteButton = foundGroupRowElement.debugElement.query(By.css('td:last-child .fa-trash-alt'));
|
const deleteButton: DebugElement = foundGroupRowElement.query(By.css('td:last-child .fa-trash-alt'));
|
||||||
expect(addButton).toBeUndefined();
|
expect(addButton).toBeNull();
|
||||||
expect(deleteButton).toBeDefined();
|
if (activeGroup.id === groupId.nativeElement.textContent) {
|
||||||
|
expect(deleteButton).toBeNull();
|
||||||
|
} else {
|
||||||
|
expect(deleteButton).not.toBeNull();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
getSubgroups.map((group: Group) => {
|
const subgroupIds: string[] = activeGroup.subgroups.map((group: Group) => group.id);
|
||||||
groupsFound.map((foundGroupRowElement) => {
|
groupsFound.map((foundGroupRowElement: DebugElement) => {
|
||||||
if (foundGroupRowElement.debugElement !== undefined) {
|
const groupId: DebugElement = foundGroupRowElement.query(By.css('td:first-child'));
|
||||||
const groupId = foundGroupRowElement.debugElement.query(By.css('td:first-child'));
|
const addButton: DebugElement = foundGroupRowElement.query(By.css('td:last-child .fa-plus'));
|
||||||
const addButton = foundGroupRowElement.debugElement.query(By.css('td:last-child .fa-plus'));
|
const deleteButton: DebugElement = foundGroupRowElement.query(By.css('td:last-child .fa-trash-alt'));
|
||||||
const deleteButton = foundGroupRowElement.debugElement.query(By.css('td:last-child .fa-trash-alt'));
|
if (subgroupIds.includes(groupId.nativeElement.textContent)) {
|
||||||
if (groupId.nativeElement.textContent === group.id) {
|
expect(addButton).toBeNull();
|
||||||
expect(addButton).toBeUndefined();
|
expect(deleteButton).not.toBeNull();
|
||||||
expect(deleteButton).toBeDefined();
|
|
||||||
} else {
|
} else {
|
||||||
expect(deleteButton).toBeUndefined();
|
expect(deleteButton).toBeNull();
|
||||||
expect(addButton).toBeDefined();
|
expect(addButton).not.toBeNull();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@@ -7,7 +7,7 @@
|
|||||||
<button class="mr-auto btn btn-success"
|
<button class="mr-auto btn btn-success"
|
||||||
[routerLink]="['newGroup']">
|
[routerLink]="['newGroup']">
|
||||||
<i class="fas fa-plus"></i>
|
<i class="fas fa-plus"></i>
|
||||||
<span class="d-none d-sm-inline">{{messagePrefix + 'button.add' | translate}}</span>
|
<span class="d-none d-sm-inline ml-1">{{messagePrefix + 'button.add' | translate}}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
<div class="flex-grow-1 mr-3">
|
<div class="flex-grow-1 mr-3">
|
||||||
<div class="form-group input-group">
|
<div class="form-group input-group">
|
||||||
<input type="text" name="query" id="query" formControlName="query"
|
<input type="text" name="query" id="query" formControlName="query"
|
||||||
class="form-control" attr.aria-label="{{messagePrefix + 'search.placeholder' | translate}}"
|
class="form-control" [attr.aria-label]="messagePrefix + 'search.placeholder' | translate"
|
||||||
[placeholder]="(messagePrefix + 'search.placeholder' | translate)" >
|
[placeholder]="(messagePrefix + 'search.placeholder' | translate)" >
|
||||||
<span class="input-group-append">
|
<span class="input-group-append">
|
||||||
<button type="submit" class="search-button btn btn-primary">
|
<button type="submit" class="search-button btn btn-primary">
|
||||||
|
@@ -1,12 +1,8 @@
|
|||||||
import { Location } from '@angular/common';
|
import { Location } from '@angular/common';
|
||||||
import { Component, OnInit } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
import { map, switchMap } from 'rxjs/operators';
|
|
||||||
import { AuthService } from '../../core/auth/auth.service';
|
|
||||||
import { METADATA_IMPORT_SCRIPT_NAME, ScriptDataService } from '../../core/data/processes/script-data.service';
|
import { METADATA_IMPORT_SCRIPT_NAME, ScriptDataService } from '../../core/data/processes/script-data.service';
|
||||||
import { EPerson } from '../../core/eperson/models/eperson.model';
|
|
||||||
import { ProcessParameter } from '../../process-page/processes/process-parameter.model';
|
import { ProcessParameter } from '../../process-page/processes/process-parameter.model';
|
||||||
import { isNotEmpty } from '../../shared/empty.util';
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
@@ -13,13 +13,14 @@
|
|||||||
[paginationOptions]="pageConfig"
|
[paginationOptions]="pageConfig"
|
||||||
[pageInfoState]="(bitstreamFormats | async)?.payload"
|
[pageInfoState]="(bitstreamFormats | async)?.payload"
|
||||||
[collectionSize]="(bitstreamFormats | async)?.payload?.totalElements"
|
[collectionSize]="(bitstreamFormats | async)?.payload?.totalElements"
|
||||||
[hideGear]="true"
|
[hideGear]="false"
|
||||||
[hidePagerWhenSinglePage]="true">
|
[hidePagerWhenSinglePage]="true">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table id="formats" class="table table-striped table-hover">
|
<table id="formats" class="table table-striped table-hover">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col"></th>
|
<th scope="col"></th>
|
||||||
|
<th scope="col">{{'admin.registries.bitstream-formats.table.id' | translate}}</th>
|
||||||
<th scope="col">{{'admin.registries.bitstream-formats.table.name' | translate}}</th>
|
<th scope="col">{{'admin.registries.bitstream-formats.table.name' | translate}}</th>
|
||||||
<th scope="col">{{'admin.registries.bitstream-formats.table.mimetype' | translate}}</th>
|
<th scope="col">{{'admin.registries.bitstream-formats.table.mimetype' | translate}}</th>
|
||||||
<th scope="col">{{'admin.registries.bitstream-formats.table.supportLevel.head' | translate}}</th>
|
<th scope="col">{{'admin.registries.bitstream-formats.table.supportLevel.head' | translate}}</th>
|
||||||
@@ -28,13 +29,14 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let bitstreamFormat of (bitstreamFormats | async)?.payload?.page">
|
<tr *ngFor="let bitstreamFormat of (bitstreamFormats | async)?.payload?.page">
|
||||||
<td>
|
<td>
|
||||||
<label>
|
<label class="mb-0">
|
||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
[checked]="isSelected(bitstreamFormat) | async"
|
[checked]="isSelected(bitstreamFormat) | async"
|
||||||
(change)="selectBitStreamFormat(bitstreamFormat, $event)"
|
(change)="selectBitStreamFormat(bitstreamFormat, $event)"
|
||||||
>
|
>
|
||||||
</label>
|
</label>
|
||||||
</td>
|
</td>
|
||||||
|
<td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{bitstreamFormat.id}}</a></td>
|
||||||
<td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{bitstreamFormat.shortDescription}}</a></td>
|
<td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{bitstreamFormat.shortDescription}}</a></td>
|
||||||
<td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{bitstreamFormat.mimetype}} <span *ngIf="bitstreamFormat.internal">({{'admin.registries.bitstream-formats.table.internal' | translate}})</span></a></td>
|
<td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{bitstreamFormat.mimetype}} <span *ngIf="bitstreamFormat.internal">({{'admin.registries.bitstream-formats.table.internal' | translate}})</span></a></td>
|
||||||
<td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{'admin.registries.bitstream-formats.table.supportLevel.'+bitstreamFormat.supportLevel | translate}}</a></td>
|
<td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{'admin.registries.bitstream-formats.table.supportLevel.'+bitstreamFormat.supportLevel | translate}}</a></td>
|
||||||
|
@@ -129,16 +129,19 @@ describe('BitstreamFormatsComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should contain the correct formats', () => {
|
it('should contain the correct formats', () => {
|
||||||
const unknownName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(1) td:nth-child(2)')).nativeElement;
|
const unknownName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(1) td:nth-child(3)')).nativeElement;
|
||||||
expect(unknownName.textContent).toBe('Unknown');
|
expect(unknownName.textContent).toBe('Unknown');
|
||||||
|
|
||||||
const licenseName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(2) td:nth-child(2)')).nativeElement;
|
const UUID: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(1) td:nth-child(2)')).nativeElement;
|
||||||
|
expect(UUID.textContent).toBe('test-uuid-1');
|
||||||
|
|
||||||
|
const licenseName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(2) td:nth-child(3)')).nativeElement;
|
||||||
expect(licenseName.textContent).toBe('License');
|
expect(licenseName.textContent).toBe('License');
|
||||||
|
|
||||||
const ccLicenseName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(3) td:nth-child(2)')).nativeElement;
|
const ccLicenseName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(3) td:nth-child(3)')).nativeElement;
|
||||||
expect(ccLicenseName.textContent).toBe('CC License');
|
expect(ccLicenseName.textContent).toBe('CC License');
|
||||||
|
|
||||||
const adobeName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(4) td:nth-child(2)')).nativeElement;
|
const adobeName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(4) td:nth-child(3)')).nativeElement;
|
||||||
expect(adobeName.textContent).toBe('Adobe PDF');
|
expect(adobeName.textContent).toBe('Adobe PDF');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,12 +1,11 @@
|
|||||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { combineLatest as observableCombineLatest, Observable, zip } from 'rxjs';
|
import { combineLatest as observableCombineLatest, Observable} from 'rxjs';
|
||||||
import { RemoteData } from '../../../core/data/remote-data';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
import { PaginatedList } from '../../../core/data/paginated-list.model';
|
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 { BitstreamFormat } from '../../../core/shared/bitstream-format.model';
|
import { BitstreamFormat } from '../../../core/shared/bitstream-format.model';
|
||||||
import { BitstreamFormatDataService } from '../../../core/data/bitstream-format-data.service';
|
import { BitstreamFormatDataService } from '../../../core/data/bitstream-format-data.service';
|
||||||
import { map, mergeMap, switchMap, take, toArray } from 'rxjs/operators';
|
import { map, mergeMap, switchMap, take, toArray } from 'rxjs/operators';
|
||||||
import { hasValue } from '../../../shared/empty.util';
|
|
||||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
@@ -29,21 +28,14 @@ export class BitstreamFormatsComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
bitstreamFormats: Observable<RemoteData<PaginatedList<BitstreamFormat>>>;
|
bitstreamFormats: Observable<RemoteData<PaginatedList<BitstreamFormat>>>;
|
||||||
|
|
||||||
/**
|
|
||||||
* The current pagination configuration for the page used by the FindAll method
|
|
||||||
* Currently simply renders all bitstream formats
|
|
||||||
*/
|
|
||||||
config: FindListOptions = Object.assign(new FindListOptions(), {
|
|
||||||
elementsPerPage: 20
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current pagination configuration for the page
|
* The current pagination configuration for the page
|
||||||
* Currently simply renders all bitstream formats
|
* Currently simply renders all bitstream formats
|
||||||
*/
|
*/
|
||||||
pageConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
pageConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
||||||
id: 'rbp',
|
id: 'rbp',
|
||||||
pageSize: 20
|
pageSize: 20,
|
||||||
|
pageSizeOptions: [20, 40, 60, 80, 100]
|
||||||
});
|
});
|
||||||
|
|
||||||
constructor(private notificationsService: NotificationsService,
|
constructor(private notificationsService: NotificationsService,
|
||||||
@@ -149,7 +141,7 @@ export class BitstreamFormatsComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
|
||||||
this.bitstreamFormats = this.paginationService.getFindListOptions(this.pageConfig.id, this.config).pipe(
|
this.bitstreamFormats = this.paginationService.getFindListOptions(this.pageConfig.id, this.pageConfig).pipe(
|
||||||
switchMap((findListOptions: FindListOptions) => {
|
switchMap((findListOptions: FindListOptions) => {
|
||||||
return this.bitstreamFormatService.findAll(findListOptions);
|
return this.bitstreamFormatService.findAll(findListOptions);
|
||||||
})
|
})
|
||||||
|
@@ -15,6 +15,7 @@ import { Router } from '@angular/router';
|
|||||||
import { hasValue, isEmpty } from '../../../../shared/empty.util';
|
import { hasValue, isEmpty } from '../../../../shared/empty.util';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { getBitstreamFormatsModuleRoute } from '../../admin-registries-routing-paths';
|
import { getBitstreamFormatsModuleRoute } from '../../admin-registries-routing-paths';
|
||||||
|
import { environment } from '../../../../../environments/environment';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The component responsible for rendering the form to create/edit a bitstream format
|
* The component responsible for rendering the form to create/edit a bitstream format
|
||||||
@@ -90,6 +91,7 @@ export class FormatFormComponent implements OnInit {
|
|||||||
name: 'description',
|
name: 'description',
|
||||||
label: 'admin.registries.bitstream-formats.edit.description.label',
|
label: 'admin.registries.bitstream-formats.edit.description.label',
|
||||||
hint: 'admin.registries.bitstream-formats.edit.description.hint',
|
hint: 'admin.registries.bitstream-formats.edit.description.hint',
|
||||||
|
spellCheck: environment.form.spellCheck,
|
||||||
|
|
||||||
}),
|
}),
|
||||||
new DynamicSelectModel({
|
new DynamicSelectModel({
|
||||||
|
@@ -29,7 +29,7 @@
|
|||||||
<tr *ngFor="let schema of (metadataSchemas | async)?.payload?.page"
|
<tr *ngFor="let schema of (metadataSchemas | async)?.payload?.page"
|
||||||
[ngClass]="{'table-primary' : isActive(schema) | async}">
|
[ngClass]="{'table-primary' : isActive(schema) | async}">
|
||||||
<td>
|
<td>
|
||||||
<label>
|
<label class="mb-0">
|
||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
[checked]="isSelected(schema) | async"
|
[checked]="isSelected(schema) | async"
|
||||||
(change)="selectMetadataSchema(schema, $event)"
|
(change)="selectMetadataSchema(schema, $event)"
|
||||||
|
@@ -1,3 +1,7 @@
|
|||||||
.selectable-row:hover {
|
.selectable-row:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep #metadatadataschemagroup {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
@@ -19,10 +19,7 @@ import { RestResponse } from '../../../core/cache/response.models';
|
|||||||
import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
|
import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||||
import { PaginationService } from '../../../core/pagination/pagination.service';
|
import { PaginationService } from '../../../core/pagination/pagination.service';
|
||||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
|
||||||
import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model';
|
|
||||||
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
|
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
|
||||||
import { FindListOptions } from '../../../core/data/find-list-options.model';
|
|
||||||
|
|
||||||
describe('MetadataRegistryComponent', () => {
|
describe('MetadataRegistryComponent', () => {
|
||||||
let comp: MetadataRegistryComponent;
|
let comp: MetadataRegistryComponent;
|
||||||
|
@@ -25,6 +25,7 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th></th>
|
<th></th>
|
||||||
|
<th scope="col">{{'admin.registries.schema.fields.table.id' | translate}}</th>
|
||||||
<th scope="col">{{'admin.registries.schema.fields.table.field' | translate}}</th>
|
<th scope="col">{{'admin.registries.schema.fields.table.field' | translate}}</th>
|
||||||
<th scope="col">{{'admin.registries.schema.fields.table.scopenote' | translate}}</th>
|
<th scope="col">{{'admin.registries.schema.fields.table.scopenote' | translate}}</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -33,13 +34,14 @@
|
|||||||
<tr *ngFor="let field of fields?.page"
|
<tr *ngFor="let field of fields?.page"
|
||||||
[ngClass]="{'table-primary' : isActive(field) | async}">
|
[ngClass]="{'table-primary' : isActive(field) | async}">
|
||||||
<td>
|
<td>
|
||||||
<label>
|
<label class="mb-0">
|
||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
[checked]="isSelected(field) | async"
|
[checked]="isSelected(field) | async"
|
||||||
(change)="selectMetadataField(field, $event)">
|
(change)="selectMetadataField(field, $event)">
|
||||||
</label>
|
</label>
|
||||||
</td>
|
</td>
|
||||||
<td class="selectable-row" (click)="editField(field)">{{schema?.prefix}}.{{field.element}}<label *ngIf="field.qualifier">.</label>{{field.qualifier}}</td>
|
<td class="selectable-row" (click)="editField(field)">{{field.id}}</td>
|
||||||
|
<td class="selectable-row" (click)="editField(field)">{{schema?.prefix}}.{{field.element}}<label *ngIf="field.qualifier" class="mb-0">.</label>{{field.qualifier}}</td>
|
||||||
<td class="selectable-row" (click)="editField(field)">{{field.scopeNote}}</td>
|
<td class="selectable-row" (click)="editField(field)">{{field.scopeNote}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@@ -1,3 +1,8 @@
|
|||||||
.selectable-row:hover {
|
.selectable-row:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:host ::ng-deep #metadatadatafieldgroup {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
@@ -23,11 +23,8 @@ import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
|
|||||||
import { MetadataField } from '../../../core/metadata/metadata-field.model';
|
import { MetadataField } from '../../../core/metadata/metadata-field.model';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||||
import { VarDirective } from '../../../shared/utils/var.directive';
|
import { VarDirective } from '../../../shared/utils/var.directive';
|
||||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
|
||||||
import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model';
|
|
||||||
import { PaginationService } from '../../../core/pagination/pagination.service';
|
import { PaginationService } from '../../../core/pagination/pagination.service';
|
||||||
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
|
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
|
||||||
import { FindListOptions } from '../../../core/data/find-list-options.model';
|
|
||||||
|
|
||||||
describe('MetadataSchemaComponent', () => {
|
describe('MetadataSchemaComponent', () => {
|
||||||
let comp: MetadataSchemaComponent;
|
let comp: MetadataSchemaComponent;
|
||||||
@@ -169,10 +166,10 @@ describe('MetadataSchemaComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should contain the correct fields', () => {
|
it('should contain the correct fields', () => {
|
||||||
const editorField: HTMLElement = fixture.debugElement.query(By.css('#metadata-fields tr:nth-child(1) td:nth-child(2)')).nativeElement;
|
const editorField: HTMLElement = fixture.debugElement.query(By.css('#metadata-fields tr:nth-child(1) td:nth-child(3)')).nativeElement;
|
||||||
expect(editorField.textContent).toBe('mock.contributor.editor');
|
expect(editorField.textContent).toBe('mock.contributor.editor');
|
||||||
|
|
||||||
const illustratorField: HTMLElement = fixture.debugElement.query(By.css('#metadata-fields tr:nth-child(2) td:nth-child(2)')).nativeElement;
|
const illustratorField: HTMLElement = fixture.debugElement.query(By.css('#metadata-fields tr:nth-child(2) td:nth-child(3)')).nativeElement;
|
||||||
expect(illustratorField.textContent).toBe('mock.contributor.illustrator');
|
expect(illustratorField.textContent).toBe('mock.contributor.illustrator');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -47,6 +47,12 @@ import { BatchImportPageComponent } from './admin-import-batch-page/batch-import
|
|||||||
component: BatchImportPageComponent,
|
component: BatchImportPageComponent,
|
||||||
data: { title: 'admin.batch-import.title', breadcrumbKey: 'admin.batch-import' }
|
data: { title: 'admin.batch-import.title', breadcrumbKey: 'admin.batch-import' }
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'system-wide-alert',
|
||||||
|
resolve: { breadcrumb: I18nBreadcrumbResolver },
|
||||||
|
loadChildren: () => import('../system-wide-alert/system-wide-alert.module').then((m) => m.SystemWideAlertModule),
|
||||||
|
data: {title: 'admin.system-wide-alert.title', breadcrumbKey: 'admin.system-wide-alert'}
|
||||||
|
},
|
||||||
])
|
])
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
<div class="sidebar-section">
|
<div class="sidebar-section">
|
||||||
<a class="nav-item nav-link d-flex flex-row flex-nowrap"
|
<a class="nav-item nav-link d-flex flex-row flex-nowrap"
|
||||||
[ngClass]="{ disabled: !hasLink }"
|
[ngClass]="{ disabled: isDisabled }"
|
||||||
[attr.aria-disabled]="!hasLink"
|
[attr.aria-disabled]="isDisabled"
|
||||||
[attr.aria-labelledby]="'sidebarName-' + section.id"
|
[attr.aria-labelledby]="'sidebarName-' + section.id"
|
||||||
[title]="('menu.section.icon.' + section.id) | translate"
|
[title]="('menu.section.icon.' + section.id) | translate"
|
||||||
[routerLink]="itemModel.link"
|
[routerLink]="itemModel.link"
|
||||||
|
@@ -2,7 +2,7 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
|||||||
|
|
||||||
import { MenuService } from '../../../shared/menu/menu.service';
|
import { MenuService } from '../../../shared/menu/menu.service';
|
||||||
import { MenuServiceStub } from '../../../shared/testing/menu-service.stub';
|
import { MenuServiceStub } from '../../../shared/testing/menu-service.stub';
|
||||||
import { CSSVariableService } from '../../../shared/sass-helper/sass-helper.service';
|
import { CSSVariableService } from '../../../shared/sass-helper/css-variable.service';
|
||||||
import { CSSVariableServiceStub } from '../../../shared/testing/css-variable-service.stub';
|
import { CSSVariableServiceStub } from '../../../shared/testing/css-variable-service.stub';
|
||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
@@ -17,6 +17,8 @@ describe('AdminSidebarSectionComponent', () => {
|
|||||||
const menuService = new MenuServiceStub();
|
const menuService = new MenuServiceStub();
|
||||||
const iconString = 'test';
|
const iconString = 'test';
|
||||||
|
|
||||||
|
describe('when not disabled', () => {
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [NoopAnimationsModule, RouterTestingModule, TranslateModule.forRoot()],
|
imports: [NoopAnimationsModule, RouterTestingModule, TranslateModule.forRoot()],
|
||||||
@@ -49,6 +51,52 @@ describe('AdminSidebarSectionComponent', () => {
|
|||||||
const icon = fixture.debugElement.query(By.css('.shortcut-icon')).query(By.css('i.fas'));
|
const icon = fixture.debugElement.query(By.css('.shortcut-icon')).query(By.css('i.fas'));
|
||||||
expect(icon.nativeElement.getAttribute('class')).toContain('fa-' + iconString);
|
expect(icon.nativeElement.getAttribute('class')).toContain('fa-' + iconString);
|
||||||
});
|
});
|
||||||
|
it('should not contain the disabled class', () => {
|
||||||
|
const disabled = fixture.debugElement.query(By.css('.disabled'));
|
||||||
|
expect(disabled).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
describe('when disabled', () => {
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [NoopAnimationsModule, RouterTestingModule, TranslateModule.forRoot()],
|
||||||
|
declarations: [AdminSidebarSectionComponent, TestComponent],
|
||||||
|
providers: [
|
||||||
|
{provide: 'sectionDataProvider', useValue: {model: {link: 'google.com', disabled: true}, icon: iconString}},
|
||||||
|
{provide: MenuService, useValue: menuService},
|
||||||
|
{provide: CSSVariableService, useClass: CSSVariableServiceStub},
|
||||||
|
]
|
||||||
|
}).overrideComponent(AdminSidebarSectionComponent, {
|
||||||
|
set: {
|
||||||
|
entryComponents: [TestComponent]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(AdminSidebarSectionComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
spyOn(component as any, 'getMenuItemComponent').and.returnValue(TestComponent);
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set the right icon', () => {
|
||||||
|
const icon = fixture.debugElement.query(By.css('.shortcut-icon')).query(By.css('i.fas'));
|
||||||
|
expect(icon.nativeElement.getAttribute('class')).toContain('fa-' + iconString);
|
||||||
|
});
|
||||||
|
it('should contain the disabled class', () => {
|
||||||
|
const disabled = fixture.debugElement.query(By.css('.disabled'));
|
||||||
|
expect(disabled).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// declare a test component
|
// declare a test component
|
||||||
|
@@ -5,7 +5,7 @@ import { rendersSectionForMenu } from '../../../shared/menu/menu-section.decorat
|
|||||||
import { LinkMenuItemModel } from '../../../shared/menu/menu-item/models/link.model';
|
import { LinkMenuItemModel } from '../../../shared/menu/menu-item/models/link.model';
|
||||||
import { MenuSection } from '../../../shared/menu/menu-section.model';
|
import { MenuSection } from '../../../shared/menu/menu-section.model';
|
||||||
import { MenuID } from '../../../shared/menu/menu-id.model';
|
import { MenuID } from '../../../shared/menu/menu-id.model';
|
||||||
import { isNotEmpty } from '../../../shared/empty.util';
|
import { isEmpty } from '../../../shared/empty.util';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -26,7 +26,12 @@ export class AdminSidebarSectionComponent extends MenuSectionComponent implement
|
|||||||
*/
|
*/
|
||||||
menuID: MenuID = MenuID.ADMIN;
|
menuID: MenuID = MenuID.ADMIN;
|
||||||
itemModel;
|
itemModel;
|
||||||
hasLink: boolean;
|
|
||||||
|
/**
|
||||||
|
* Boolean to indicate whether this section is disabled
|
||||||
|
*/
|
||||||
|
isDisabled: boolean;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject('sectionDataProvider') menuSection: MenuSection,
|
@Inject('sectionDataProvider') menuSection: MenuSection,
|
||||||
protected menuService: MenuService,
|
protected menuService: MenuService,
|
||||||
@@ -38,13 +43,13 @@ export class AdminSidebarSectionComponent extends MenuSectionComponent implement
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.hasLink = isNotEmpty(this.itemModel?.link);
|
this.isDisabled = this.itemModel?.disabled || isEmpty(this.itemModel?.link);
|
||||||
super.ngOnInit();
|
super.ngOnInit();
|
||||||
}
|
}
|
||||||
|
|
||||||
navigate(event: any): void {
|
navigate(event: any): void {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (this.hasLink) {
|
if (!this.isDisabled) {
|
||||||
this.router.navigate(this.itemModel.link);
|
this.router.navigate(this.itemModel.link);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -6,7 +6,7 @@ import { ScriptDataService } from '../../core/data/processes/script-data.service
|
|||||||
import { AdminSidebarComponent } from './admin-sidebar.component';
|
import { AdminSidebarComponent } from './admin-sidebar.component';
|
||||||
import { MenuService } from '../../shared/menu/menu.service';
|
import { MenuService } from '../../shared/menu/menu.service';
|
||||||
import { MenuServiceStub } from '../../shared/testing/menu-service.stub';
|
import { MenuServiceStub } from '../../shared/testing/menu-service.stub';
|
||||||
import { CSSVariableService } from '../../shared/sass-helper/sass-helper.service';
|
import { CSSVariableService } from '../../shared/sass-helper/css-variable.service';
|
||||||
import { CSSVariableServiceStub } from '../../shared/testing/css-variable-service.stub';
|
import { CSSVariableServiceStub } from '../../shared/testing/css-variable-service.stub';
|
||||||
import { AuthServiceStub } from '../../shared/testing/auth-service.stub';
|
import { AuthServiceStub } from '../../shared/testing/auth-service.stub';
|
||||||
import { AuthService } from '../../core/auth/auth.service';
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
@@ -16,7 +16,6 @@ import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
|||||||
import { RouterTestingModule } from '@angular/router/testing';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||||
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
|
||||||
import createSpy = jasmine.createSpy;
|
import createSpy = jasmine.createSpy;
|
||||||
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
|
||||||
import { Item } from '../../core/shared/item.model';
|
import { Item } from '../../core/shared/item.model';
|
||||||
|
@@ -5,7 +5,7 @@ import { AuthService } from '../../core/auth/auth.service';
|
|||||||
import { slideSidebar } from '../../shared/animations/slide';
|
import { slideSidebar } from '../../shared/animations/slide';
|
||||||
import { MenuComponent } from '../../shared/menu/menu.component';
|
import { MenuComponent } from '../../shared/menu/menu.component';
|
||||||
import { MenuService } from '../../shared/menu/menu.service';
|
import { MenuService } from '../../shared/menu/menu.service';
|
||||||
import { CSSVariableService } from '../../shared/sass-helper/sass-helper.service';
|
import { CSSVariableService } from '../../shared/sass-helper/css-variable.service';
|
||||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||||
import { MenuID } from '../../shared/menu/menu-id.model';
|
import { MenuID } from '../../shared/menu/menu-id.model';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
@@ -69,7 +69,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
super.ngOnInit();
|
super.ngOnInit();
|
||||||
this.sidebarWidth = this.variableService.getVariable('sidebarItemsWidth');
|
this.sidebarWidth = this.variableService.getVariable('--ds-sidebar-items-width');
|
||||||
this.authService.isAuthenticated()
|
this.authService.isAuthenticated()
|
||||||
.subscribe((loggedIn: boolean) => {
|
.subscribe((loggedIn: boolean) => {
|
||||||
if (loggedIn) {
|
if (loggedIn) {
|
||||||
@@ -100,6 +100,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
this.menuVisible = this.menuService.isMenuVisibleWithVisibleSections(this.menuID);
|
||||||
}
|
}
|
||||||
|
|
||||||
@HostListener('focusin')
|
@HostListener('focusin')
|
||||||
|
@@ -7,6 +7,7 @@
|
|||||||
[attr.aria-labelledby]="'sidebarName-' + section.id"
|
[attr.aria-labelledby]="'sidebarName-' + section.id"
|
||||||
[attr.aria-expanded]="expanded | async"
|
[attr.aria-expanded]="expanded | async"
|
||||||
[title]="('menu.section.icon.' + section.id) | translate"
|
[title]="('menu.section.icon.' + section.id) | translate"
|
||||||
|
[class.disabled]="section.model?.disabled"
|
||||||
(click)="toggleSection($event)"
|
(click)="toggleSection($event)"
|
||||||
(keyup.space)="toggleSection($event)"
|
(keyup.space)="toggleSection($event)"
|
||||||
(keyup.enter)="toggleSection($event)"
|
(keyup.enter)="toggleSection($event)"
|
||||||
|
@@ -3,7 +3,7 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
|||||||
import { ExpandableAdminSidebarSectionComponent } from './expandable-admin-sidebar-section.component';
|
import { ExpandableAdminSidebarSectionComponent } from './expandable-admin-sidebar-section.component';
|
||||||
import { MenuService } from '../../../shared/menu/menu.service';
|
import { MenuService } from '../../../shared/menu/menu.service';
|
||||||
import { MenuServiceStub } from '../../../shared/testing/menu-service.stub';
|
import { MenuServiceStub } from '../../../shared/testing/menu-service.stub';
|
||||||
import { CSSVariableService } from '../../../shared/sass-helper/sass-helper.service';
|
import { CSSVariableService } from '../../../shared/sass-helper/css-variable.service';
|
||||||
import { CSSVariableServiceStub } from '../../../shared/testing/css-variable-service.stub';
|
import { CSSVariableServiceStub } from '../../../shared/testing/css-variable-service.stub';
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
@@ -23,7 +23,7 @@ describe('ExpandableAdminSidebarSectionComponent', () => {
|
|||||||
imports: [NoopAnimationsModule, TranslateModule.forRoot()],
|
imports: [NoopAnimationsModule, TranslateModule.forRoot()],
|
||||||
declarations: [ExpandableAdminSidebarSectionComponent, TestComponent],
|
declarations: [ExpandableAdminSidebarSectionComponent, TestComponent],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: 'sectionDataProvider', useValue: { icon: iconString } },
|
{ provide: 'sectionDataProvider', useValue: { icon: iconString, model: {} } },
|
||||||
{ provide: MenuService, useValue: menuService },
|
{ provide: MenuService, useValue: menuService },
|
||||||
{ provide: CSSVariableService, useClass: CSSVariableServiceStub },
|
{ provide: CSSVariableService, useClass: CSSVariableServiceStub },
|
||||||
{ provide: Router, useValue: new RouterStub() },
|
{ provide: Router, useValue: new RouterStub() },
|
||||||
|
@@ -2,7 +2,7 @@ import { Component, Inject, Injector, OnInit } from '@angular/core';
|
|||||||
import { rotate } from '../../../shared/animations/rotate';
|
import { rotate } from '../../../shared/animations/rotate';
|
||||||
import { AdminSidebarSectionComponent } from '../admin-sidebar-section/admin-sidebar-section.component';
|
import { AdminSidebarSectionComponent } from '../admin-sidebar-section/admin-sidebar-section.component';
|
||||||
import { slide } from '../../../shared/animations/slide';
|
import { slide } from '../../../shared/animations/slide';
|
||||||
import { CSSVariableService } from '../../../shared/sass-helper/sass-helper.service';
|
import { CSSVariableService } from '../../../shared/sass-helper/css-variable.service';
|
||||||
import { bgColor } from '../../../shared/animations/bgColor';
|
import { bgColor } from '../../../shared/animations/bgColor';
|
||||||
import { MenuService } from '../../../shared/menu/menu.service';
|
import { MenuService } from '../../../shared/menu/menu.service';
|
||||||
import { combineLatest as combineLatestObservable, Observable } from 'rxjs';
|
import { combineLatest as combineLatestObservable, Observable } from 'rxjs';
|
||||||
@@ -65,7 +65,7 @@ export class ExpandableAdminSidebarSectionComponent extends AdminSidebarSectionC
|
|||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
super.ngOnInit();
|
super.ngOnInit();
|
||||||
this.sidebarActiveBg = this.variableService.getVariable('adminSidebarActiveBg');
|
this.sidebarActiveBg = this.variableService.getVariable('--ds-admin-sidebar-active-bg');
|
||||||
this.sidebarCollapsed = this.menuService.isMenuCollapsed(this.menuID);
|
this.sidebarCollapsed = this.menuService.isMenuCollapsed(this.menuID);
|
||||||
this.sidebarPreviewCollapsed = this.menuService.isMenuPreviewCollapsed(this.menuID);
|
this.sidebarPreviewCollapsed = this.menuService.isMenuPreviewCollapsed(this.menuID);
|
||||||
this.expanded = combineLatestObservable(this.active, this.sidebarCollapsed, this.sidebarPreviewCollapsed)
|
this.expanded = combineLatestObservable(this.active, this.sidebarCollapsed, this.sidebarPreviewCollapsed)
|
||||||
|
@@ -1 +1 @@
|
|||||||
<ds-configuration-search-page configuration="workflowAdmin" [context]="context"></ds-configuration-search-page>
|
<ds-configuration-search-page configuration="supervision" [context]="context"></ds-configuration-search-page>
|
||||||
|
@@ -4,24 +4,32 @@ import { NO_ERRORS_SCHEMA } from '@angular/core';
|
|||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { RouterTestingModule } from '@angular/router/testing';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
import { URLCombiner } from '../../../core/url-combiner/url-combiner';
|
import { URLCombiner } from '../../../../../core/url-combiner/url-combiner';
|
||||||
import { WorkflowItemAdminWorkflowActionsComponent } from './workflow-item-admin-workflow-actions.component';
|
import { WorkflowItemAdminWorkflowActionsComponent } from './workflow-item-admin-workflow-actions.component';
|
||||||
import { WorkflowItem } from '../../../core/submission/models/workflowitem.model';
|
import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model';
|
||||||
import {
|
import {
|
||||||
getWorkflowItemDeleteRoute,
|
getWorkflowItemDeleteRoute,
|
||||||
getWorkflowItemSendBackRoute
|
getWorkflowItemSendBackRoute
|
||||||
} from '../../../workflowitems-edit-page/workflowitems-edit-page-routing-paths';
|
} from '../../../../../workflowitems-edit-page/workflowitems-edit-page-routing-paths';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
import { Item } from '../../../../../core/shared/item.model';
|
||||||
|
import { RemoteData } from '../../../../../core/data/remote-data';
|
||||||
|
import { RequestEntryState } from '../../../../../core/data/request-entry-state.model';
|
||||||
|
|
||||||
describe('WorkflowItemAdminWorkflowActionsComponent', () => {
|
describe('WorkflowItemAdminWorkflowActionsComponent', () => {
|
||||||
let component: WorkflowItemAdminWorkflowActionsComponent;
|
let component: WorkflowItemAdminWorkflowActionsComponent;
|
||||||
let fixture: ComponentFixture<WorkflowItemAdminWorkflowActionsComponent>;
|
let fixture: ComponentFixture<WorkflowItemAdminWorkflowActionsComponent>;
|
||||||
let id;
|
let id;
|
||||||
let wfi;
|
let wfi;
|
||||||
|
let item = new Item();
|
||||||
|
item.uuid = 'itemUUID1111';
|
||||||
|
const rd = new RemoteData(undefined, undefined, undefined, RequestEntryState.Success, undefined, item, 200);
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
id = '780b2588-bda5-4112-a1cd-0b15000a5339';
|
id = '780b2588-bda5-4112-a1cd-0b15000a5339';
|
||||||
wfi = new WorkflowItem();
|
wfi = new WorkflowItem();
|
||||||
wfi.id = id;
|
wfi.id = id;
|
||||||
|
wfi.item = of(rd);
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
@@ -59,4 +67,5 @@ describe('WorkflowItemAdminWorkflowActionsComponent', () => {
|
|||||||
const link = a.nativeElement.href;
|
const link = a.nativeElement.href;
|
||||||
expect(link).toContain(new URLCombiner(getWorkflowItemSendBackRoute(wfi.id)).toString());
|
expect(link).toContain(new URLCombiner(getWorkflowItemSendBackRoute(wfi.id)).toString());
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
@@ -1,9 +1,10 @@
|
|||||||
import { Component, Input } from '@angular/core';
|
import { Component, Input } from '@angular/core';
|
||||||
import { WorkflowItem } from '../../../core/submission/models/workflowitem.model';
|
|
||||||
|
import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model';
|
||||||
import {
|
import {
|
||||||
getWorkflowItemSendBackRoute,
|
getWorkflowItemDeleteRoute,
|
||||||
getWorkflowItemDeleteRoute
|
getWorkflowItemSendBackRoute
|
||||||
} from '../../../workflowitems-edit-page/workflowitems-edit-page-routing-paths';
|
} from '../../../../../workflowitems-edit-page/workflowitems-edit-page-routing-paths';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-workflow-item-admin-workflow-actions-element',
|
selector: 'ds-workflow-item-admin-workflow-actions-element',
|
||||||
@@ -11,7 +12,7 @@ import {
|
|||||||
templateUrl: './workflow-item-admin-workflow-actions.component.html'
|
templateUrl: './workflow-item-admin-workflow-actions.component.html'
|
||||||
})
|
})
|
||||||
/**
|
/**
|
||||||
* The component for displaying the actions for a list element for an item on the admin workflow search page
|
* The component for displaying the actions for a list element for a workflow-item on the admin workflow search page
|
||||||
*/
|
*/
|
||||||
export class WorkflowItemAdminWorkflowActionsComponent {
|
export class WorkflowItemAdminWorkflowActionsComponent {
|
||||||
|
|
||||||
@@ -21,7 +22,7 @@ export class WorkflowItemAdminWorkflowActionsComponent {
|
|||||||
@Input() public wfi: WorkflowItem;
|
@Input() public wfi: WorkflowItem;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether or not to use small buttons
|
* Whether to use small buttons or not
|
||||||
*/
|
*/
|
||||||
@Input() public small: boolean;
|
@Input() public small: boolean;
|
||||||
|
|
||||||
@@ -29,7 +30,6 @@ export class WorkflowItemAdminWorkflowActionsComponent {
|
|||||||
* Returns the path to the delete page of this workflow item
|
* Returns the path to the delete page of this workflow item
|
||||||
*/
|
*/
|
||||||
getDeleteRoute(): string {
|
getDeleteRoute(): string {
|
||||||
|
|
||||||
return getWorkflowItemDeleteRoute(this.wfi.id);
|
return getWorkflowItemDeleteRoute(this.wfi.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,4 +39,5 @@ export class WorkflowItemAdminWorkflowActionsComponent {
|
|||||||
getSendBackRoute(): string {
|
getSendBackRoute(): string {
|
||||||
return getWorkflowItemSendBackRoute(this.wfi.id);
|
return getWorkflowItemSendBackRoute(this.wfi.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@@ -0,0 +1,44 @@
|
|||||||
|
<div>
|
||||||
|
<div class="modal-header">{{'supervision-group-selector.header' | translate}}
|
||||||
|
<button type="button" class="close" (click)="close()" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="control-group col-sm-12">
|
||||||
|
<label for="supervisionOrder">{{'supervision-group-selector.select.type-of-order.label' | translate}}</label>
|
||||||
|
<select name="supervisionOrder" id="supervisionOrder" class="form-control"
|
||||||
|
[(ngModel)]="selectedOrderType"
|
||||||
|
attr.aria-label="{{'supervision-group-selector.select.type-of-order.label' | translate}}">
|
||||||
|
<option value="EDITOR">{{'supervision-group-selector.select.type-of-order.option.editor' | translate}}</option>
|
||||||
|
<option value="OBSERVER">{{'supervision-group-selector.select.type-of-order.option.observer' | translate}}</option>
|
||||||
|
</select>
|
||||||
|
<ds-error *ngIf="isSubmitted && (!selectedOrderType || selectedOrderType === '')"
|
||||||
|
message="{{'supervision-group-selector.select.type-of-order.error' | translate}}"></ds-error>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="control-group col-sm-12">
|
||||||
|
<label for="supervisionGroup">{{'supervision-group-selector.select.group.label' | translate}}</label>
|
||||||
|
<ng-container class="mb-3">
|
||||||
|
<input id="supervisionGroup" class="form-control" type="text" [value]="selectedGroup ? dsoNameService.getName(selectedGroup) : ''" disabled>
|
||||||
|
<ds-error *ngIf="isSubmitted && !selectedGroup" message="{{'supervision-group-selector.select.group.error' | translate}}"></ds-error>
|
||||||
|
</ng-container>
|
||||||
|
<ds-eperson-group-list [isListOfEPerson]="false"
|
||||||
|
(select)="updateGroupObjectSelected($event)"></ds-eperson-group-list>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- <div class="d-flex flex-row-reverse m-2"> -->
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-outline-secondary"
|
||||||
|
(click)="close()">
|
||||||
|
<i class="fas fa-times"></i> {{"supervision-group-selector.button.cancel" | translate}}
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-primary save"
|
||||||
|
(click)="save()">
|
||||||
|
<i class="fas fa-save"></i> {{"supervision-group-selector.button.save" | translate}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -0,0 +1,70 @@
|
|||||||
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { SupervisionOrderGroupSelectorComponent } from './supervision-order-group-selector.component';
|
||||||
|
import { SupervisionOrderDataService } from '../../../../../../core/supervision-order/supervision-order-data.service';
|
||||||
|
import { NotificationsService } from '../../../../../../shared/notifications/notifications.service';
|
||||||
|
import { Group } from '../../../../../../core/eperson/models/group.model';
|
||||||
|
import { SupervisionOrder } from '../../../../../../core/supervision-order/models/supervision-order.model';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
|
||||||
|
describe('SupervisionOrderGroupSelectorComponent', () => {
|
||||||
|
let component: SupervisionOrderGroupSelectorComponent;
|
||||||
|
let fixture: ComponentFixture<SupervisionOrderGroupSelectorComponent>;
|
||||||
|
let debugElement: DebugElement;
|
||||||
|
|
||||||
|
const modalStub = jasmine.createSpyObj('modalStub', ['close']);
|
||||||
|
|
||||||
|
const supervisionOrderDataService: any = jasmine.createSpyObj('supervisionOrderDataService', {
|
||||||
|
create: of(new SupervisionOrder())
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedOrderType = 'NONE';
|
||||||
|
const itemUUID = 'itemUUID1234';
|
||||||
|
|
||||||
|
const selectedGroup = new Group();
|
||||||
|
selectedGroup.uuid = 'GroupUUID1234';
|
||||||
|
|
||||||
|
const supervisionDataObject = new SupervisionOrder();
|
||||||
|
supervisionDataObject.ordertype = selectedOrderType;
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot()],
|
||||||
|
declarations: [SupervisionOrderGroupSelectorComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: NgbActiveModal, useValue: modalStub },
|
||||||
|
{ provide: SupervisionOrderDataService, useValue: supervisionOrderDataService },
|
||||||
|
{ provide: NotificationsService, useValue: {} },
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
fixture = TestBed.createComponent(SupervisionOrderGroupSelectorComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
component.itemUUID = itemUUID;
|
||||||
|
component.selectedGroup = selectedGroup;
|
||||||
|
component.selectedOrderType = selectedOrderType;
|
||||||
|
debugElement = fixture.debugElement;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create component', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call create for supervision order', () => {
|
||||||
|
component.save();
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(supervisionOrderDataService.create).toHaveBeenCalledWith(supervisionDataObject, itemUUID, selectedGroup.uuid, selectedOrderType);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@@ -0,0 +1,97 @@
|
|||||||
|
import { Component, EventEmitter, Output } from '@angular/core';
|
||||||
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { getFirstCompletedRemoteData } from 'src/app/core/shared/operators';
|
||||||
|
import { NotificationsService } from 'src/app/shared/notifications/notifications.service';
|
||||||
|
import { DSONameService } from '../../../../../../core/breadcrumbs/dso-name.service';
|
||||||
|
import { Group } from '../../../../../../core/eperson/models/group.model';
|
||||||
|
import { SupervisionOrder } from '../../../../../../core/supervision-order/models/supervision-order.model';
|
||||||
|
import { SupervisionOrderDataService } from '../../../../../../core/supervision-order/supervision-order-data.service';
|
||||||
|
import { RemoteData } from '../../../../../../core/data/remote-data';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component to wrap a dropdown - for type of order -
|
||||||
|
* and a list of groups
|
||||||
|
* inside a modal
|
||||||
|
* Used to create a new supervision order
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-supervision-group-selector',
|
||||||
|
styleUrls: ['./supervision-order-group-selector.component.scss'],
|
||||||
|
templateUrl: './supervision-order-group-selector.component.html',
|
||||||
|
})
|
||||||
|
export class SupervisionOrderGroupSelectorComponent {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The item to perform the actions on
|
||||||
|
*/
|
||||||
|
itemUUID: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The selected supervision order type
|
||||||
|
*/
|
||||||
|
selectedOrderType: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* selected group for supervision
|
||||||
|
*/
|
||||||
|
selectedGroup: Group;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* boolean flag for the validations
|
||||||
|
*/
|
||||||
|
isSubmitted = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event emitted when a new SupervisionOrder has been created
|
||||||
|
*/
|
||||||
|
@Output() create: EventEmitter<SupervisionOrder> = new EventEmitter<SupervisionOrder>();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public dsoNameService: DSONameService,
|
||||||
|
private activeModal: NgbActiveModal,
|
||||||
|
private supervisionOrderDataService: SupervisionOrderDataService,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected translateService: TranslateService,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the modal
|
||||||
|
*/
|
||||||
|
close() {
|
||||||
|
this.activeModal.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assign the value of group on select
|
||||||
|
*/
|
||||||
|
updateGroupObjectSelected(object) {
|
||||||
|
this.selectedGroup = object;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the supervision order
|
||||||
|
*/
|
||||||
|
save() {
|
||||||
|
this.isSubmitted = true;
|
||||||
|
if (this.selectedOrderType && this.selectedGroup) {
|
||||||
|
let supervisionDataObject = new SupervisionOrder();
|
||||||
|
supervisionDataObject.ordertype = this.selectedOrderType;
|
||||||
|
this.supervisionOrderDataService.create(supervisionDataObject, this.itemUUID, this.selectedGroup.uuid, this.selectedOrderType).pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
).subscribe((rd: RemoteData<SupervisionOrder>) => {
|
||||||
|
if (rd.state === 'Success') {
|
||||||
|
this.notificationsService.success(this.translateService.get('supervision-group-selector.notification.create.success.title', { name: this.dsoNameService.getName(this.selectedGroup) }));
|
||||||
|
this.create.emit(rd.payload);
|
||||||
|
this.close();
|
||||||
|
} else {
|
||||||
|
this.notificationsService.error(
|
||||||
|
this.translateService.get('supervision-group-selector.notification.create.failure.title'),
|
||||||
|
rd.statusCode === 422 ? this.translateService.get('supervision-group-selector.notification.create.already-existing') : rd.errorMessage);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,15 @@
|
|||||||
|
<ng-container *ngVar="(supervisionOrderEntries$ | async) as supervisionOrders">
|
||||||
|
<div class="item-list-supervision" *ngIf="supervisionOrders?.length > 0">
|
||||||
|
<div>
|
||||||
|
<span>{{'workflow-item.search.result.list.element.supervised-by' | translate}} </span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a class="badge badge-primary mr-1 mb-1 text-capitalize mw-100 text-truncate" *ngFor="let supervisionOrder of supervisionOrders" data-test="soBadge"
|
||||||
|
[ngbTooltip]="'workflow-item.search.result.list.element.supervised.remove-tooltip' | translate"
|
||||||
|
(click)="$event.preventDefault(); $event.stopImmediatePropagation(); deleteSupervisionOrder(supervisionOrder)" aria-label="Close">
|
||||||
|
{{ dsoNameService.getName(supervisionOrder.group) }}
|
||||||
|
<span aria-hidden="true"> ×</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
@@ -0,0 +1,3 @@
|
|||||||
|
.badge {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
@@ -0,0 +1,61 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { NO_ERRORS_SCHEMA, SimpleChange } from '@angular/core';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
|
||||||
|
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { SupervisionOrderStatusComponent } from './supervision-order-status.component';
|
||||||
|
import { VarDirective } from '../../../../../../shared/utils/var.directive';
|
||||||
|
import { TranslateLoaderMock } from '../../../../../../shared/mocks/translate-loader.mock';
|
||||||
|
import { supervisionOrderListMock } from '../../../../../../shared/testing/supervision-order.mock';
|
||||||
|
|
||||||
|
describe('SupervisionOrderStatusComponent', () => {
|
||||||
|
let component: SupervisionOrderStatusComponent;
|
||||||
|
let fixture: ComponentFixture<SupervisionOrderStatusComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
NgbTooltipModule,
|
||||||
|
TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: TranslateLoaderMock
|
||||||
|
}
|
||||||
|
})
|
||||||
|
],
|
||||||
|
declarations: [ SupervisionOrderStatusComponent, VarDirective ],
|
||||||
|
schemas: [
|
||||||
|
NO_ERRORS_SCHEMA
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(SupervisionOrderStatusComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
component.supervisionOrderList = supervisionOrderListMock;
|
||||||
|
component.ngOnChanges( {
|
||||||
|
supervisionOrderList: new SimpleChange(null, supervisionOrderListMock, true)
|
||||||
|
});
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render badges properly', () => {
|
||||||
|
const badges = fixture.debugElement.queryAll(By.css('[data-test="soBadge"]'));
|
||||||
|
expect(badges.length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit delete event on click', () => {
|
||||||
|
spyOn(component.delete, 'emit');
|
||||||
|
const badges = fixture.debugElement.queryAll(By.css('[data-test="soBadge"]'));
|
||||||
|
badges[0].nativeElement.click();
|
||||||
|
expect(component.delete.emit).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,88 @@
|
|||||||
|
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
|
||||||
|
|
||||||
|
import { BehaviorSubject, from, Observable } from 'rxjs';
|
||||||
|
import { map, mergeMap, reduce } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { SupervisionOrder } from '../../../../../../core/supervision-order/models/supervision-order.model';
|
||||||
|
import { Group } from '../../../../../../core/eperson/models/group.model';
|
||||||
|
import { getFirstCompletedRemoteData } from '../../../../../../core/shared/operators';
|
||||||
|
import { isNotEmpty } from '../../../../../../shared/empty.util';
|
||||||
|
import { RemoteData } from '../../../../../../core/data/remote-data';
|
||||||
|
import { DSONameService } from '../../../../../../core/breadcrumbs/dso-name.service';
|
||||||
|
|
||||||
|
export interface SupervisionOrderListEntry {
|
||||||
|
supervisionOrder: SupervisionOrder;
|
||||||
|
group: Group
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-supervision-order-status',
|
||||||
|
templateUrl: './supervision-order-status.component.html',
|
||||||
|
styleUrls: ['./supervision-order-status.component.scss']
|
||||||
|
})
|
||||||
|
export class SupervisionOrderStatusComponent implements OnChanges {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of supervision order object to show
|
||||||
|
*/
|
||||||
|
@Input() supervisionOrderList: SupervisionOrder[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of the supervision orders combined with the group
|
||||||
|
*/
|
||||||
|
supervisionOrderEntries$: BehaviorSubject<SupervisionOrderListEntry[]> = new BehaviorSubject([]);
|
||||||
|
|
||||||
|
@Output() delete: EventEmitter<SupervisionOrderListEntry> = new EventEmitter<SupervisionOrderListEntry>();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public dsoNameService: DSONameService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
|
if (changes && changes.supervisionOrderList) {
|
||||||
|
this.getSupervisionOrderEntries(changes.supervisionOrderList.currentValue)
|
||||||
|
.subscribe((supervisionOrderEntries: SupervisionOrderListEntry[]) => {
|
||||||
|
this.supervisionOrderEntries$.next(supervisionOrderEntries);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a list of SupervisionOrderListEntry by the given SupervisionOrder list
|
||||||
|
*
|
||||||
|
* @param supervisionOrderList
|
||||||
|
*/
|
||||||
|
private getSupervisionOrderEntries(supervisionOrderList: SupervisionOrder[]): Observable<SupervisionOrderListEntry[]> {
|
||||||
|
return from(supervisionOrderList).pipe(
|
||||||
|
mergeMap((so: SupervisionOrder) => so.group.pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
map((sogRD: RemoteData<Group>) => {
|
||||||
|
if (sogRD.hasSucceeded) {
|
||||||
|
const entry: SupervisionOrderListEntry = {
|
||||||
|
supervisionOrder: so,
|
||||||
|
group: sogRD.payload
|
||||||
|
};
|
||||||
|
return entry;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)),
|
||||||
|
reduce((acc: SupervisionOrderListEntry[], value: any) => {
|
||||||
|
if (isNotEmpty(value)) {
|
||||||
|
return [...acc, value];
|
||||||
|
} else {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
}, []),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emit a delete event with the given SupervisionOrderListEntry.
|
||||||
|
*/
|
||||||
|
deleteSupervisionOrder(supervisionOrder: SupervisionOrderListEntry) {
|
||||||
|
this.delete.emit(supervisionOrder);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,16 @@
|
|||||||
|
<div class="my-1">
|
||||||
|
<ds-supervision-order-status [supervisionOrderList]="supervisionOrderList"
|
||||||
|
(delete)="deleteSupervisionOrder($event)"></ds-supervision-order-status>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-children-mr">
|
||||||
|
<a [ngClass]="{'btn-sm': small}" class="btn btn-light my-1 delete-link" [routerLink]="[getDeleteRoute()]" [title]="'admin.workflow.item.delete' | translate">
|
||||||
|
<i class="fa fa-trash"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.workflow.item.delete" | translate}}</span>
|
||||||
|
</a>
|
||||||
|
<a [ngClass]="{'btn-sm': small}" class="btn btn-light my-1 policies-link" [routerLink]="resourcePoliciesPageRoute" [title]="'admin.workflow.item.policies' | translate">
|
||||||
|
<i class="fas fa-edit"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{'admin.workflow.item.policies' | translate}}</span>
|
||||||
|
</a>
|
||||||
|
<a [ngClass]="{'btn-sm': small}" class="btn btn-light my-1 supervision-group-selector" [title]="'admin.workflow.item.supervision' | translate" (click)="openSupervisionModal()">
|
||||||
|
<i class="fas fa-users-cog"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{'admin.workflow.item.supervision' | translate}}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
@@ -0,0 +1,156 @@
|
|||||||
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
import { NgbModalModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { URLCombiner } from '../../../../../core/url-combiner/url-combiner';
|
||||||
|
import { WorkspaceItemAdminWorkflowActionsComponent } from './workspace-item-admin-workflow-actions.component';
|
||||||
|
import { WorkspaceItem } from '../../../../../core/submission/models/workspaceitem.model';
|
||||||
|
import {
|
||||||
|
getWorkflowItemDeleteRoute,
|
||||||
|
} from '../../../../../workflowitems-edit-page/workflowitems-edit-page-routing-paths';
|
||||||
|
import { Item } from '../../../../../core/shared/item.model';
|
||||||
|
import { RemoteData } from '../../../../../core/data/remote-data';
|
||||||
|
import { RequestEntryState } from '../../../../../core/data/request-entry-state.model';
|
||||||
|
import { NotificationsServiceStub } from '../../../../../shared/testing/notifications-service.stub';
|
||||||
|
import { NotificationsService } from '../../../../../shared/notifications/notifications.service';
|
||||||
|
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
||||||
|
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
|
||||||
|
import { SupervisionOrderDataService } from '../../../../../core/supervision-order/supervision-order-data.service';
|
||||||
|
import { ConfirmationModalComponent } from '../../../../../shared/confirmation-modal/confirmation-modal.component';
|
||||||
|
import { supervisionOrderEntryMock } from '../../../../../shared/testing/supervision-order.mock';
|
||||||
|
import {
|
||||||
|
SupervisionOrderGroupSelectorComponent
|
||||||
|
} from './supervision-order-group-selector/supervision-order-group-selector.component';
|
||||||
|
|
||||||
|
describe('WorkspaceItemAdminWorkflowActionsComponent', () => {
|
||||||
|
let component: WorkspaceItemAdminWorkflowActionsComponent;
|
||||||
|
let fixture: ComponentFixture<WorkspaceItemAdminWorkflowActionsComponent>;
|
||||||
|
let id;
|
||||||
|
let wsi;
|
||||||
|
let item = new Item();
|
||||||
|
item.uuid = 'itemUUID1111';
|
||||||
|
const rd = new RemoteData(undefined, undefined, undefined, RequestEntryState.Success, undefined, item, 200);
|
||||||
|
let supervisionOrderDataService;
|
||||||
|
let notificationService: NotificationsServiceStub;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
notificationService = new NotificationsServiceStub();
|
||||||
|
supervisionOrderDataService = jasmine.createSpyObj('supervisionOrderDataService', {
|
||||||
|
searchByItem: jasmine.createSpy('searchByItem'),
|
||||||
|
delete: jasmine.createSpy('delete'),
|
||||||
|
});
|
||||||
|
id = '780b2588-bda5-4112-a1cd-0b15000a5339';
|
||||||
|
wsi = new WorkspaceItem();
|
||||||
|
wsi.id = id;
|
||||||
|
wsi.item = of(rd);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
init();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
NgbModalModule,
|
||||||
|
TranslateModule.forRoot(),
|
||||||
|
RouterTestingModule.withRoutes([])
|
||||||
|
],
|
||||||
|
declarations: [WorkspaceItemAdminWorkflowActionsComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: DSONameService, useClass: DSONameServiceMock },
|
||||||
|
{ provide: NotificationsService, useValue: notificationService },
|
||||||
|
{ provide: SupervisionOrderDataService, useValue: supervisionOrderDataService }
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(WorkspaceItemAdminWorkflowActionsComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
component.wsi = wsi;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render a delete button with the correct link', () => {
|
||||||
|
const button = fixture.debugElement.query(By.css('a.delete-link'));
|
||||||
|
const link = button.nativeElement.href;
|
||||||
|
expect(link).toContain(new URLCombiner(getWorkflowItemDeleteRoute(wsi.id)).toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render a policies button with the correct link', () => {
|
||||||
|
const a = fixture.debugElement.query(By.css('a.policies-link'));
|
||||||
|
const link = a.nativeElement.href;
|
||||||
|
expect(link).toContain(new URLCombiner('/items/itemUUID1111/edit/authorizations').toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deleteSupervisionOrder', () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(component.delete, 'emit');
|
||||||
|
spyOn((component as any).modalService, 'open').and.returnValue({
|
||||||
|
componentInstance: { response: of(true) }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when delete succeeded', () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
supervisionOrderDataService.delete.and.returnValue(of(true));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should notify success', () => {
|
||||||
|
component.deleteSupervisionOrder(supervisionOrderEntryMock);
|
||||||
|
expect((component as any).modalService.open).toHaveBeenCalledWith(ConfirmationModalComponent);
|
||||||
|
expect(notificationService.success).toHaveBeenCalled();
|
||||||
|
expect(component.delete.emit).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when delete failed', () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
supervisionOrderDataService.delete.and.returnValue(of(false));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should notify success', () => {
|
||||||
|
component.deleteSupervisionOrder(supervisionOrderEntryMock);
|
||||||
|
expect((component as any).modalService.open).toHaveBeenCalledWith(ConfirmationModalComponent);
|
||||||
|
expect(notificationService.error).toHaveBeenCalled();
|
||||||
|
expect(component.delete.emit).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('openSupervisionModal', () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(component.create, 'emit');
|
||||||
|
spyOn((component as any).modalService, 'open').and.returnValue({
|
||||||
|
componentInstance: { create: of(true) }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit create event properly', () => {
|
||||||
|
component.openSupervisionModal();
|
||||||
|
expect((component as any).modalService.open).toHaveBeenCalledWith(SupervisionOrderGroupSelectorComponent, {
|
||||||
|
size: 'lg',
|
||||||
|
backdrop: 'static'
|
||||||
|
});
|
||||||
|
expect(component.create.emit).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
@@ -0,0 +1,192 @@
|
|||||||
|
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||||
|
|
||||||
|
import { map, Observable } from 'rxjs';
|
||||||
|
import { switchMap, take, tap } from 'rxjs/operators';
|
||||||
|
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { Item } from '../../../../../core/shared/item.model';
|
||||||
|
import { getFirstSucceededRemoteDataPayload } from '../../../../../core/shared/operators';
|
||||||
|
import {
|
||||||
|
SupervisionOrderGroupSelectorComponent
|
||||||
|
} from './supervision-order-group-selector/supervision-order-group-selector.component';
|
||||||
|
import {
|
||||||
|
getWorkflowItemDeleteRoute
|
||||||
|
} from '../../../../../workflowitems-edit-page/workflowitems-edit-page-routing-paths';
|
||||||
|
import { ITEM_EDIT_AUTHORIZATIONS_PATH } from '../../../../../item-page/edit-item-page/edit-item-page.routing-paths';
|
||||||
|
import { WorkspaceItem } from '../../../../../core/submission/models/workspaceitem.model';
|
||||||
|
import { SupervisionOrder } from '../../../../../core/supervision-order/models/supervision-order.model';
|
||||||
|
import { SupervisionOrderListEntry } from './supervision-order-status/supervision-order-status.component';
|
||||||
|
import { ConfirmationModalComponent } from '../../../../../shared/confirmation-modal/confirmation-modal.component';
|
||||||
|
import { hasValue } from '../../../../../shared/empty.util';
|
||||||
|
import { NotificationsService } from '../../../../../shared/notifications/notifications.service';
|
||||||
|
import { SupervisionOrderDataService } from '../../../../../core/supervision-order/supervision-order-data.service';
|
||||||
|
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
||||||
|
import { DSpaceObject } from '../../../../../core/shared/dspace-object.model';
|
||||||
|
import { getSearchResultFor } from '../../../../../shared/search/search-result-element-decorator';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-workspace-item-admin-workflow-actions-element',
|
||||||
|
styleUrls: ['./workspace-item-admin-workflow-actions.component.scss'],
|
||||||
|
templateUrl: './workspace-item-admin-workflow-actions.component.html'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* The component for displaying the actions for a list element for a workspace-item on the admin workflow search page
|
||||||
|
*/
|
||||||
|
export class WorkspaceItemAdminWorkflowActionsComponent implements OnInit {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The workspace item to perform the actions on
|
||||||
|
*/
|
||||||
|
@Input() public wsi: WorkspaceItem;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to use small buttons or not
|
||||||
|
*/
|
||||||
|
@Input() public small: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of supervision order object to show
|
||||||
|
*/
|
||||||
|
@Input() supervisionOrderList: SupervisionOrder[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The item related to the workspace item
|
||||||
|
*/
|
||||||
|
item: Item;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array containing the route to the resource policies page
|
||||||
|
*/
|
||||||
|
resourcePoliciesPageRoute: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The i18n keys prefix
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private messagePrefix = 'workflow-item.search.result';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event emitted when a new SupervisionOrder has been created
|
||||||
|
*/
|
||||||
|
@Output() create: EventEmitter<DSpaceObject> = new EventEmitter<DSpaceObject>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event emitted when new SupervisionOrder has been deleted
|
||||||
|
*/
|
||||||
|
@Output() delete: EventEmitter<DSpaceObject> = new EventEmitter<DSpaceObject>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event emitted when a new SupervisionOrder has been created
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
protected dsoNameService: DSONameService,
|
||||||
|
protected modalService: NgbModal,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected supervisionOrderDataService: SupervisionOrderDataService,
|
||||||
|
protected translateService: TranslateService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
const item$: Observable<Item> = this.wsi.item.pipe(
|
||||||
|
getFirstSucceededRemoteDataPayload(),
|
||||||
|
);
|
||||||
|
|
||||||
|
item$.pipe(
|
||||||
|
map((item: Item) => this.getPoliciesRoute(item))
|
||||||
|
).subscribe((route: string[]) => {
|
||||||
|
this.resourcePoliciesPageRoute = route;
|
||||||
|
});
|
||||||
|
|
||||||
|
item$.subscribe((item: Item) => {
|
||||||
|
this.item = item;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the path to the delete page of this workflow item
|
||||||
|
*/
|
||||||
|
getDeleteRoute(): string {
|
||||||
|
return getWorkflowItemDeleteRoute(this.wsi.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the path to the administrative edit page policies tab
|
||||||
|
*/
|
||||||
|
getPoliciesRoute(item: Item): string[] {
|
||||||
|
return ['/items', item.uuid, 'edit', ITEM_EDIT_AUTHORIZATIONS_PATH];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the Group from the Repository. The Group will be the only that this form is showing.
|
||||||
|
* It'll either show a success or error message depending on whether delete was successful or not.
|
||||||
|
*/
|
||||||
|
deleteSupervisionOrder(supervisionOrderEntry: SupervisionOrderListEntry) {
|
||||||
|
const modalRef = this.modalService.open(ConfirmationModalComponent);
|
||||||
|
modalRef.componentInstance.dso = supervisionOrderEntry.group;
|
||||||
|
modalRef.componentInstance.headerLabel = this.messagePrefix + '.delete-supervision.modal.header';
|
||||||
|
modalRef.componentInstance.infoLabel = this.messagePrefix + '.delete-supervision.modal.info';
|
||||||
|
modalRef.componentInstance.cancelLabel = this.messagePrefix + '.delete-supervision.modal.cancel';
|
||||||
|
modalRef.componentInstance.confirmLabel = this.messagePrefix + '.delete-supervision.modal.confirm';
|
||||||
|
modalRef.componentInstance.brandColor = 'danger';
|
||||||
|
modalRef.componentInstance.confirmIcon = 'fas fa-trash';
|
||||||
|
modalRef.componentInstance.response.pipe(
|
||||||
|
take(1),
|
||||||
|
switchMap((confirm: boolean) => {
|
||||||
|
if (confirm && hasValue(supervisionOrderEntry.supervisionOrder.id)) {
|
||||||
|
return this.supervisionOrderDataService.delete(supervisionOrderEntry.supervisionOrder.id).pipe(
|
||||||
|
take(1),
|
||||||
|
tap((result: boolean) => {
|
||||||
|
if (result) {
|
||||||
|
this.notificationsService.success(
|
||||||
|
null,
|
||||||
|
this.translateService.get(
|
||||||
|
this.messagePrefix + '.notification.deleted.success',
|
||||||
|
{ name: this.dsoNameService.getName(supervisionOrderEntry.group) }
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.notificationsService.error(
|
||||||
|
null,
|
||||||
|
this.translateService.get(
|
||||||
|
this.messagePrefix + '.notification.deleted.failure',
|
||||||
|
{ name: this.dsoNameService.getName(supervisionOrderEntry.group) }
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
).subscribe((result: boolean) => {
|
||||||
|
if (result) {
|
||||||
|
this.delete.emit(this.convertReloadedObject());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the Supervision Modal to create a supervision order
|
||||||
|
*/
|
||||||
|
openSupervisionModal() {
|
||||||
|
const supervisionModal: NgbModalRef = this.modalService.open(SupervisionOrderGroupSelectorComponent, {
|
||||||
|
size: 'lg',
|
||||||
|
backdrop: 'static'
|
||||||
|
});
|
||||||
|
supervisionModal.componentInstance.itemUUID = this.item.uuid;
|
||||||
|
supervisionModal.componentInstance.create.subscribe(() => {
|
||||||
|
this.create.emit(this.convertReloadedObject());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert the reloadedObject to the Type required by this dso.
|
||||||
|
*/
|
||||||
|
private convertReloadedObject(): DSpaceObject {
|
||||||
|
const constructor = getSearchResultFor((this.wsi as any).constructor);
|
||||||
|
return Object.assign(new constructor(), this.wsi, {
|
||||||
|
indexableObject: this.wsi
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@@ -7,14 +7,22 @@ import { TruncatableService } from '../../../../../shared/truncatable/truncatabl
|
|||||||
import { CollectionElementLinkType } from '../../../../../shared/object-collection/collection-element-link.type';
|
import { CollectionElementLinkType } from '../../../../../shared/object-collection/collection-element-link.type';
|
||||||
import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
||||||
import { RouterTestingModule } from '@angular/router/testing';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
import { WorkflowItemSearchResultAdminWorkflowGridElementComponent } from './workflow-item-search-result-admin-workflow-grid-element.component';
|
import {
|
||||||
|
WorkflowItemSearchResultAdminWorkflowGridElementComponent
|
||||||
|
} from './workflow-item-search-result-admin-workflow-grid-element.component';
|
||||||
import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model';
|
import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model';
|
||||||
import { LinkService } from '../../../../../core/cache/builders/link.service';
|
import { LinkService } from '../../../../../core/cache/builders/link.service';
|
||||||
import { followLink } from '../../../../../shared/utils/follow-link-config.model';
|
import { followLink } from '../../../../../shared/utils/follow-link-config.model';
|
||||||
import { Item } from '../../../../../core/shared/item.model';
|
import { Item } from '../../../../../core/shared/item.model';
|
||||||
import { ItemGridElementComponent } from '../../../../../shared/object-grid/item-grid-element/item-types/item/item-grid-element.component';
|
import {
|
||||||
import { ListableObjectDirective } from '../../../../../shared/object-collection/shared/listable-object/listable-object.directive';
|
ItemGridElementComponent
|
||||||
import { WorkflowItemSearchResult } from '../../../../../shared/object-collection/shared/workflow-item-search-result.model';
|
} from '../../../../../shared/object-grid/item-grid-element/item-types/item/item-grid-element.component';
|
||||||
|
import {
|
||||||
|
ListableObjectDirective
|
||||||
|
} from '../../../../../shared/object-collection/shared/listable-object/listable-object.directive';
|
||||||
|
import {
|
||||||
|
WorkflowItemSearchResult
|
||||||
|
} from '../../../../../shared/object-collection/shared/workflow-item-search-result.model';
|
||||||
import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service';
|
import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils';
|
||||||
import { getMockLinkService } from '../../../../../shared/mocks/link-service.mock';
|
import { getMockLinkService } from '../../../../../shared/mocks/link-service.mock';
|
||||||
@@ -22,7 +30,7 @@ import { of as observableOf } from 'rxjs';
|
|||||||
import { getMockThemeService } from '../../../../../shared/mocks/theme-service.mock';
|
import { getMockThemeService } from '../../../../../shared/mocks/theme-service.mock';
|
||||||
import { ThemeService } from '../../../../../shared/theme-support/theme.service';
|
import { ThemeService } from '../../../../../shared/theme-support/theme.service';
|
||||||
|
|
||||||
describe('WorkflowItemAdminWorkflowGridElementComponent', () => {
|
describe('WorkflowItemSearchResultAdminWorkflowGridElementComponent', () => {
|
||||||
let component: WorkflowItemSearchResultAdminWorkflowGridElementComponent;
|
let component: WorkflowItemSearchResultAdminWorkflowGridElementComponent;
|
||||||
let fixture: ComponentFixture<WorkflowItemSearchResultAdminWorkflowGridElementComponent>;
|
let fixture: ComponentFixture<WorkflowItemSearchResultAdminWorkflowGridElementComponent>;
|
||||||
let id;
|
let id;
|
||||||
|
@@ -0,0 +1,16 @@
|
|||||||
|
<ng-template dsListableObject>
|
||||||
|
</ng-template>
|
||||||
|
<div #badges class="position-absolute ml-1">
|
||||||
|
<div class="workflow-badge">
|
||||||
|
<span class="badge badge-info">{{ "admin.workflow.item.workspace" | translate }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ul #buttons class="list-group list-group-flush">
|
||||||
|
<li class="list-group-item">
|
||||||
|
<ds-workspace-item-admin-workflow-actions-element [small]="true"
|
||||||
|
[supervisionOrderList]="supervisionOrder$ | async"
|
||||||
|
[wsi]="dso"
|
||||||
|
(create)="reloadObject($event)"
|
||||||
|
(delete)="reloadObject($event)"></ds-workspace-item-admin-workflow-actions-element>
|
||||||
|
</li>
|
||||||
|
</ul>
|
@@ -0,0 +1,127 @@
|
|||||||
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
|
||||||
|
import { CollectionElementLinkType } from '../../../../../shared/object-collection/collection-element-link.type';
|
||||||
|
import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
||||||
|
import {
|
||||||
|
WorkspaceItemSearchResultAdminWorkflowGridElementComponent
|
||||||
|
} from './workspace-item-search-result-admin-workflow-grid-element.component';
|
||||||
|
import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model';
|
||||||
|
import { LinkService } from '../../../../../core/cache/builders/link.service';
|
||||||
|
import { followLink } from '../../../../../shared/utils/follow-link-config.model';
|
||||||
|
import { Item } from '../../../../../core/shared/item.model';
|
||||||
|
import {
|
||||||
|
ItemGridElementComponent
|
||||||
|
} from '../../../../../shared/object-grid/item-grid-element/item-types/item/item-grid-element.component';
|
||||||
|
import {
|
||||||
|
ListableObjectDirective
|
||||||
|
} from '../../../../../shared/object-collection/shared/listable-object/listable-object.directive';
|
||||||
|
import {
|
||||||
|
WorkflowItemSearchResult
|
||||||
|
} from '../../../../../shared/object-collection/shared/workflow-item-search-result.model';
|
||||||
|
import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils';
|
||||||
|
import { getMockLinkService } from '../../../../../shared/mocks/link-service.mock';
|
||||||
|
import { getMockThemeService } from '../../../../../shared/mocks/theme-service.mock';
|
||||||
|
import { ThemeService } from '../../../../../shared/theme-support/theme.service';
|
||||||
|
import {
|
||||||
|
supervisionOrderPaginatedListRD,
|
||||||
|
supervisionOrderPaginatedListRD$
|
||||||
|
} from '../../../../../shared/testing/supervision-order.mock';
|
||||||
|
import { SupervisionOrderDataService } from '../../../../../core/supervision-order/supervision-order-data.service';
|
||||||
|
import { DSpaceObject } from '../../../../../core/shared/dspace-object.model';
|
||||||
|
|
||||||
|
describe('WorkspaceItemSearchResultAdminWorkflowGridElementComponent', () => {
|
||||||
|
let component: WorkspaceItemSearchResultAdminWorkflowGridElementComponent;
|
||||||
|
let fixture: ComponentFixture<WorkspaceItemSearchResultAdminWorkflowGridElementComponent>;
|
||||||
|
let id;
|
||||||
|
let wfi;
|
||||||
|
let itemRD$;
|
||||||
|
let linkService;
|
||||||
|
let object;
|
||||||
|
let themeService;
|
||||||
|
let supervisionOrderDataService;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
itemRD$ = createSuccessfulRemoteDataObject$(new Item());
|
||||||
|
id = '780b2588-bda5-4112-a1cd-0b15000a5339';
|
||||||
|
object = new WorkflowItemSearchResult();
|
||||||
|
wfi = new WorkflowItem();
|
||||||
|
wfi.item = itemRD$;
|
||||||
|
object.indexableObject = wfi;
|
||||||
|
linkService = getMockLinkService();
|
||||||
|
themeService = getMockThemeService();
|
||||||
|
supervisionOrderDataService = jasmine.createSpyObj('supervisionOrderDataService', {
|
||||||
|
searchByItem: jasmine.createSpy('searchByItem'),
|
||||||
|
delete: jasmine.createSpy('delete'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
init();
|
||||||
|
TestBed.configureTestingModule(
|
||||||
|
{
|
||||||
|
declarations: [WorkspaceItemSearchResultAdminWorkflowGridElementComponent, ItemGridElementComponent, ListableObjectDirective],
|
||||||
|
imports: [
|
||||||
|
NoopAnimationsModule,
|
||||||
|
TranslateModule.forRoot(),
|
||||||
|
RouterTestingModule.withRoutes([]),
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{ provide: LinkService, useValue: linkService },
|
||||||
|
{ provide: ThemeService, useValue: themeService },
|
||||||
|
{
|
||||||
|
provide: TruncatableService, useValue: {
|
||||||
|
isCollapsed: () => observableOf(true),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ provide: BitstreamDataService, useValue: {} },
|
||||||
|
{ provide: SupervisionOrderDataService, useValue: supervisionOrderDataService }
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
})
|
||||||
|
.overrideComponent(WorkspaceItemSearchResultAdminWorkflowGridElementComponent, {
|
||||||
|
set: {
|
||||||
|
entryComponents: [ItemGridElementComponent]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
linkService.resolveLink.and.callFake((a) => a);
|
||||||
|
fixture = TestBed.createComponent(WorkspaceItemSearchResultAdminWorkflowGridElementComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
component.object = object;
|
||||||
|
component.linkTypes = CollectionElementLinkType;
|
||||||
|
component.index = 0;
|
||||||
|
component.viewModes = ViewMode;
|
||||||
|
supervisionOrderDataService.searchByItem.and.returnValue(supervisionOrderPaginatedListRD$);
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should retrieve the item using the link service', () => {
|
||||||
|
expect(linkService.resolveLink).toHaveBeenCalledWith(wfi, followLink('item'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should retrieve supervision order objects properly', () => {
|
||||||
|
expect(component.supervisionOrder$.value).toEqual(supervisionOrderPaginatedListRD.payload.page);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit reloadedObject properly ', () => {
|
||||||
|
spyOn(component.reloadedObject, 'emit');
|
||||||
|
const dso = new DSpaceObject();
|
||||||
|
component.reloadObject(dso);
|
||||||
|
expect(component.reloadedObject.emit).toHaveBeenCalledWith(dso);
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,160 @@
|
|||||||
|
import { Component, ComponentFactoryResolver, ElementRef, ViewChild, OnInit } from '@angular/core';
|
||||||
|
|
||||||
|
import { BehaviorSubject, Observable } from 'rxjs';
|
||||||
|
import { map, mergeMap, take, tap } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { Item } from '../../../../../core/shared/item.model';
|
||||||
|
import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
||||||
|
import {
|
||||||
|
getListableObjectComponent,
|
||||||
|
listableObjectComponent
|
||||||
|
} from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
|
||||||
|
import { Context } from '../../../../../core/shared/context.model';
|
||||||
|
import {
|
||||||
|
SearchResultGridElementComponent
|
||||||
|
} from '../../../../../shared/object-grid/search-result-grid-element/search-result-grid-element.component';
|
||||||
|
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
|
||||||
|
import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service';
|
||||||
|
import { GenericConstructor } from '../../../../../core/shared/generic-constructor';
|
||||||
|
import {
|
||||||
|
ListableObjectDirective
|
||||||
|
} from '../../../../../shared/object-collection/shared/listable-object/listable-object.directive';
|
||||||
|
import { WorkspaceItem } from '../../../../../core/submission/models/workspaceitem.model';
|
||||||
|
import { LinkService } from '../../../../../core/cache/builders/link.service';
|
||||||
|
import { followLink } from '../../../../../shared/utils/follow-link-config.model';
|
||||||
|
import { RemoteData } from '../../../../../core/data/remote-data';
|
||||||
|
import {
|
||||||
|
getAllSucceededRemoteData,
|
||||||
|
getFirstCompletedRemoteData,
|
||||||
|
getRemoteDataPayload
|
||||||
|
} from '../../../../../core/shared/operators';
|
||||||
|
import {
|
||||||
|
WorkspaceItemSearchResult
|
||||||
|
} from '../../../../../shared/object-collection/shared/workspace-item-search-result.model';
|
||||||
|
import { ThemeService } from '../../../../../shared/theme-support/theme.service';
|
||||||
|
import { DSpaceObject } from '../../../../../core/shared/dspace-object.model';
|
||||||
|
import { SupervisionOrder } from '../../../../../core/supervision-order/models/supervision-order.model';
|
||||||
|
import { PaginatedList } from '../../../../../core/data/paginated-list.model';
|
||||||
|
import { SupervisionOrderDataService } from '../../../../../core/supervision-order/supervision-order-data.service';
|
||||||
|
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
||||||
|
|
||||||
|
@listableObjectComponent(WorkspaceItemSearchResult, ViewMode.GridElement, Context.AdminWorkflowSearch)
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-workflow-item-search-result-admin-workflow-grid-element',
|
||||||
|
styleUrls: ['./workspace-item-search-result-admin-workflow-grid-element.component.scss'],
|
||||||
|
templateUrl: './workspace-item-search-result-admin-workflow-grid-element.component.html'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* The component for displaying a grid element for an workflow item on the admin workflow search page
|
||||||
|
*/
|
||||||
|
export class WorkspaceItemSearchResultAdminWorkflowGridElementComponent extends SearchResultGridElementComponent<WorkspaceItemSearchResult, WorkspaceItem> implements OnInit {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The item linked to the workspace item
|
||||||
|
*/
|
||||||
|
public item$: Observable<Item>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The id of the item linked to the workflow item
|
||||||
|
*/
|
||||||
|
public itemId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The supervision orders linked to the workflow item
|
||||||
|
*/
|
||||||
|
public supervisionOrder$: BehaviorSubject<SupervisionOrder[]> = new BehaviorSubject<SupervisionOrder[]>([]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Directive used to render the dynamic component in
|
||||||
|
*/
|
||||||
|
@ViewChild(ListableObjectDirective, { static: true }) listableObjectDirective: ListableObjectDirective;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The html child that contains the badges html
|
||||||
|
*/
|
||||||
|
@ViewChild('badges', { static: true }) badges: ElementRef;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The html child that contains the button html
|
||||||
|
*/
|
||||||
|
@ViewChild('buttons', { static: true }) buttons: ElementRef;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public dsoNameService: DSONameService,
|
||||||
|
private componentFactoryResolver: ComponentFactoryResolver,
|
||||||
|
private linkService: LinkService,
|
||||||
|
protected truncatableService: TruncatableService,
|
||||||
|
private themeService: ThemeService,
|
||||||
|
protected bitstreamDataService: BitstreamDataService,
|
||||||
|
protected supervisionOrderDataService: SupervisionOrderDataService,
|
||||||
|
) {
|
||||||
|
super(dsoNameService, truncatableService, bitstreamDataService);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup the dynamic child component
|
||||||
|
* Initialize the item object from the workflow item
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
super.ngOnInit();
|
||||||
|
this.dso = this.linkService.resolveLink(this.dso, followLink('item'));
|
||||||
|
this.item$ = (this.dso.item as Observable<RemoteData<Item>>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload());
|
||||||
|
this.item$.pipe(take(1)).subscribe((item: Item) => {
|
||||||
|
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.getComponent(item));
|
||||||
|
|
||||||
|
const viewContainerRef = this.listableObjectDirective.viewContainerRef;
|
||||||
|
viewContainerRef.clear();
|
||||||
|
|
||||||
|
const componentRef = viewContainerRef.createComponent(
|
||||||
|
componentFactory,
|
||||||
|
0,
|
||||||
|
undefined,
|
||||||
|
[
|
||||||
|
[this.badges.nativeElement],
|
||||||
|
[this.buttons.nativeElement]
|
||||||
|
]);
|
||||||
|
(componentRef.instance as any).object = item;
|
||||||
|
(componentRef.instance as any).index = this.index;
|
||||||
|
(componentRef.instance as any).linkType = this.linkType;
|
||||||
|
(componentRef.instance as any).listID = this.listID;
|
||||||
|
componentRef.changeDetectorRef.detectChanges();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
this.item$.pipe(
|
||||||
|
take(1),
|
||||||
|
tap((item: Item) => this.itemId = item.id),
|
||||||
|
mergeMap((item: Item) => this.retrieveSupervisorOrders(item.id))
|
||||||
|
).subscribe((supervisionOrderList: SupervisionOrder[]) => {
|
||||||
|
this.supervisionOrder$.next(supervisionOrderList);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the component depending on the item's entity type, view mode and context
|
||||||
|
* @returns {GenericConstructor<Component>}
|
||||||
|
*/
|
||||||
|
private getComponent(item: Item): GenericConstructor<Component> {
|
||||||
|
return getListableObjectComponent(item.getRenderTypes(), ViewMode.GridElement, undefined, this.themeService.getThemeName());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the list of SupervisionOrder object related to the given item
|
||||||
|
*
|
||||||
|
* @param itemId
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private retrieveSupervisorOrders(itemId): Observable<SupervisionOrder[]> {
|
||||||
|
return this.supervisionOrderDataService.searchByItem(
|
||||||
|
itemId, false, true, followLink('group')
|
||||||
|
).pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
map((soRD: RemoteData<PaginatedList<SupervisionOrder>>) => soRD.hasSucceeded && !soRD.hasNoContent ? soRD.payload.page : [])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
reloadObject(dso: DSpaceObject) {
|
||||||
|
this.reloadedObject.emit(dso);
|
||||||
|
}
|
||||||
|
}
|
@@ -9,11 +9,15 @@ import { CollectionElementLinkType } from '../../../../../shared/object-collecti
|
|||||||
import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
||||||
import { RouterTestingModule } from '@angular/router/testing';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model';
|
import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model';
|
||||||
import { WorkflowItemSearchResultAdminWorkflowListElementComponent } from './workflow-item-search-result-admin-workflow-list-element.component';
|
import {
|
||||||
|
WorkflowItemSearchResultAdminWorkflowListElementComponent
|
||||||
|
} from './workflow-item-search-result-admin-workflow-list-element.component';
|
||||||
import { LinkService } from '../../../../../core/cache/builders/link.service';
|
import { LinkService } from '../../../../../core/cache/builders/link.service';
|
||||||
import { followLink } from '../../../../../shared/utils/follow-link-config.model';
|
import { followLink } from '../../../../../shared/utils/follow-link-config.model';
|
||||||
import { Item } from '../../../../../core/shared/item.model';
|
import { Item } from '../../../../../core/shared/item.model';
|
||||||
import { WorkflowItemSearchResult } from '../../../../../shared/object-collection/shared/workflow-item-search-result.model';
|
import {
|
||||||
|
WorkflowItemSearchResult
|
||||||
|
} from '../../../../../shared/object-collection/shared/workflow-item-search-result.model';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils';
|
||||||
import { getMockLinkService } from '../../../../../shared/mocks/link-service.mock';
|
import { getMockLinkService } from '../../../../../shared/mocks/link-service.mock';
|
||||||
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
||||||
@@ -21,7 +25,7 @@ import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service
|
|||||||
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
|
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
|
||||||
import { environment } from '../../../../../../environments/environment';
|
import { environment } from '../../../../../../environments/environment';
|
||||||
|
|
||||||
describe('WorkflowItemAdminWorkflowListElementComponent', () => {
|
describe('WorkflowItemSearchResultAdminWorkflowListElementComponent', () => {
|
||||||
let component: WorkflowItemSearchResultAdminWorkflowListElementComponent;
|
let component: WorkflowItemSearchResultAdminWorkflowListElementComponent;
|
||||||
let fixture: ComponentFixture<WorkflowItemSearchResultAdminWorkflowListElementComponent>;
|
let fixture: ComponentFixture<WorkflowItemSearchResultAdminWorkflowListElementComponent>;
|
||||||
let id;
|
let id;
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
import { Component, Inject, OnInit } from '@angular/core';
|
import { Component, Inject, OnInit } from '@angular/core';
|
||||||
import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
||||||
import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
|
import {
|
||||||
|
listableObjectComponent
|
||||||
|
} from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
|
||||||
import { Context } from '../../../../../core/shared/context.model';
|
import { Context } from '../../../../../core/shared/context.model';
|
||||||
import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model';
|
import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
@@ -9,9 +11,13 @@ import { followLink } from '../../../../../shared/utils/follow-link-config.model
|
|||||||
import { RemoteData } from '../../../../../core/data/remote-data';
|
import { RemoteData } from '../../../../../core/data/remote-data';
|
||||||
import { getAllSucceededRemoteData, getRemoteDataPayload } from '../../../../../core/shared/operators';
|
import { getAllSucceededRemoteData, getRemoteDataPayload } from '../../../../../core/shared/operators';
|
||||||
import { Item } from '../../../../../core/shared/item.model';
|
import { Item } from '../../../../../core/shared/item.model';
|
||||||
import { SearchResultListElementComponent } from '../../../../../shared/object-list/search-result-list-element/search-result-list-element.component';
|
import {
|
||||||
|
SearchResultListElementComponent
|
||||||
|
} from '../../../../../shared/object-list/search-result-list-element/search-result-list-element.component';
|
||||||
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
|
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
|
||||||
import { WorkflowItemSearchResult } from '../../../../../shared/object-collection/shared/workflow-item-search-result.model';
|
import {
|
||||||
|
WorkflowItemSearchResult
|
||||||
|
} from '../../../../../shared/object-collection/shared/workflow-item-search-result.model';
|
||||||
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
||||||
import { APP_CONFIG, AppConfig } from '../../../../../../config/app-config.interface';
|
import { APP_CONFIG, AppConfig } from '../../../../../../config/app-config.interface';
|
||||||
|
|
||||||
@@ -22,7 +28,7 @@ import { APP_CONFIG, AppConfig } from '../../../../../../config/app-config.inter
|
|||||||
templateUrl: './workflow-item-search-result-admin-workflow-list-element.component.html'
|
templateUrl: './workflow-item-search-result-admin-workflow-list-element.component.html'
|
||||||
})
|
})
|
||||||
/**
|
/**
|
||||||
* The component for displaying a list element for an workflow item on the admin workflow search page
|
* The component for displaying a list element for a workflow item on the admin workflow search page
|
||||||
*/
|
*/
|
||||||
export class WorkflowItemSearchResultAdminWorkflowListElementComponent extends SearchResultListElementComponent<WorkflowItemSearchResult, WorkflowItem> implements OnInit {
|
export class WorkflowItemSearchResultAdminWorkflowListElementComponent extends SearchResultListElementComponent<WorkflowItemSearchResult, WorkflowItem> implements OnInit {
|
||||||
|
|
||||||
|
@@ -0,0 +1,13 @@
|
|||||||
|
<span class="badge badge-info">{{ "admin.workflow.item.workspace" | translate }}</span>
|
||||||
|
<ds-listable-object-component-loader *ngIf="item$ | async"
|
||||||
|
[object]="item$ | async"
|
||||||
|
[viewMode]="viewModes.ListElement"
|
||||||
|
[index]="index"
|
||||||
|
[linkType]="linkType"
|
||||||
|
[listID]="listID"></ds-listable-object-component-loader>
|
||||||
|
|
||||||
|
<ds-workspace-item-admin-workflow-actions-element [small]="false"
|
||||||
|
[supervisionOrderList]="supervisionOrder$ | async"
|
||||||
|
[wsi]="dso"
|
||||||
|
(create)="reloadObject($event)"
|
||||||
|
(delete)="reloadObject($event)"></ds-workspace-item-admin-workflow-actions-element>
|
@@ -0,0 +1,111 @@
|
|||||||
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { mockTruncatableService } from '../../../../../shared/mocks/mock-trucatable.service';
|
||||||
|
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
|
||||||
|
import { CollectionElementLinkType } from '../../../../../shared/object-collection/collection-element-link.type';
|
||||||
|
import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
||||||
|
import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model';
|
||||||
|
import {
|
||||||
|
WorkspaceItemSearchResultAdminWorkflowListElementComponent
|
||||||
|
} from './workspace-item-search-result-admin-workflow-list-element.component';
|
||||||
|
import { LinkService } from '../../../../../core/cache/builders/link.service';
|
||||||
|
import { followLink } from '../../../../../shared/utils/follow-link-config.model';
|
||||||
|
import { Item } from '../../../../../core/shared/item.model';
|
||||||
|
import {
|
||||||
|
WorkflowItemSearchResult
|
||||||
|
} from '../../../../../shared/object-collection/shared/workflow-item-search-result.model';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils';
|
||||||
|
import { getMockLinkService } from '../../../../../shared/mocks/link-service.mock';
|
||||||
|
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
||||||
|
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
|
||||||
|
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
|
||||||
|
import { environment } from '../../../../../../environments/environment';
|
||||||
|
import { SupervisionOrderDataService } from '../../../../../core/supervision-order/supervision-order-data.service';
|
||||||
|
import {
|
||||||
|
supervisionOrderPaginatedListRD,
|
||||||
|
supervisionOrderPaginatedListRD$
|
||||||
|
} from '../../../../../shared/testing/supervision-order.mock';
|
||||||
|
import { DSpaceObject } from '../../../../../core/shared/dspace-object.model';
|
||||||
|
|
||||||
|
describe('WorkspaceItemSearchResultAdminWorkflowListElementComponent', () => {
|
||||||
|
let component: WorkspaceItemSearchResultAdminWorkflowListElementComponent;
|
||||||
|
let fixture: ComponentFixture<WorkspaceItemSearchResultAdminWorkflowListElementComponent>;
|
||||||
|
let id;
|
||||||
|
let wfi;
|
||||||
|
let itemRD$;
|
||||||
|
let linkService;
|
||||||
|
let object;
|
||||||
|
let supervisionOrderDataService;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
itemRD$ = createSuccessfulRemoteDataObject$(new Item());
|
||||||
|
id = '780b2588-bda5-4112-a1cd-0b15000a5339';
|
||||||
|
object = new WorkflowItemSearchResult();
|
||||||
|
wfi = new WorkflowItem();
|
||||||
|
wfi.item = itemRD$;
|
||||||
|
object.indexableObject = wfi;
|
||||||
|
linkService = getMockLinkService();
|
||||||
|
supervisionOrderDataService = jasmine.createSpyObj('supervisionOrderDataService', {
|
||||||
|
searchByItem: jasmine.createSpy('searchByItem'),
|
||||||
|
delete: jasmine.createSpy('delete'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
init();
|
||||||
|
TestBed.configureTestingModule(
|
||||||
|
{
|
||||||
|
declarations: [WorkspaceItemSearchResultAdminWorkflowListElementComponent],
|
||||||
|
imports: [
|
||||||
|
NoopAnimationsModule,
|
||||||
|
TranslateModule.forRoot(),
|
||||||
|
RouterTestingModule.withRoutes([]),
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{ provide: TruncatableService, useValue: mockTruncatableService },
|
||||||
|
{ provide: LinkService, useValue: linkService },
|
||||||
|
{ provide: DSONameService, useClass: DSONameServiceMock },
|
||||||
|
{ provide: SupervisionOrderDataService, useValue: supervisionOrderDataService },
|
||||||
|
{ provide: APP_CONFIG, useValue: environment }
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
linkService.resolveLink.and.callFake((a) => a);
|
||||||
|
fixture = TestBed.createComponent(WorkspaceItemSearchResultAdminWorkflowListElementComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
component.object = object;
|
||||||
|
component.linkTypes = CollectionElementLinkType;
|
||||||
|
component.index = 0;
|
||||||
|
component.viewModes = ViewMode;
|
||||||
|
supervisionOrderDataService.searchByItem.and.returnValue(supervisionOrderPaginatedListRD$);
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should retrieve the item using the link service', () => {
|
||||||
|
expect(linkService.resolveLink).toHaveBeenCalledWith(wfi, followLink('item'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should retrieve supervision order objects properly', () => {
|
||||||
|
expect(component.supervisionOrder$.value).toEqual(supervisionOrderPaginatedListRD.payload.page);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit reloadedObject properly ', () => {
|
||||||
|
spyOn(component.reloadedObject, 'emit');
|
||||||
|
const dso = new DSpaceObject();
|
||||||
|
component.reloadObject(dso);
|
||||||
|
expect(component.reloadedObject.emit).toHaveBeenCalledWith(dso);
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,109 @@
|
|||||||
|
import { Component, Inject, OnInit } from '@angular/core';
|
||||||
|
|
||||||
|
import { BehaviorSubject, Observable } from 'rxjs';
|
||||||
|
import { map, mergeMap, take, tap } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
||||||
|
import {
|
||||||
|
listableObjectComponent
|
||||||
|
} from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
|
||||||
|
import { Context } from '../../../../../core/shared/context.model';
|
||||||
|
import { WorkspaceItem } from '../../../../../core/submission/models/workspaceitem.model';
|
||||||
|
import { LinkService } from '../../../../../core/cache/builders/link.service';
|
||||||
|
import { followLink } from '../../../../../shared/utils/follow-link-config.model';
|
||||||
|
import { RemoteData } from '../../../../../core/data/remote-data';
|
||||||
|
import {
|
||||||
|
getAllSucceededRemoteData,
|
||||||
|
getFirstCompletedRemoteData,
|
||||||
|
getRemoteDataPayload
|
||||||
|
} from '../../../../../core/shared/operators';
|
||||||
|
import { Item } from '../../../../../core/shared/item.model';
|
||||||
|
import {
|
||||||
|
SearchResultListElementComponent
|
||||||
|
} from '../../../../../shared/object-list/search-result-list-element/search-result-list-element.component';
|
||||||
|
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
|
||||||
|
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
||||||
|
import { APP_CONFIG, AppConfig } from '../../../../../../config/app-config.interface';
|
||||||
|
import {
|
||||||
|
WorkspaceItemSearchResult
|
||||||
|
} from '../../../../../shared/object-collection/shared/workspace-item-search-result.model';
|
||||||
|
import { SupervisionOrder } from '../../../../../core/supervision-order/models/supervision-order.model';
|
||||||
|
import { SupervisionOrderDataService } from '../../../../../core/supervision-order/supervision-order-data.service';
|
||||||
|
import { PaginatedList } from '../../../../../core/data/paginated-list.model';
|
||||||
|
import { DSpaceObject } from '../../../../../core/shared/dspace-object.model';
|
||||||
|
|
||||||
|
@listableObjectComponent(WorkspaceItemSearchResult, ViewMode.ListElement, Context.AdminWorkflowSearch)
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-workflow-item-search-result-admin-workflow-list-element',
|
||||||
|
styleUrls: ['./workspace-item-search-result-admin-workflow-list-element.component.scss'],
|
||||||
|
templateUrl: './workspace-item-search-result-admin-workflow-list-element.component.html'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* The component for displaying a list element for a workflow item on the admin workflow search page
|
||||||
|
*/
|
||||||
|
export class WorkspaceItemSearchResultAdminWorkflowListElementComponent extends SearchResultListElementComponent<WorkspaceItemSearchResult, WorkspaceItem> implements OnInit {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The item linked to the workflow item
|
||||||
|
*/
|
||||||
|
public item$: Observable<Item>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The id of the item linked to the workflow item
|
||||||
|
*/
|
||||||
|
public itemId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The supervision orders linked to the workflow item
|
||||||
|
*/
|
||||||
|
public supervisionOrder$: BehaviorSubject<SupervisionOrder[]> = new BehaviorSubject<SupervisionOrder[]>([]);
|
||||||
|
|
||||||
|
constructor(private linkService: LinkService,
|
||||||
|
public dsoNameService: DSONameService,
|
||||||
|
protected supervisionOrderDataService: SupervisionOrderDataService,
|
||||||
|
protected truncatableService: TruncatableService,
|
||||||
|
@Inject(APP_CONFIG) protected appConfig: AppConfig
|
||||||
|
) {
|
||||||
|
super(truncatableService, dsoNameService, appConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the item object from the workflow item
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
super.ngOnInit();
|
||||||
|
this.dso = this.linkService.resolveLink(this.dso, followLink('item'));
|
||||||
|
this.item$ = (this.dso.item as Observable<RemoteData<Item>>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload());
|
||||||
|
|
||||||
|
this.item$.pipe(
|
||||||
|
take(1),
|
||||||
|
tap((item: Item) => this.itemId = item.id),
|
||||||
|
mergeMap((item: Item) => this.retrieveSupervisorOrders(item.id))
|
||||||
|
).subscribe((supervisionOrderList: SupervisionOrder[]) => {
|
||||||
|
this.supervisionOrder$.next(supervisionOrderList);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the list of SupervisionOrder object related to the given item
|
||||||
|
*
|
||||||
|
* @param itemId
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private retrieveSupervisorOrders(itemId): Observable<SupervisionOrder[]> {
|
||||||
|
return this.supervisionOrderDataService.searchByItem(
|
||||||
|
itemId, false, true, followLink('group')
|
||||||
|
).pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
map((soRD: RemoteData<PaginatedList<SupervisionOrder>>) => soRD.hasSucceeded && !soRD.hasNoContent ? soRD.payload.page : [])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reload list element after supervision order change.
|
||||||
|
*/
|
||||||
|
reloadObject(dso: DSpaceObject) {
|
||||||
|
this.reloadedObject.emit(dso);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -1,16 +1,39 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { SharedModule } from '../../shared/shared.module';
|
|
||||||
|
|
||||||
import { WorkflowItemSearchResultAdminWorkflowGridElementComponent } from './admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component';
|
import { SharedModule } from '../../shared/shared.module';
|
||||||
import { WorkflowItemAdminWorkflowActionsComponent } from './admin-workflow-search-results/workflow-item-admin-workflow-actions.component';
|
import {
|
||||||
import { WorkflowItemSearchResultAdminWorkflowListElementComponent } from './admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component';
|
WorkflowItemSearchResultAdminWorkflowGridElementComponent
|
||||||
|
} from './admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component';
|
||||||
|
import {
|
||||||
|
WorkflowItemAdminWorkflowActionsComponent
|
||||||
|
} from './admin-workflow-search-results/actions/workflow-item/workflow-item-admin-workflow-actions.component';
|
||||||
|
import {
|
||||||
|
WorkflowItemSearchResultAdminWorkflowListElementComponent
|
||||||
|
} from './admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component';
|
||||||
import { AdminWorkflowPageComponent } from './admin-workflow-page.component';
|
import { AdminWorkflowPageComponent } from './admin-workflow-page.component';
|
||||||
import { SearchModule } from '../../shared/search/search.module';
|
import { SearchModule } from '../../shared/search/search.module';
|
||||||
|
import {
|
||||||
|
WorkspaceItemAdminWorkflowActionsComponent
|
||||||
|
} from './admin-workflow-search-results/actions/workspace-item/workspace-item-admin-workflow-actions.component';
|
||||||
|
import {
|
||||||
|
WorkspaceItemSearchResultAdminWorkflowListElementComponent
|
||||||
|
} from './admin-workflow-search-results/admin-workflow-search-result-list-element/workspace-item/workspace-item-search-result-admin-workflow-list-element.component';
|
||||||
|
import {
|
||||||
|
WorkspaceItemSearchResultAdminWorkflowGridElementComponent
|
||||||
|
} from './admin-workflow-search-results/admin-workflow-search-result-grid-element/workspace-item/workspace-item-search-result-admin-workflow-grid-element.component';
|
||||||
|
import {
|
||||||
|
SupervisionOrderGroupSelectorComponent
|
||||||
|
} from './admin-workflow-search-results/actions/workspace-item/supervision-order-group-selector/supervision-order-group-selector.component';
|
||||||
|
import {
|
||||||
|
SupervisionOrderStatusComponent
|
||||||
|
} from './admin-workflow-search-results/actions/workspace-item/supervision-order-status/supervision-order-status.component';
|
||||||
|
|
||||||
const ENTRY_COMPONENTS = [
|
const ENTRY_COMPONENTS = [
|
||||||
// put only entry components that use custom decorator
|
// put only entry components that use custom decorator
|
||||||
WorkflowItemSearchResultAdminWorkflowListElementComponent,
|
WorkflowItemSearchResultAdminWorkflowListElementComponent,
|
||||||
WorkflowItemSearchResultAdminWorkflowGridElementComponent,
|
WorkflowItemSearchResultAdminWorkflowGridElementComponent,
|
||||||
|
WorkspaceItemSearchResultAdminWorkflowListElementComponent,
|
||||||
|
WorkspaceItemSearchResultAdminWorkflowGridElementComponent
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
@@ -20,7 +43,10 @@ const ENTRY_COMPONENTS = [
|
|||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
AdminWorkflowPageComponent,
|
AdminWorkflowPageComponent,
|
||||||
|
SupervisionOrderGroupSelectorComponent,
|
||||||
|
SupervisionOrderStatusComponent,
|
||||||
WorkflowItemAdminWorkflowActionsComponent,
|
WorkflowItemAdminWorkflowActionsComponent,
|
||||||
|
WorkspaceItemAdminWorkflowActionsComponent,
|
||||||
...ENTRY_COMPONENTS
|
...ENTRY_COMPONENTS
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
|
@@ -10,6 +10,7 @@ import { AdminSearchModule } from './admin-search-page/admin-search.module';
|
|||||||
import { AdminSidebarSectionComponent } from './admin-sidebar/admin-sidebar-section/admin-sidebar-section.component';
|
import { AdminSidebarSectionComponent } from './admin-sidebar/admin-sidebar-section/admin-sidebar-section.component';
|
||||||
import { ExpandableAdminSidebarSectionComponent } from './admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component';
|
import { ExpandableAdminSidebarSectionComponent } from './admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component';
|
||||||
import { BatchImportPageComponent } from './admin-import-batch-page/batch-import-page.component';
|
import { BatchImportPageComponent } from './admin-import-batch-page/batch-import-page.component';
|
||||||
|
import { UploadModule } from '../shared/upload/upload.module';
|
||||||
|
|
||||||
const ENTRY_COMPONENTS = [
|
const ENTRY_COMPONENTS = [
|
||||||
// put only entry components that use custom decorator
|
// put only entry components that use custom decorator
|
||||||
@@ -25,7 +26,8 @@ const ENTRY_COMPONENTS = [
|
|||||||
AccessControlModule,
|
AccessControlModule,
|
||||||
AdminSearchModule.withEntryComponents(),
|
AdminSearchModule.withEntryComponents(),
|
||||||
AdminWorkflowModuleModule.withEntryComponents(),
|
AdminWorkflowModuleModule.withEntryComponents(),
|
||||||
SharedModule
|
SharedModule,
|
||||||
|
UploadModule,
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
AdminCurationTasksComponent,
|
AdminCurationTasksComponent,
|
||||||
|
@@ -126,3 +126,9 @@ export function getRequestCopyModulePath() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const HEALTH_PAGE_PATH = 'health';
|
export const HEALTH_PAGE_PATH = 'health';
|
||||||
|
|
||||||
|
export const SUBSCRIPTIONS_MODULE_PATH = 'subscriptions';
|
||||||
|
|
||||||
|
export function getSubscriptionsModuleRoute() {
|
||||||
|
return `/${SUBSCRIPTIONS_MODULE_PATH}`;
|
||||||
|
}
|
||||||
|
@@ -230,6 +230,12 @@ import { ThemedPageErrorComponent } from './page-error/themed-page-error.compone
|
|||||||
loadChildren: () => import('./access-control/access-control.module').then((m) => m.AccessControlModule),
|
loadChildren: () => import('./access-control/access-control.module').then((m) => m.AccessControlModule),
|
||||||
canActivate: [GroupAdministratorGuard],
|
canActivate: [GroupAdministratorGuard],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'subscriptions',
|
||||||
|
loadChildren: () => import('./subscriptions-page/subscriptions-page-routing.module')
|
||||||
|
.then((m) => m.SubscriptionsPageRoutingModule),
|
||||||
|
canActivate: [AuthenticatedGuard]
|
||||||
|
},
|
||||||
{ path: '**', pathMatch: 'full', component: ThemedPageNotFoundComponent },
|
{ path: '**', pathMatch: 'full', component: ThemedPageNotFoundComponent },
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@@ -18,7 +18,7 @@ import { AngularticsProviderMock } from './shared/mocks/angulartics-provider.ser
|
|||||||
import { AuthServiceMock } from './shared/mocks/auth.service.mock';
|
import { AuthServiceMock } from './shared/mocks/auth.service.mock';
|
||||||
import { AuthService } from './core/auth/auth.service';
|
import { AuthService } from './core/auth/auth.service';
|
||||||
import { MenuService } from './shared/menu/menu.service';
|
import { MenuService } from './shared/menu/menu.service';
|
||||||
import { CSSVariableService } from './shared/sass-helper/sass-helper.service';
|
import { CSSVariableService } from './shared/sass-helper/css-variable.service';
|
||||||
import { CSSVariableServiceStub } from './shared/testing/css-variable-service.stub';
|
import { CSSVariableServiceStub } from './shared/testing/css-variable-service.stub';
|
||||||
import { MenuServiceStub } from './shared/testing/menu-service.stub';
|
import { MenuServiceStub } from './shared/testing/menu-service.stub';
|
||||||
import { HostWindowService } from './shared/host-window.service';
|
import { HostWindowService } from './shared/host-window.service';
|
||||||
|
@@ -25,13 +25,12 @@ import { HostWindowState } from './shared/search/host-window.reducer';
|
|||||||
import { NativeWindowRef, NativeWindowService } from './core/services/window.service';
|
import { NativeWindowRef, NativeWindowService } from './core/services/window.service';
|
||||||
import { isAuthenticationBlocking } from './core/auth/selectors';
|
import { isAuthenticationBlocking } from './core/auth/selectors';
|
||||||
import { AuthService } from './core/auth/auth.service';
|
import { AuthService } from './core/auth/auth.service';
|
||||||
import { CSSVariableService } from './shared/sass-helper/sass-helper.service';
|
import { CSSVariableService } from './shared/sass-helper/css-variable.service';
|
||||||
import { environment } from '../environments/environment';
|
import { environment } from '../environments/environment';
|
||||||
import { models } from './core/core.module';
|
import { models } from './core/core.module';
|
||||||
import { ThemeService } from './shared/theme-support/theme.service';
|
import { ThemeService } from './shared/theme-support/theme.service';
|
||||||
import { IdleModalComponent } from './shared/idle-modal/idle-modal.component';
|
import { IdleModalComponent } from './shared/idle-modal/idle-modal.component';
|
||||||
import { distinctNext } from './core/shared/distinct-next';
|
import { distinctNext } from './core/shared/distinct-next';
|
||||||
import { ModalBeforeDismiss } from './shared/interfaces/modal-before-dismiss.interface';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-app',
|
selector: 'ds-app',
|
||||||
@@ -110,18 +109,8 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private storeCSSVariables() {
|
private storeCSSVariables() {
|
||||||
this.cssService.addCSSVariable('xlMin', '1200px');
|
this.cssService.clearCSSVariables();
|
||||||
this.cssService.addCSSVariable('mdMin', '768px');
|
this.cssService.addCSSVariables(this.cssService.getCSSVariablesFromStylesheets(this.document));
|
||||||
this.cssService.addCSSVariable('lgMin', '576px');
|
|
||||||
this.cssService.addCSSVariable('smMin', '0');
|
|
||||||
this.cssService.addCSSVariable('adminSidebarActiveBg', '#0f1b28');
|
|
||||||
this.cssService.addCSSVariable('sidebarItemsWidth', '250px');
|
|
||||||
this.cssService.addCSSVariable('collapsedSidebarWidth', '53.234px');
|
|
||||||
this.cssService.addCSSVariable('totalSidebarWidth', '303.234px');
|
|
||||||
// const vars = variables.locals || {};
|
|
||||||
// Object.keys(vars).forEach((name: string) => {
|
|
||||||
// this.cssService.addCSSVariable(name, vars[name]);
|
|
||||||
// })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngAfterViewInit() {
|
ngAfterViewInit() {
|
||||||
|
@@ -1,16 +1,16 @@
|
|||||||
import { APP_BASE_HREF, CommonModule, DOCUMENT } from '@angular/common';
|
import { APP_BASE_HREF, CommonModule, DOCUMENT } from '@angular/common';
|
||||||
import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
|
import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { AbstractControl } from '@angular/forms';
|
|
||||||
import { BrowserModule } from '@angular/platform-browser';
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
|
|
||||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { EffectsModule } from '@ngrx/effects';
|
import { EffectsModule } from '@ngrx/effects';
|
||||||
import { RouterStateSerializer, StoreRouterConnectingModule } from '@ngrx/router-store';
|
import { RouterStateSerializer, StoreRouterConnectingModule } from '@ngrx/router-store';
|
||||||
import { MetaReducer, StoreModule, USER_PROVIDED_META_REDUCERS } from '@ngrx/store';
|
import { MetaReducer, StoreModule, USER_PROVIDED_META_REDUCERS } from '@ngrx/store';
|
||||||
import { DYNAMIC_ERROR_MESSAGES_MATCHER, DYNAMIC_MATCHER_PROVIDERS, DynamicErrorMessagesMatcher } from '@ng-dynamic-forms/core';
|
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { ScrollToModule } from '@nicky-lenaers/ngx-scroll-to';
|
import { ScrollToModule } from '@nicky-lenaers/ngx-scroll-to';
|
||||||
|
import { DYNAMIC_MATCHER_PROVIDERS } from '@ng-dynamic-forms/core';
|
||||||
|
|
||||||
import { AppRoutingModule } from './app-routing.module';
|
import { AppRoutingModule } from './app-routing.module';
|
||||||
import { AppComponent } from './app.component';
|
import { AppComponent } from './app.component';
|
||||||
import { appEffects } from './app.effects';
|
import { appEffects } from './app.effects';
|
||||||
@@ -28,7 +28,6 @@ import { XsrfInterceptor } from './core/xsrf/xsrf.interceptor';
|
|||||||
import { LogInterceptor } from './core/log/log.interceptor';
|
import { LogInterceptor } from './core/log/log.interceptor';
|
||||||
import { EagerThemesModule } from '../themes/eager-themes.module';
|
import { EagerThemesModule } from '../themes/eager-themes.module';
|
||||||
import { APP_CONFIG, AppConfig } from '../config/app-config.interface';
|
import { APP_CONFIG, AppConfig } from '../config/app-config.interface';
|
||||||
import { NgxMaskModule } from 'ngx-mask';
|
|
||||||
import { StoreDevModules } from '../config/store/devtools';
|
import { StoreDevModules } from '../config/store/devtools';
|
||||||
import { RootModule } from './root.module';
|
import { RootModule } from './root.module';
|
||||||
|
|
||||||
@@ -46,14 +45,6 @@ export function getMetaReducers(appConfig: AppConfig): MetaReducer<AppState>[] {
|
|||||||
return appConfig.debug ? [...appMetaReducers, ...debugMetaReducers] : appMetaReducers;
|
return appConfig.debug ? [...appMetaReducers, ...debugMetaReducers] : appMetaReducers;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Condition for displaying error messages on email form field
|
|
||||||
*/
|
|
||||||
export const ValidateEmailErrorStateMatcher: DynamicErrorMessagesMatcher =
|
|
||||||
(control: AbstractControl, model: any, hasFocus: boolean) => {
|
|
||||||
return (control.touched && !hasFocus) || (control.errors?.emailTaken && hasFocus);
|
|
||||||
};
|
|
||||||
|
|
||||||
const IMPORTS = [
|
const IMPORTS = [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
SharedModule,
|
SharedModule,
|
||||||
@@ -64,7 +55,6 @@ const IMPORTS = [
|
|||||||
ScrollToModule.forRoot(),
|
ScrollToModule.forRoot(),
|
||||||
NgbModule,
|
NgbModule,
|
||||||
TranslateModule.forRoot(),
|
TranslateModule.forRoot(),
|
||||||
NgxMaskModule.forRoot(),
|
|
||||||
EffectsModule.forRoot(appEffects),
|
EffectsModule.forRoot(appEffects),
|
||||||
StoreModule.forRoot(appReducers, storeModuleConfig),
|
StoreModule.forRoot(appReducers, storeModuleConfig),
|
||||||
StoreRouterConnectingModule.forRoot(),
|
StoreRouterConnectingModule.forRoot(),
|
||||||
@@ -113,10 +103,7 @@ const PROVIDERS = [
|
|||||||
useClass: LogInterceptor,
|
useClass: LogInterceptor,
|
||||||
multi: true
|
multi: true
|
||||||
},
|
},
|
||||||
{
|
// register the dynamic matcher used by form. MUST be provided by the app module
|
||||||
provide: DYNAMIC_ERROR_MESSAGES_MATCHER,
|
|
||||||
useValue: ValidateEmailErrorStateMatcher
|
|
||||||
},
|
|
||||||
...DYNAMIC_MATCHER_PROVIDERS,
|
...DYNAMIC_MATCHER_PROVIDERS,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import * as fromRouter from '@ngrx/router-store';
|
import { routerReducer, RouterReducerState } from '@ngrx/router-store';
|
||||||
import { ActionReducerMap, createSelector, MemoizedSelector } from '@ngrx/store';
|
import { ActionReducerMap, createSelector, MemoizedSelector } from '@ngrx/store';
|
||||||
import {
|
import {
|
||||||
ePeopleRegistryReducer,
|
ePeopleRegistryReducer,
|
||||||
@@ -35,31 +35,27 @@ import {
|
|||||||
ObjectSelectionListState,
|
ObjectSelectionListState,
|
||||||
objectSelectionReducer
|
objectSelectionReducer
|
||||||
} from './shared/object-select/object-select.reducer';
|
} from './shared/object-select/object-select.reducer';
|
||||||
import { cssVariablesReducer, CSSVariablesState } from './shared/sass-helper/sass-helper.reducer';
|
import { cssVariablesReducer, CSSVariablesState } from './shared/sass-helper/css-variable.reducer';
|
||||||
|
|
||||||
import { hostWindowReducer, HostWindowState } from './shared/search/host-window.reducer';
|
import { hostWindowReducer, HostWindowState } from './shared/search/host-window.reducer';
|
||||||
import {
|
import {
|
||||||
filterReducer,
|
filterReducer,
|
||||||
SearchFiltersState
|
SearchFiltersState
|
||||||
} from './shared/search/search-filters/search-filter/search-filter.reducer';
|
} from './shared/search/search-filters/search-filter/search-filter.reducer';
|
||||||
import {
|
|
||||||
sidebarFilterReducer,
|
|
||||||
SidebarFiltersState
|
|
||||||
} from './shared/sidebar/filter/sidebar-filter.reducer';
|
|
||||||
import { sidebarReducer, SidebarState } from './shared/sidebar/sidebar.reducer';
|
import { sidebarReducer, SidebarState } from './shared/sidebar/sidebar.reducer';
|
||||||
import { truncatableReducer, TruncatablesState } from './shared/truncatable/truncatable.reducer';
|
import { truncatableReducer, TruncatablesState } from './shared/truncatable/truncatable.reducer';
|
||||||
import { ThemeState, themeReducer } from './shared/theme-support/theme.reducer';
|
import { ThemeState, themeReducer } from './shared/theme-support/theme.reducer';
|
||||||
import { MenusState } from './shared/menu/menus-state.model';
|
import { MenusState } from './shared/menu/menus-state.model';
|
||||||
import { correlationIdReducer } from './correlation-id/correlation-id.reducer';
|
import { correlationIdReducer } from './correlation-id/correlation-id.reducer';
|
||||||
|
import { contextHelpReducer, ContextHelpState } from './shared/context-help.reducer';
|
||||||
|
|
||||||
export interface AppState {
|
export interface AppState {
|
||||||
router: fromRouter.RouterReducerState;
|
router: RouterReducerState;
|
||||||
hostWindow: HostWindowState;
|
hostWindow: HostWindowState;
|
||||||
forms: FormState;
|
forms: FormState;
|
||||||
metadataRegistry: MetadataRegistryState;
|
metadataRegistry: MetadataRegistryState;
|
||||||
notifications: NotificationsState;
|
notifications: NotificationsState;
|
||||||
sidebar: SidebarState;
|
sidebar: SidebarState;
|
||||||
sidebarFilter: SidebarFiltersState;
|
|
||||||
searchFilter: SearchFiltersState;
|
searchFilter: SearchFiltersState;
|
||||||
truncatable: TruncatablesState;
|
truncatable: TruncatablesState;
|
||||||
cssVariables: CSSVariablesState;
|
cssVariables: CSSVariablesState;
|
||||||
@@ -72,16 +68,16 @@ export interface AppState {
|
|||||||
epeopleRegistry: EPeopleRegistryState;
|
epeopleRegistry: EPeopleRegistryState;
|
||||||
groupRegistry: GroupRegistryState;
|
groupRegistry: GroupRegistryState;
|
||||||
correlationId: string;
|
correlationId: string;
|
||||||
|
contextHelp: ContextHelpState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const appReducers: ActionReducerMap<AppState> = {
|
export const appReducers: ActionReducerMap<AppState> = {
|
||||||
router: fromRouter.routerReducer,
|
router: routerReducer,
|
||||||
hostWindow: hostWindowReducer,
|
hostWindow: hostWindowReducer,
|
||||||
forms: formReducer,
|
forms: formReducer,
|
||||||
metadataRegistry: metadataRegistryReducer,
|
metadataRegistry: metadataRegistryReducer,
|
||||||
notifications: notificationsReducer,
|
notifications: notificationsReducer,
|
||||||
sidebar: sidebarReducer,
|
sidebar: sidebarReducer,
|
||||||
sidebarFilter: sidebarFilterReducer,
|
|
||||||
searchFilter: filterReducer,
|
searchFilter: filterReducer,
|
||||||
truncatable: truncatableReducer,
|
truncatable: truncatableReducer,
|
||||||
cssVariables: cssVariablesReducer,
|
cssVariables: cssVariablesReducer,
|
||||||
@@ -93,7 +89,8 @@ export const appReducers: ActionReducerMap<AppState> = {
|
|||||||
communityList: CommunityListReducer,
|
communityList: CommunityListReducer,
|
||||||
epeopleRegistry: ePeopleRegistryReducer,
|
epeopleRegistry: ePeopleRegistryReducer,
|
||||||
groupRegistry: groupRegistryReducer,
|
groupRegistry: groupRegistryReducer,
|
||||||
correlationId: correlationIdReducer
|
correlationId: correlationIdReducer,
|
||||||
|
contextHelp: contextHelpReducer,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const routerStateSelector = (state: AppState) => state.router;
|
export const routerStateSelector = (state: AppState) => state.router;
|
||||||
|
@@ -6,7 +6,7 @@ import { Bitstream } from '../../core/shared/bitstream.model';
|
|||||||
import { BitstreamDownloadPageComponent } from './bitstream-download-page.component';
|
import { BitstreamDownloadPageComponent } from './bitstream-download-page.component';
|
||||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||||
import { HardRedirectService } from '../../core/services/hard-redirect.service';
|
import { HardRedirectService } from '../../core/services/hard-redirect.service';
|
||||||
import { createSuccessfulRemoteDataObject } from '../remote-data.utils';
|
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { getForbiddenRoute } from '../../app-routing-paths';
|
import { getForbiddenRoute } from '../../app-routing-paths';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
@@ -1,7 +1,7 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { filter, map, switchMap, take } from 'rxjs/operators';
|
import { filter, map, switchMap, take } from 'rxjs/operators';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { hasValue, isNotEmpty } from '../empty.util';
|
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||||
import { getRemoteDataPayload} from '../../core/shared/operators';
|
import { getRemoteDataPayload} from '../../core/shared/operators';
|
||||||
import { Bitstream } from '../../core/shared/bitstream.model';
|
import { Bitstream } from '../../core/shared/bitstream.model';
|
||||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
@@ -3,7 +3,7 @@ import { RouterModule } from '@angular/router';
|
|||||||
import { EditBitstreamPageComponent } from './edit-bitstream-page/edit-bitstream-page.component';
|
import { EditBitstreamPageComponent } from './edit-bitstream-page/edit-bitstream-page.component';
|
||||||
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
|
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
|
||||||
import { BitstreamPageResolver } from './bitstream-page.resolver';
|
import { BitstreamPageResolver } from './bitstream-page.resolver';
|
||||||
import { BitstreamDownloadPageComponent } from '../shared/bitstream-download-page/bitstream-download-page.component';
|
import { BitstreamDownloadPageComponent } from './bitstream-download-page/bitstream-download-page.component';
|
||||||
import { ResourcePolicyTargetResolver } from '../shared/resource-policies/resolvers/resource-policy-target.resolver';
|
import { ResourcePolicyTargetResolver } from '../shared/resource-policies/resolvers/resource-policy-target.resolver';
|
||||||
import { ResourcePolicyCreateComponent } from '../shared/resource-policies/create/resource-policy-create.component';
|
import { ResourcePolicyCreateComponent } from '../shared/resource-policies/create/resource-policy-create.component';
|
||||||
import { ResourcePolicyResolver } from '../shared/resource-policies/resolvers/resource-policy.resolver';
|
import { ResourcePolicyResolver } from '../shared/resource-policies/resolvers/resource-policy.resolver';
|
||||||
|
@@ -6,6 +6,7 @@ import { BitstreamPageRoutingModule } from './bitstream-page-routing.module';
|
|||||||
import { BitstreamAuthorizationsComponent } from './bitstream-authorizations/bitstream-authorizations.component';
|
import { BitstreamAuthorizationsComponent } from './bitstream-authorizations/bitstream-authorizations.component';
|
||||||
import { FormModule } from '../shared/form/form.module';
|
import { FormModule } from '../shared/form/form.module';
|
||||||
import { ResourcePoliciesModule } from '../shared/resource-policies/resource-policies.module';
|
import { ResourcePoliciesModule } from '../shared/resource-policies/resource-policies.module';
|
||||||
|
import { BitstreamDownloadPageComponent } from './bitstream-download-page/bitstream-download-page.component';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This module handles all components that are necessary for Bitstream related pages
|
* This module handles all components that are necessary for Bitstream related pages
|
||||||
@@ -20,7 +21,8 @@ import { ResourcePoliciesModule } from '../shared/resource-policies/resource-pol
|
|||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
BitstreamAuthorizationsComponent,
|
BitstreamAuthorizationsComponent,
|
||||||
EditBitstreamPageComponent
|
EditBitstreamPageComponent,
|
||||||
|
BitstreamDownloadPageComponent,
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class BitstreamPageModule {
|
export class BitstreamPageModule {
|
||||||
|
@@ -26,7 +26,7 @@ import {
|
|||||||
import { FormGroup } from '@angular/forms';
|
import { FormGroup } from '@angular/forms';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { DynamicCustomSwitchModel } from '../../shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.model';
|
import { DynamicCustomSwitchModel } from '../../shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.model';
|
||||||
import { cloneDeep } from 'lodash';
|
import cloneDeep from 'lodash/cloneDeep';
|
||||||
import { BitstreamDataService } from '../../core/data/bitstream-data.service';
|
import { BitstreamDataService } from '../../core/data/bitstream-data.service';
|
||||||
import {
|
import {
|
||||||
getAllSucceededRemoteDataPayload,
|
getAllSucceededRemoteDataPayload,
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { LegacyBitstreamUrlResolver } from './legacy-bitstream-url.resolver';
|
import { LegacyBitstreamUrlResolver } from './legacy-bitstream-url.resolver';
|
||||||
import { of as observableOf, EMPTY } from 'rxjs';
|
import { EMPTY } from 'rxjs';
|
||||||
import { BitstreamDataService } from '../core/data/bitstream-data.service';
|
import { BitstreamDataService } from '../core/data/bitstream-data.service';
|
||||||
import { RemoteData } from '../core/data/remote-data';
|
import { RemoteData } from '../core/data/remote-data';
|
||||||
import { TestScheduler } from 'rxjs/testing';
|
import { TestScheduler } from 'rxjs/testing';
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user