diff --git a/.browserslistrc b/.browserslistrc deleted file mode 100644 index 427441dc930..00000000000 --- a/.browserslistrc +++ /dev/null @@ -1,17 +0,0 @@ -# This file is used by the build system to adjust CSS and JS output to support the specified browsers below. -# For additional information regarding the format and rule options, please see: -# https://github.com/browserslist/browserslist#queries - -# For the full list of supported browsers by the Angular framework, please see: -# https://angular.io/guide/browser-support - -# You can see what browsers were selected by your queries by running: -# npx browserslist - -last 1 Chrome version -last 1 Firefox version -last 2 Edge major versions -last 2 Safari major versions -last 2 iOS major versions -Firefox ESR -not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line. diff --git a/.editorconfig b/.editorconfig index 15d4c87b142..590d1dea081 100644 --- a/.editorconfig +++ b/.editorconfig @@ -15,3 +15,6 @@ trim_trailing_whitespace = false [*.ts] quote_type = single + +[*.json5] +ij_json_keep_blank_lines_in_code = 3 diff --git a/.eslintrc.json b/.eslintrc.json index b95b54b979a..af1b97849b6 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -7,7 +7,8 @@ "eslint-plugin-jsdoc", "eslint-plugin-deprecation", "unused-imports", - "eslint-plugin-lodash" + "eslint-plugin-lodash", + "eslint-plugin-jsonc" ], "overrides": [ { @@ -224,6 +225,42 @@ "@angular-eslint/template/no-negated-async": "off", "@angular-eslint/template/eqeqeq": "off" } + }, + { + "files": [ + "*.json5" + ], + "extends": [ + "plugin:jsonc/recommended-with-jsonc" + ], + "rules": { + "no-irregular-whitespace": "error", + "no-trailing-spaces": "error", + "jsonc/comma-dangle": [ + "error", + "always-multiline" + ], + "jsonc/indent": [ + "error", + 2 + ], + "jsonc/key-spacing": [ + "error", + { + "beforeColon": false, + "afterColon": true, + "mode": "strict" + } + ], + "jsonc/no-dupe-keys": "off", + "jsonc/quotes": [ + "error", + "double", + { + "avoidEscape": false + } + ] + } } ] } diff --git a/.github/disabled-workflows/pull_request_opened.yml b/.github/disabled-workflows/pull_request_opened.yml deleted file mode 100644 index 0dc718c0b9a..00000000000 --- a/.github/disabled-workflows/pull_request_opened.yml +++ /dev/null @@ -1,26 +0,0 @@ -# This workflow runs whenever a new pull request is created -# TEMPORARILY DISABLED. Unfortunately this doesn't work for PRs created from forked repositories (which is how we tend to create PRs). -# There is no known workaround yet. See https://github.community/t/how-to-use-github-token-for-prs-from-forks/16818 -name: Pull Request opened - -# Only run for newly opened PRs against the "main" branch -on: - pull_request: - types: [opened] - branches: - - main - -jobs: - automation: - runs-on: ubuntu-latest - steps: - # Assign the PR to whomever created it. This is useful for visualizing assignments on project boards - # See https://github.com/marketplace/actions/pull-request-assigner - - name: Assign PR to creator - uses: thomaseizinger/assign-pr-creator-action@v1.0.0 - # Note, this authentication token is created automatically - # See: https://docs.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - # Ignore errors. It is possible the PR was created by someone who cannot be assigned - continue-on-error: true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dabc0b428e8..c50cb80b695 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,15 +15,24 @@ jobs: env: # 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. + # NOTE: These settings should be kept in sync with those in [src]/docker/docker-compose-ci.yml DSPACE_REST_HOST: 127.0.0.1 DSPACE_REST_PORT: 8080 DSPACE_REST_NAMESPACE: '/server' 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 + DSPACE_UI_PORT: 4000 + # Ensure all SSR caching is disabled in test environment + DSPACE_CACHE_SERVERSIDE_BOTCACHE_MAX: 0 + DSPACE_CACHE_SERVERSIDE_ANONYMOUSCACHE_MAX: 0 + # Tell Cypress to run e2e tests using the same UI URL + CYPRESS_BASE_URL: http://127.0.0.1:4000 # When Chrome version is specified, we pin to a specific version of Chrome # Comment this out to use the latest release CHROME_VERSION: "116.0.5845.187-1" + # Bump Node heap size (OOM in CI after upgrading to Angular 15) + NODE_OPTIONS: '--max-old-space-size=4096' strategy: # Create a matrix of Node versions to test against (in parallel) matrix: @@ -61,7 +70,7 @@ jobs: # https://github.com/actions/cache/blob/main/examples.md#node---yarn - name: Get Yarn cache directory id: yarn-cache-dir-path - run: echo "::set-output name=dir::$(yarn cache dir)" + run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT - name: Cache Yarn dependencies uses: actions/cache@v3 with: @@ -86,12 +95,16 @@ jobs: - name: Run specs (unit tests) run: yarn run test:headless + # Upload code coverage report to artifact (for one version of Node only), + # so that it can be shared with the 'codecov' job (see below) # NOTE: Angular CLI only supports code coverage for specs. See https://github.com/angular/angular-cli/issues/6286 - # Upload coverage reports to Codecov (for one version of Node only) - # https://github.com/codecov/codecov-action - - name: Upload coverage to Codecov.io - uses: codecov/codecov-action@v3 - if: matrix.node-version == '16.x' + - name: Upload code coverage report to Artifact + uses: actions/upload-artifact@v3 + if: matrix.node-version == '18.x' + with: + name: dspace-angular coverage report + path: 'coverage/dspace-angular/lcov.info' + retention-days: 14 # Using docker-compose start backend using CI configuration # and load assetstore from a cached copy @@ -105,11 +118,10 @@ jobs: # https://github.com/cypress-io/github-action # (NOTE: to run these e2e tests locally, just use 'ng e2e') - name: Run e2e tests (integration tests) - uses: cypress-io/github-action@v4 + uses: cypress-io/github-action@v5 with: - # Run tests in Chrome, headless mode + # Run tests in Chrome, headless mode (default) browser: chrome - headless: true # Start app before running tests (will be stopped automatically after tests finish) start: yarn run serve:ssr # Wait for backend & frontend to be available @@ -169,3 +181,32 @@ jobs: - name: Shutdown Docker containers run: docker-compose -f ./docker/docker-compose-ci.yml down + + # Codecov upload is a separate job in order to allow us to restart this separate from the entire build/test + # job above. This is necessary because Codecov uploads seem to randomly fail at times. + # See https://community.codecov.com/t/upload-issues-unable-to-locate-build-via-github-actions-api/3954 + codecov: + # Must run after 'tests' job above + needs: tests + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + # Download artifacts from previous 'tests' job + - name: Download coverage artifacts + uses: actions/download-artifact@v3 + + # Now attempt upload to Codecov using its action. + # NOTE: We use a retry action to retry the Codecov upload if it fails the first time. + # + # Retry action: https://github.com/marketplace/actions/retry-action + # Codecov action: https://github.com/codecov/codecov-action + - name: Upload coverage to Codecov.io + uses: Wandalen/wretry.action@v1.0.36 + with: + action: codecov/codecov-action@v3 + # Try upload 5 times max + attempt_limit: 5 + # Run again in 30 seconds + attempt_delay: 30000 diff --git a/.github/workflows/codescan.yml b/.github/workflows/codescan.yml index 35a2e2d24aa..8b415296c71 100644 --- a/.github/workflows/codescan.yml +++ b/.github/workflows/codescan.yml @@ -5,12 +5,16 @@ # 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. +# Run this code scan for all pushes / PRs to main or maintenance branches. Also run once a week. on: push: - branches: [ main ] + branches: + - main + - 'dspace-**' pull_request: - branches: [ main ] + branches: + - main + - 'dspace-**' # Don't run if PR is only updating static documentation paths-ignore: - '**/*.md' diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 908c5c34fdc..0c36d5af987 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -15,29 +15,35 @@ on: permissions: contents: read # to fetch code (actions/checkout) + +env: + # Define tags to use for Docker images based on Git tags/branches (for docker/metadata-action) + # For a new commit on default branch (main), use the literal tag 'latest' on Docker image. + # For a new commit on other branches, use the branch name as the tag for Docker image. + # For a new tag, copy that tag name as the tag for Docker image. + IMAGE_TAGS: | + type=raw,value=latest,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }} + type=ref,event=branch,enable=${{ !endsWith(github.ref, github.event.repository.default_branch) }} + type=ref,event=tag + # Define default tag "flavor" for docker/metadata-action per + # https://github.com/docker/metadata-action#flavor-input + # We manage the 'latest' tag ourselves to the 'main' branch (see settings above) + TAGS_FLAVOR: | + latest=false + # Architectures / Platforms for which we will build Docker images + # If this is a PR, we ONLY build for AMD64. For PRs we only do a sanity check test to ensure Docker builds work. + # If this is NOT a PR (e.g. a tag or merge commit), also build for ARM64. + PLATFORMS: linux/amd64${{ github.event_name != 'pull_request' && ', linux/arm64' || '' }} + + jobs: - docker: + ############################################### + # Build/Push the 'dspace/dspace-angular' image + ############################################### + dspace-angular: # Ensure this job never runs on forked repos. It's only executed for 'dspace/dspace-angular' if: github.repository == 'dspace/dspace-angular' runs-on: ubuntu-latest - env: - # Define tags to use for Docker images based on Git tags/branches (for docker/metadata-action) - # For a new commit on default branch (main), use the literal tag 'dspace-7_x' on Docker image. - # For a new commit on other branches, use the branch name as the tag for Docker image. - # For a new tag, copy that tag name as the tag for Docker image. - IMAGE_TAGS: | - type=raw,value=dspace-7_x,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }} - type=ref,event=branch,enable=${{ !endsWith(github.ref, github.event.repository.default_branch) }} - type=ref,event=tag - # Define default tag "flavor" for docker/metadata-action per - # https://github.com/docker/metadata-action#flavor-input - # We turn off 'latest' tag by default. - TAGS_FLAVOR: | - latest=false - # Architectures / Platforms for which we will build Docker images - # If this is a PR, we ONLY build for AMD64. For PRs we only do a sanity check test to ensure Docker builds work. - # If this is NOT a PR (e.g. a tag or merge commit), also build for ARM64. - PLATFORMS: linux/amd64${{ github.event_name != 'pull_request' && ', linux/arm64' || '' }} steps: # https://github.com/actions/checkout @@ -61,9 +67,6 @@ jobs: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_ACCESS_TOKEN }} - ############################################### - # Build/Push the 'dspace/dspace-angular' image - ############################################### # https://github.com/docker/metadata-action # Get Metadata for docker_build step below - name: Sync metadata (tags, labels) from GitHub to Docker for 'dspace-angular' image @@ -77,7 +80,7 @@ jobs: # https://github.com/docker/build-push-action - name: Build and push 'dspace-angular' image id: docker_build - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v4 with: context: . file: ./Dockerfile @@ -88,3 +91,60 @@ jobs: # Use tags / labels provided by 'docker/metadata-action' above tags: ${{ steps.meta_build.outputs.tags }} labels: ${{ steps.meta_build.outputs.labels }} + + ############################################################# + # Build/Push the 'dspace/dspace-angular' image ('-dist' tag) + ############################################################# + dspace-angular-dist: + # Ensure this job never runs on forked repos. It's only executed for 'dspace/dspace-angular' + if: github.repository == 'dspace/dspace-angular' + runs-on: ubuntu-latest + + steps: + # https://github.com/actions/checkout + - name: Checkout codebase + uses: actions/checkout@v3 + + # https://github.com/docker/setup-buildx-action + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v2 + + # https://github.com/docker/setup-qemu-action + - name: Set up QEMU emulation to build for multiple architectures + uses: docker/setup-qemu-action@v2 + + # https://github.com/docker/login-action + - name: Login to DockerHub + # Only login if not a PR, as PRs only trigger a Docker build and not a push + if: github.event_name != 'pull_request' + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_ACCESS_TOKEN }} + + # https://github.com/docker/metadata-action + # Get Metadata for docker_build_dist step below + - name: Sync metadata (tags, labels) from GitHub to Docker for 'dspace-angular-dist' image + id: meta_build_dist + uses: docker/metadata-action@v4 + with: + images: dspace/dspace-angular + tags: ${{ env.IMAGE_TAGS }} + # As this is a "dist" image, its tags are all suffixed with "-dist". Otherwise, it uses the same + # tagging logic as the primary 'dspace/dspace-angular' image above. + flavor: ${{ env.TAGS_FLAVOR }} + suffix=-dist + + - name: Build and push 'dspace-angular-dist' image + id: docker_build_dist + uses: docker/build-push-action@v4 + with: + context: . + file: ./Dockerfile.dist + platforms: ${{ env.PLATFORMS }} + # For pull requests, we run the Docker build (to ensure no PR changes break the build), + # but we ONLY do an image push to DockerHub if it's NOT a PR + push: ${{ github.event_name != 'pull_request' }} + # Use tags / labels provided by 'docker/metadata-action' above + tags: ${{ steps.meta_build_dist.outputs.tags }} + labels: ${{ steps.meta_build_dist.outputs.labels }} diff --git a/.github/workflows/issue_opened.yml b/.github/workflows/issue_opened.yml index 5d7c1c30f7d..b4436dca3aa 100644 --- a/.github/workflows/issue_opened.yml +++ b/.github/workflows/issue_opened.yml @@ -16,7 +16,7 @@ jobs: # 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 if: (contains(github.event.issue.labels.*.name, 'needs triage') || join(github.event.issue.labels.*.name) == '') - uses: actions/add-to-project@v0.3.0 + uses: actions/add-to-project@v0.5.0 # Note, the authentication token below is an ORG level Secret. # 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 diff --git a/.github/workflows/label_merge_conflicts.yml b/.github/workflows/label_merge_conflicts.yml index a840a4fd171..ccc6c401c0b 100644 --- a/.github/workflows/label_merge_conflicts.yml +++ b/.github/workflows/label_merge_conflicts.yml @@ -1,11 +1,12 @@ # This workflow checks open PRs for merge conflicts and labels them when conflicts are found name: Check for merge conflicts -# Run whenever the "main" branch is updated -# NOTE: This means merge conflicts are only checked for when a PR is merged to main. +# Run this for all pushes (i.e. merges) to 'main' or maintenance branches on: push: - branches: [ main ] + branches: + - main + - 'dspace-**' # 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: @@ -23,7 +24,9 @@ jobs: steps: # See: https://github.com/prince-chrismc/label-merge-conflicts-action - name: Auto-label PRs with merge conflicts - uses: prince-chrismc/label-merge-conflicts-action@v2 + uses: prince-chrismc/label-merge-conflicts-action@v3 + # Ignore any failures -- may occur (randomly?) for older, outdated PRs. + continue-on-error: true # Add "merge conflict" label if a merge conflict is detected. Remove it when resolved. # Note, the authentication token is created automatically # See: https://docs.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token diff --git a/.github/workflows/port_merged_pull_request.yml b/.github/workflows/port_merged_pull_request.yml new file mode 100644 index 00000000000..109835d14d3 --- /dev/null +++ b/.github/workflows/port_merged_pull_request.yml @@ -0,0 +1,46 @@ +# This workflow will attempt to port a merged pull request to +# the branch specified in a "port to" label (if exists) +name: Port merged Pull Request + +# Only run for merged PRs against the "main" or maintenance branches +# We allow this to run for `pull_request_target` so that github secrets are available +# (This is required when the PR comes from a forked repo) +on: + pull_request_target: + types: [ closed ] + branches: + - main + - 'dspace-**' + +permissions: + contents: write # so action can add comments + pull-requests: write # so action can create pull requests + +jobs: + port_pr: + runs-on: ubuntu-latest + # Don't run on closed *unmerged* pull requests + if: github.event.pull_request.merged + steps: + # Checkout code + - uses: actions/checkout@v3 + # Port PR to other branch (ONLY if labeled with "port to") + # See https://github.com/korthout/backport-action + - name: Create backport pull requests + uses: korthout/backport-action@v1 + with: + # Trigger based on a "port to [branch]" label on PR + # (This label must specify the branch name to port to) + label_pattern: '^port to ([^ ]+)$' + # Title to add to the (newly created) port PR + pull_title: '[Port ${target_branch}] ${pull_title}' + # Description to add to the (newly created) port PR + pull_description: 'Port of #${pull_number} by @${pull_author} to `${target_branch}`.' + # Copy all labels from original PR to (newly created) port PR + # NOTE: The labels matching 'label_pattern' are automatically excluded + copy_labels_pattern: '.*' + # Skip any merge commits in the ported PR. This means only non-merge commits are cherry-picked to the new PR + merge_commits: 'skip' + # Use a personal access token (PAT) to create PR as 'dspace-bot' user. + # A PAT is required in order for the new PR to trigger its own actions (for CI checks) + github_token: ${{ secrets.PR_PORT_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/pull_request_opened.yml b/.github/workflows/pull_request_opened.yml new file mode 100644 index 00000000000..9b61af72d18 --- /dev/null +++ b/.github/workflows/pull_request_opened.yml @@ -0,0 +1,24 @@ +# This workflow runs whenever a new pull request is created +name: Pull Request opened + +# Only run for newly opened PRs against the "main" or maintenance branches +# We allow this to run for `pull_request_target` so that github secrets are available +# (This is required to assign a PR back to the creator when the PR comes from a forked repo) +on: + pull_request_target: + types: [ opened ] + branches: + - main + - 'dspace-**' + +permissions: + pull-requests: write + +jobs: + automation: + runs-on: ubuntu-latest + steps: + # Assign the PR to whomever created it. This is useful for visualizing assignments on project boards + # See https://github.com/toshimaru/auto-author-assign + - name: Assign PR to creator + uses: toshimaru/auto-author-assign@v1.6.2 diff --git a/.gitignore b/.gitignore index bdd0d4e5895..7d065aca061 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,5 @@ package-lock.json /nbproject/ junit.xml + +/src/mirador-viewer/config.local.js diff --git a/Dockerfile b/Dockerfile index 61d960e7d3b..8fac7495e1f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,20 +2,27 @@ # See https://github.com/DSpace/dspace-angular/tree/main/docker for usage details FROM node:18-alpine -WORKDIR /app -ADD . /app/ -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/* +WORKDIR /app +ADD . /app/ +EXPOSE 4000 + # 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 RUN yarn install --network-timeout 300000 +# When running in dev mode, 4GB of memory is required to build & launch the app. +# This default setting can be overridden as needed in your shell, via an env file or in docker-compose. +# See Docker environment var precedence: https://docs.docker.com/compose/environment-variables/envvars-precedence/ +ENV NODE_OPTIONS="--max_old_space_size=4096" + # On startup, run in DEVELOPMENT mode (this defaults to live reloading enabled, etc). # Listen / accept connections from all IP addresses. # NOTE: At this time it is only possible to run Docker container in Production mode -# if you have a public IP. See https://github.com/DSpace/dspace-angular/issues/1485 +# if you have a public URL. See https://github.com/DSpace/dspace-angular/issues/1485 +ENV NODE_ENV development CMD yarn serve --host 0.0.0.0 diff --git a/Dockerfile.dist b/Dockerfile.dist new file mode 100644 index 00000000000..2a6a66fc063 --- /dev/null +++ b/Dockerfile.dist @@ -0,0 +1,31 @@ +# This image will be published as dspace/dspace-angular:$DSPACE_VERSION-dist +# See https://github.com/DSpace/dspace-angular/tree/main/docker for usage details + +# Test build: +# docker build -f Dockerfile.dist -t dspace/dspace-angular:dspace-7_x-dist . + +FROM node:18-alpine as build + +# 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/* + +WORKDIR /app +COPY package.json yarn.lock ./ +RUN yarn install --network-timeout 300000 + +ADD . /app/ +RUN yarn build:prod + +FROM node:18-alpine +RUN npm install --global pm2 + +COPY --chown=node:node --from=build /app/dist /app/dist +COPY --chown=node:node config /app/config +COPY --chown=node:node docker/dspace-ui.json /app/dspace-ui.json + +WORKDIR /app +USER node +ENV NODE_ENV production +EXPOSE 4000 +CMD pm2-runtime start dspace-ui.json --json diff --git a/README.md b/README.md index 3aad58c9b9b..bfea0e25e03 100644 --- a/README.md +++ b/README.md @@ -135,8 +135,8 @@ DSPACE_UI_SSL => DSPACE_SSL The same settings can also be overwritten by setting system environment variables instead, E.g.: ```bash -export DSPACE_HOST=api7.dspace.org -export DSPACE_UI_PORT=4200 +export DSPACE_HOST=demo.dspace.org +export DSPACE_UI_PORT=4000 ``` The priority works as follows: **environment variable** overrides **variable in `.env` file** overrides external config set by `DSPACE_APP_CONFIG_PATH` overrides **`config.(prod or dev).yml`** @@ -266,7 +266,7 @@ E2E tests (aka integration tests) use [Cypress.io](https://www.cypress.io/). Con The test files can be found in the `./cypress/integration/` folder. Before you can run e2e tests, two things are REQUIRED: -1. You MUST be running the DSpace backend (i.e. REST API) locally. The e2e tests will *NOT* succeed if run against our demo REST API (https://api7.dspace.org/server/), as that server is uncontrolled and may have content added/removed at any time. +1. You MUST be running the DSpace backend (i.e. REST API) locally. The e2e tests will *NOT* succeed if run against our demo/sandbox REST API (https://demo.dspace.org/server/ or https://sandbox.dspace.org/server/), as those sites may have content added/removed at any time. * After starting up your backend on localhost, make sure either your `config.prod.yml` or `config.dev.yml` has its `rest` settings defined to use that localhost backend. * If you'd prefer, you may instead use environment variables as described at [Configuring](#configuring). For example: ``` @@ -391,8 +391,7 @@ dspace-angular │ ├── merge-i18n-files.ts * │ ├── serve.ts * │ ├── sync-i18n-files.ts * -│ ├── test-rest.ts * -│ └── webpack.js * +│ └── test-rest.ts * ├── src * The source of the application │ ├── app * The source code of the application, subdivided by module/page. │ ├── assets * Folder for static resources diff --git a/angular.json b/angular.json index 3d991a57744..93c56156510 100644 --- a/angular.json +++ b/angular.json @@ -271,20 +271,30 @@ "options": { "lintFilePatterns": [ "src/**/*.ts", - "src/**/*.html" + "src/**/*.html", + "src/**/*.json5" ] } } } } }, - "defaultProject": "dspace-angular", "cli": { "analytics": false, - "defaultCollection": "@angular-eslint/schematics", + "schematicCollections": [ + "@angular-eslint/schematics" + ], "cache": { "enabled": true, "environment": "local" } + }, + "schematics": { + "@angular-eslint/schematics:application": { + "setParserOptionsProject": true + }, + "@angular-eslint/schematics:library": { + "setParserOptionsProject": true + } } } diff --git a/bitbucket-pipelines.yml b/bitbucket-pipelines.yml index 5cdc81bfd51..3e36e21f1f0 100644 --- a/bitbucket-pipelines.yml +++ b/bitbucket-pipelines.yml @@ -20,7 +20,7 @@ definitions: pipelines: branches: - 'dspace-cris-7': + 'dspace-cris-2023_02_x': - step: *unittest-code-checks pull-requests: '**': diff --git a/config/config.example.yml b/config/config.example.yml index 10344a47a2b..3ea6eb92fda 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -211,6 +211,9 @@ languages: - code: it label: Italiano active: true + - code: it + label: Italiano + active: true - code: lv label: Latviešu active: true @@ -229,6 +232,9 @@ languages: - code: pt-BR label: Português do Brasil active: true + - code: sr-lat + label: Srpski (lat) + active: true - code: fi label: Suomi active: true @@ -238,6 +244,9 @@ languages: - code: tr label: Türkçe active: true + - code: vi + label: Tiếng Việt + active: true - code: kk label: Қазақ active: true @@ -250,6 +259,9 @@ languages: - code: el label: Ελληνικά active: true + - code: sr-cyr + label: Српски + active: true - code: uk label: Yкраї́нська active: true @@ -319,33 +331,33 @@ themes: # # # A theme with a handle property will match the community, collection or item with the given # # handle, and all collections and/or items within it - # - name: 'custom', - # handle: '10673/1233' + # - name: custom + # handle: 10673/1233 # # # A theme with a regex property will match the route using a regular expression. If it # # matches the route for a community or collection it will also apply to all collections # # and/or items within it - # - name: 'custom', - # regex: 'collections\/e8043bc2.*' + # - name: custom + # regex: collections\/e8043bc2.* # # # A theme with a uuid property will match the community, collection or item with the given # # ID, and all collections and/or items within it - # - name: 'custom', - # uuid: '0958c910-2037-42a9-81c7-dca80e3892b4' + # - name: custom + # uuid: 0958c910-2037-42a9-81c7-dca80e3892b4 # # # The extends property specifies an ancestor theme (by name). Whenever a themed component is not found # # in the current theme, its ancestor theme(s) will be checked recursively before falling back to default. - # - name: 'custom-A', - # extends: 'custom-B', + # - name: custom-A + # extends: custom-B # # Any of the matching properties above can be used - # handle: '10673/34' + # handle: 10673/34 # - # - name: 'custom-B', - # extends: 'custom', - # handle: '10673/12' + # - name: custom-B + # extends: custom + # handle: 10673/12 # # # A theme with only a name will match every route - # name: 'custom' + # name: custom # # # This theme will use the default bootstrap styling for DSpace components # - name: BASE_THEME_NAME @@ -407,6 +419,11 @@ vocabularies: 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' + crisLayout: urn: - name: doi diff --git a/cypress.config.ts b/cypress.config.ts new file mode 100644 index 00000000000..91eeb9838b3 --- /dev/null +++ b/cypress.config.ts @@ -0,0 +1,44 @@ +import { defineConfig } from 'cypress'; + +export default defineConfig({ + videosFolder: 'cypress/videos', + screenshotsFolder: 'cypress/screenshots', + fixturesFolder: 'cypress/fixtures', + retries: { + runMode: 2, + openMode: 0, + }, + env: { + // Global constants used in DSpace e2e tests (see also ./cypress/support/e2e.ts) + // May be overridden in our cypress.json config file using specified environment variables. + // Default values listed here are all valid for the Demo Entities Data set available at + // https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data + // (This is the data set used in our CI environment) + + // Admin account used for administrative tests + DSPACE_TEST_ADMIN_USER: 'dspacedemo+admin@gmail.com', + DSPACE_TEST_ADMIN_PASSWORD: 'dspace', + // Community/collection/publication used for view/edit tests + DSPACE_TEST_COMMUNITY: '0958c910-2037-42a9-81c7-dca80e3892b4', + DSPACE_TEST_COLLECTION: '282164f5-d325-4740-8dd1-fa4d6d3e7200', + DSPACE_TEST_ENTITY_PUBLICATION: 'e98b0f27-5c19-49a0-960d-eb6ad5287067', + // Search term (should return results) used in search tests + DSPACE_TEST_SEARCH_TERM: 'test', + // Collection used for submission tests + DSPACE_TEST_SUBMIT_COLLECTION_NAME: 'Sample Collection', + DSPACE_TEST_SUBMIT_COLLECTION_UUID: '9d8334e9-25d3-4a67-9cea-3dffdef80144', + // Account used to test basic submission process + DSPACE_TEST_SUBMIT_USER: 'dspacedemo+submit@gmail.com', + DSPACE_TEST_SUBMIT_USER_PASSWORD: 'dspace', + }, + e2e: { + // Setup our plugins for e2e tests + setupNodeEvents(on, config) { + return require('./cypress/plugins/index.ts')(on, config); + }, + // This is the base URL that Cypress will run all tests against + // It can be overridden via the CYPRESS_BASE_URL environment variable + // (By default we set this to a value which should work in most development environments) + baseUrl: 'http://localhost:4000', + }, +}); diff --git a/cypress.json b/cypress.json deleted file mode 100644 index 3adf7839c24..00000000000 --- a/cypress.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "integrationFolder": "cypress/integration", - "supportFile": "cypress/support/index.ts", - "videosFolder": "cypress/videos", - "screenshotsFolder": "cypress/screenshots", - "pluginsFile": "cypress/plugins/index.ts", - "fixturesFolder": "cypress/fixtures", - "baseUrl": "http://127.0.0.1:4000", - "retries": { - "runMode": 2, - "openMode": 0 - }, - "env": { - "DSPACE_TEST_ADMIN_USER": "dspacedemo+admin@gmail.com", - "DSPACE_TEST_ADMIN_PASSWORD": "dspace", - "DSPACE_TEST_COMMUNITY": "0958c910-2037-42a9-81c7-dca80e3892b4", - "DSPACE_TEST_COLLECTION": "282164f5-d325-4740-8dd1-fa4d6d3e7200", - "DSPACE_TEST_ENTITY_PUBLICATION": "e98b0f27-5c19-49a0-960d-eb6ad5287067", - "DSPACE_TEST_SEARCH_TERM": "test", - "DSPACE_TEST_SUBMIT_COLLECTION_NAME": "Sample Collection", - "DSPACE_TEST_SUBMIT_COLLECTION_UUID": "9d8334e9-25d3-4a67-9cea-3dffdef80144", - "DSPACE_TEST_SUBMIT_USER": "dspacedemo+submit@gmail.com", - "DSPACE_TEST_SUBMIT_USER_PASSWORD": "dspace" - } -} diff --git a/cypress/integration/breadcrumbs.spec.ts b/cypress/e2e/breadcrumbs.cy.ts similarity index 80% rename from cypress/integration/breadcrumbs.spec.ts rename to cypress/e2e/breadcrumbs.cy.ts index a74de1660c0..849cfeba2ab 100644 --- a/cypress/integration/breadcrumbs.spec.ts +++ b/cypress/e2e/breadcrumbs.cy.ts @@ -1,11 +1,11 @@ -import { TEST_ENTITY_PUBLICATION } from 'cypress/support'; +import { TEST_ENTITY_PUBLICATION } from 'cypress/support/e2e'; import { testA11y } from 'cypress/support/utils'; import { Options } from 'cypress-axe'; xdescribe('Breadcrumbs', () => { it('should pass accessibility tests', () => { // Visit an Item, as those have more breadcrumbs - cy.visit('/entities/publication/' + TEST_ENTITY_PUBLICATION); + cy.visit('/entities/publication/'.concat(TEST_ENTITY_PUBLICATION)); // Wait for breadcrumbs to be visible cy.get('ds-breadcrumbs').should('be.visible'); diff --git a/cypress/integration/browse-by-author.spec.ts b/cypress/e2e/browse-by-author.cy.ts similarity index 100% rename from cypress/integration/browse-by-author.spec.ts rename to cypress/e2e/browse-by-author.cy.ts diff --git a/cypress/integration/browse-by-dateissued.spec.ts b/cypress/e2e/browse-by-dateissued.cy.ts similarity index 100% rename from cypress/integration/browse-by-dateissued.spec.ts rename to cypress/e2e/browse-by-dateissued.cy.ts diff --git a/cypress/integration/browse-by-subject.spec.ts b/cypress/e2e/browse-by-subject.cy.ts similarity index 100% rename from cypress/integration/browse-by-subject.spec.ts rename to cypress/e2e/browse-by-subject.cy.ts diff --git a/cypress/integration/browse-by-title.spec.ts b/cypress/e2e/browse-by-title.cy.ts similarity index 100% rename from cypress/integration/browse-by-title.spec.ts rename to cypress/e2e/browse-by-title.cy.ts diff --git a/cypress/integration/collection-page.spec.ts b/cypress/e2e/collection-page.cy.ts similarity index 64% rename from cypress/integration/collection-page.spec.ts rename to cypress/e2e/collection-page.cy.ts index a0140d8faf2..a034b4361d6 100644 --- a/cypress/integration/collection-page.spec.ts +++ b/cypress/e2e/collection-page.cy.ts @@ -1,13 +1,13 @@ -import { TEST_COLLECTION } from 'cypress/support'; +import { TEST_COLLECTION } from 'cypress/support/e2e'; import { testA11y } from 'cypress/support/utils'; describe('Collection Page', () => { it('should pass accessibility tests', () => { - cy.visit('/collections/' + TEST_COLLECTION); + cy.visit('/collections/'.concat(TEST_COLLECTION)); // tag must be loaded - cy.get('ds-collection-page').should('exist'); + cy.get('ds-collection-page').should('be.visible'); // Analyze for accessibility issues testA11y('ds-collection-page'); diff --git a/cypress/e2e/collection-statistics.cy.ts b/cypress/e2e/collection-statistics.cy.ts new file mode 100644 index 00000000000..68775ee1b80 --- /dev/null +++ b/cypress/e2e/collection-statistics.cy.ts @@ -0,0 +1,37 @@ +import { REGEX_MATCH_NON_EMPTY_TEXT, TEST_COLLECTION } from 'cypress/support/e2e'; +import { testA11y } from 'cypress/support/utils'; + +xdescribe('Collection Statistics Page', () => { + const COLLECTIONSTATISTICSPAGE = '/statistics/collections/'.concat(TEST_COLLECTION); + + it('should load if you click on "Statistics" from a Collection page', () => { + cy.visit('/collections/'.concat(TEST_COLLECTION)); + cy.get('ds-navbar ds-link-menu-item a[title="Statistics"]').click(); + cy.location('pathname').should('eq', COLLECTIONSTATISTICSPAGE); + }); + + it('should contain a "Total visits" section', () => { + cy.visit(COLLECTIONSTATISTICSPAGE); + cy.get('table[data-test="TotalVisits"]').should('be.visible'); + }); + + it('should contain a "Total visits per month" section', () => { + cy.visit(COLLECTIONSTATISTICSPAGE); + // Check just for existence because this table is empty in CI environment as it's historical data + cy.get('.'.concat(TEST_COLLECTION).concat('_TotalVisitsPerMonth')).should('exist'); + }); + + it('should pass accessibility tests', () => { + cy.visit(COLLECTIONSTATISTICSPAGE); + + // tag must be loaded + cy.get('ds-collection-statistics-page').should('be.visible'); + + // Verify / wait until "Total Visits" table's label is non-empty + // (This table loads these labels asynchronously, so we want to wait for them before analyzing page) + cy.get('table[data-test="TotalVisits"] th[data-test="statistics-label"]').contains(REGEX_MATCH_NON_EMPTY_TEXT); + + // Analyze for accessibility issues + testA11y('ds-collection-statistics-page'); + }); +}); diff --git a/cypress/e2e/community-list.cy.ts b/cypress/e2e/community-list.cy.ts new file mode 100644 index 00000000000..c371f6ceae7 --- /dev/null +++ b/cypress/e2e/community-list.cy.ts @@ -0,0 +1,17 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('Community List Page', () => { + + it('should pass accessibility tests', () => { + cy.visit('/community-list'); + + // tag must be loaded + cy.get('ds-community-list-page').should('be.visible'); + + // Open every expand button on page, so that we can scan sub-elements as well + cy.get('[data-test="expand-button"]').click({ multiple: true }); + + // Analyze for accessibility issues + testA11y('ds-community-list-page'); + }); +}); diff --git a/cypress/integration/community-page.spec.ts b/cypress/e2e/community-page.cy.ts similarity index 73% rename from cypress/integration/community-page.spec.ts rename to cypress/e2e/community-page.cy.ts index fec570f8ec8..52420b4bb10 100644 --- a/cypress/integration/community-page.spec.ts +++ b/cypress/e2e/community-page.cy.ts @@ -1,14 +1,14 @@ -import { TEST_COMMUNITY } from 'cypress/support'; +import { TEST_COMMUNITY } from 'cypress/support/e2e'; import { testA11y } from 'cypress/support/utils'; import { Options } from 'cypress-axe'; describe('Community Page', () => { it('should pass accessibility tests', () => { - cy.visit('/communities/' + TEST_COMMUNITY); + cy.visit('/communities/'.concat(TEST_COMMUNITY)); // tag must be loaded - cy.get('ds-community-page').should('exist'); + cy.get('ds-community-page').should('be.visible'); // Analyze for accessibility issues testA11y('ds-community-page', diff --git a/cypress/e2e/community-statistics.cy.ts b/cypress/e2e/community-statistics.cy.ts new file mode 100644 index 00000000000..ddc42a82bb4 --- /dev/null +++ b/cypress/e2e/community-statistics.cy.ts @@ -0,0 +1,37 @@ +import { REGEX_MATCH_NON_EMPTY_TEXT, TEST_COMMUNITY } from 'cypress/support/e2e'; +import { testA11y } from 'cypress/support/utils'; + +xdescribe('Community Statistics Page', () => { + const COMMUNITYSTATISTICSPAGE = '/statistics/communities/'.concat(TEST_COMMUNITY); + + it('should load if you click on "Statistics" from a Community page', () => { + cy.visit('/communities/'.concat(TEST_COMMUNITY)); + cy.get('ds-navbar ds-link-menu-item a[title="Statistics"]').click(); + cy.location('pathname').should('eq', COMMUNITYSTATISTICSPAGE); + }); + + it('should contain a "Total visits" section', () => { + cy.visit(COMMUNITYSTATISTICSPAGE); + cy.get('table[data-test="TotalVisits"]').should('be.visible'); + }); + + it('should contain a "Total visits per month" section', () => { + cy.visit(COMMUNITYSTATISTICSPAGE); + // Check just for existence because this table is empty in CI environment as it's historical data + cy.get('.'.concat(TEST_COMMUNITY).concat('_TotalVisitsPerMonth')).should('exist'); + }); + + it('should pass accessibility tests', () => { + cy.visit(COMMUNITYSTATISTICSPAGE); + + // tag must be loaded + cy.get('ds-community-statistics-page').should('be.visible'); + + // Verify / wait until "Total Visits" table's label is non-empty + // (This table loads these labels asynchronously, so we want to wait for them before analyzing page) + cy.get('table[data-test="TotalVisits"] th[data-test="statistics-label"]').contains(REGEX_MATCH_NON_EMPTY_TEXT); + + // Analyze for accessibility issues + testA11y('ds-community-statistics-page'); + }); +}); diff --git a/cypress/integration/footer.spec.ts b/cypress/e2e/footer.cy.ts similarity index 100% rename from cypress/integration/footer.spec.ts rename to cypress/e2e/footer.cy.ts diff --git a/cypress/integration/header.spec.ts b/cypress/e2e/header.cy.ts similarity index 64% rename from cypress/integration/header.spec.ts rename to cypress/e2e/header.cy.ts index 236208db686..1a9b841eb7d 100644 --- a/cypress/integration/header.spec.ts +++ b/cypress/e2e/header.cy.ts @@ -11,8 +11,7 @@ describe('Header', () => { testA11y({ include: ['ds-header'], exclude: [ - ['#search-navbar-container'], // search in navbar has duplicative ID. Will be fixed in #1174 - ['.dropdownLogin'] // "Log in" link has color contrast issues. Will be fixed in #1149 + ['#search-navbar-container'] // search in navbar has duplicative ID. Will be fixed in #1174 ], }); }); diff --git a/cypress/e2e/homepage-statistics.cy.ts b/cypress/e2e/homepage-statistics.cy.ts new file mode 100644 index 00000000000..06edb17a6d3 --- /dev/null +++ b/cypress/e2e/homepage-statistics.cy.ts @@ -0,0 +1,31 @@ +import { REGEX_MATCH_NON_EMPTY_TEXT, TEST_ENTITY_PUBLICATION } from 'cypress/support/e2e'; +import { testA11y } from 'cypress/support/utils'; +import '../support/commands'; + +xdescribe('Site Statistics Page', () => { + it('should load if you click on "Statistics" from homepage', () => { + cy.visit('/'); + cy.get('ds-navbar ds-link-menu-item a[title="Statistics"]').click(); + cy.location('pathname').should('eq', '/statistics'); + }); + + it('should pass accessibility tests', () => { + // generate 2 view events on an Item's page + cy.generateViewEvent(TEST_ENTITY_PUBLICATION, 'item'); + cy.generateViewEvent(TEST_ENTITY_PUBLICATION, 'item'); + + cy.visit('/statistics'); + + // tag must be visable + cy.get('ds-site-statistics-page').should('be.visible'); + + // Verify / wait until "Total Visits" table's *last* label is non-empty + // (This table loads these labels asynchronously, so we want to wait for them before analyzing page) + cy.get('table[data-test="TotalVisits"] th[data-test="statistics-label"]').last().contains(REGEX_MATCH_NON_EMPTY_TEXT); + // Wait an extra 500ms, just so all entries in Total Visits have loaded. + cy.wait(500); + + // Analyze for accessibility issues + testA11y('ds-site-statistics-page'); + }); +}); diff --git a/cypress/integration/homepage.spec.ts b/cypress/e2e/homepage.cy.ts similarity index 85% rename from cypress/integration/homepage.spec.ts rename to cypress/e2e/homepage.cy.ts index 39097dc5186..287f7b3da56 100644 --- a/cypress/integration/homepage.spec.ts +++ b/cypress/e2e/homepage.cy.ts @@ -7,8 +7,8 @@ describe('Homepage', () => { cy.visit('/'); }); - it('should display translated title "DSpace at My University :: Home"', () => { - cy.title().should('eq', 'DSpace at My University :: Home'); + it('should display translated title "DSpace at My University Angular :: Home"', () => { + cy.title().should('eq', 'DSpace at My University Angular :: Home'); }); it('should contain a news section', () => { diff --git a/cypress/e2e/item-page.cy.ts b/cypress/e2e/item-page.cy.ts new file mode 100644 index 00000000000..2468abfbd21 --- /dev/null +++ b/cypress/e2e/item-page.cy.ts @@ -0,0 +1,33 @@ +import { TEST_ENTITY_PUBLICATION } from 'cypress/support/e2e'; +import { testA11y } from 'cypress/support/utils'; + +xdescribe('Item Page', () => { + const ITEMPAGE = '/items/'.concat(TEST_ENTITY_PUBLICATION); + const ENTITYPAGE = '/entities/publication/'.concat(TEST_ENTITY_PUBLICATION); + + // Test that entities will redirect to /entities/[type]/[uuid] when accessed via /items/[uuid] + it('should redirect to the entity page when navigating to an item page', () => { + cy.visit(ITEMPAGE); + cy.location('pathname').should('eq', ENTITYPAGE); + }); + + it('should pass accessibility tests', () => { + cy.visit(ENTITYPAGE); + + // tag must be loaded + cy.get('ds-item-page').should('be.visible'); + + // Analyze for accessibility issues + testA11y('ds-item-page'); + }); + + it('should pass accessibility tests on full item page', () => { + cy.visit(ENTITYPAGE + '/full'); + + // tag must be loaded + cy.get('ds-full-item-page').should('be.visible'); + + // Analyze for accessibility issues + testA11y('ds-full-item-page'); + }); +}); diff --git a/cypress/integration/item-statistics.spec.ts b/cypress/e2e/item-statistics.cy.ts similarity index 51% rename from cypress/integration/item-statistics.spec.ts rename to cypress/e2e/item-statistics.cy.ts index 71269dc9c58..d9c6287f005 100644 --- a/cypress/integration/item-statistics.spec.ts +++ b/cypress/e2e/item-statistics.cy.ts @@ -1,36 +1,41 @@ -import { TEST_ENTITY_PUBLICATION } from 'cypress/support'; +import { REGEX_MATCH_NON_EMPTY_TEXT, TEST_ENTITY_PUBLICATION } from 'cypress/support/e2e'; import { testA11y } from 'cypress/support/utils'; xdescribe('Item Statistics Page', () => { - const ITEMSTATISTICSPAGE = '/statistics/items/' + TEST_ENTITY_PUBLICATION; + const ITEMSTATISTICSPAGE = '/statistics/items/'.concat(TEST_ENTITY_PUBLICATION); it('should load if you click on "Statistics" from an Item/Entity page', () => { - cy.visit('/entities/publication/' + TEST_ENTITY_PUBLICATION); + cy.visit('/entities/publication/'.concat(TEST_ENTITY_PUBLICATION)); cy.get('ds-navbar ds-link-menu-item a[title="Statistics"]').click(); cy.location('pathname').should('eq', ITEMSTATISTICSPAGE); }); it('should contain element ds-item-statistics-page when navigating to an item statistics page', () => { cy.visit(ITEMSTATISTICSPAGE); - cy.get('ds-item-statistics-page').should('exist'); + cy.get('ds-item-statistics-page').should('be.visible'); cy.get('ds-item-page').should('not.exist'); }); it('should contain a "Total visits" section', () => { cy.visit(ITEMSTATISTICSPAGE); - cy.get('.' + TEST_ENTITY_PUBLICATION + '_TotalVisits').should('exist'); + cy.get('table[data-test="TotalVisits"]').should('be.visible'); }); it('should contain a "Total visits per month" section', () => { cy.visit(ITEMSTATISTICSPAGE); - cy.get('.' + TEST_ENTITY_PUBLICATION + '_TotalVisitsPerMonth').should('exist'); + // Check just for existence because this table is empty in CI environment as it's historical data + cy.get('.'.concat(TEST_ENTITY_PUBLICATION).concat('_TotalVisitsPerMonth')).should('exist'); }); it('should pass accessibility tests', () => { cy.visit(ITEMSTATISTICSPAGE); // tag must be loaded - cy.get('ds-item-statistics-page').should('exist'); + cy.get('ds-item-statistics-page').should('be.visible'); + + // Verify / wait until "Total Visits" table's label is non-empty + // (This table loads these labels asynchronously, so we want to wait for them before analyzing page) + cy.get('table[data-test="TotalVisits"] th[data-test="statistics-label"]').contains(REGEX_MATCH_NON_EMPTY_TEXT); // Analyze for accessibility issues testA11y('ds-item-statistics-page'); diff --git a/cypress/integration/login-modal.spec.ts b/cypress/e2e/login-modal.cy.ts similarity index 87% rename from cypress/integration/login-modal.spec.ts rename to cypress/e2e/login-modal.cy.ts index 4d4b7cacaef..74226255da9 100644 --- a/cypress/integration/login-modal.spec.ts +++ b/cypress/e2e/login-modal.cy.ts @@ -1,4 +1,5 @@ -import { TEST_ADMIN_PASSWORD, TEST_ADMIN_USER, TEST_ENTITY_PUBLICATION } from 'cypress/support'; +import { TEST_ADMIN_PASSWORD, TEST_ADMIN_USER, TEST_COLLECTION } from 'cypress/support/e2e'; +import { testA11y } from 'cypress/support/utils'; const page = { openLoginMenu() { @@ -35,9 +36,9 @@ const page = { }; describe('Login Modal', () => { - xit('should login when clicking button & stay on same page', () => { - const ENTITYPAGE = '/entities/publication/' + TEST_ENTITY_PUBLICATION; - cy.visit(ENTITYPAGE); + it('should login when clicking button & stay on same page', () => { + const COLLECTIONPAGE = '/collections/'.concat(TEST_COLLECTION); + cy.visit(COLLECTIONPAGE); // Login menu should exist cy.get('ds-log-in').should('exist'); @@ -50,7 +51,7 @@ describe('Login Modal', () => { cy.get('ds-log-in').should('not.exist'); // Verify we are still on the same page - cy.url().should('include', ENTITYPAGE); + cy.url().should('include', COLLECTIONPAGE); // Open user menu, verify user menu & logout button now available page.openUserMenu(); @@ -123,4 +124,15 @@ describe('Login Modal', () => { cy.location('pathname').should('eq', '/forgot'); cy.get('ds-forgot-email').should('exist'); }); + + it('should pass accessibility tests', () => { + cy.visit('/'); + + page.openLoginMenu(); + + cy.get('ds-log-in').should('exist'); + + // Analyze for accessibility issues + testA11y('ds-log-in'); + }); }); diff --git a/cypress/integration/my-dspace.spec.ts b/cypress/e2e/my-dspace.cy.ts similarity index 88% rename from cypress/integration/my-dspace.spec.ts rename to cypress/e2e/my-dspace.cy.ts index a214a865c93..89adb7d5057 100644 --- a/cypress/integration/my-dspace.spec.ts +++ b/cypress/e2e/my-dspace.cy.ts @@ -1,5 +1,5 @@ import { Options } from 'cypress-axe'; -import { TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD, TEST_SUBMIT_COLLECTION_NAME } from 'cypress/support'; +import { TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD, TEST_SUBMIT_COLLECTION_NAME } from 'cypress/support/e2e'; import { testA11y } from 'cypress/support/utils'; xdescribe('My DSpace page', () => { @@ -9,7 +9,7 @@ xdescribe('My DSpace page', () => { // 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('be.visible'); // At least one recent submission should be displayed cy.get('[data-test="list-object"]').should('be.visible'); @@ -19,21 +19,7 @@ xdescribe('My DSpace page', () => { cy.get('.filter-toggle').click({ multiple: true }); // Analyze for accessibility issues - testA11y( - { - include: ['ds-my-dspace-page'], - exclude: [ - ['nouislider'] // Date filter slider is missing ARIA labels. Will be fixed by #1175 - ], - }, - { - rules: { - // Search filters fail these two "moderate" impact rules - 'heading-order': { enabled: false }, - 'landmark-unique': { enabled: false } - } - } as Options - ); + testA11y('ds-my-dspace-page'); }); it('should have a working detailed view that passes accessibility tests', () => { @@ -42,12 +28,12 @@ xdescribe('My DSpace page', () => { // 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('be.visible'); // Click button in sidebar to display detailed view cy.get('ds-search-sidebar [data-test="detail-view"]').click(); - cy.get('ds-object-detail').should('exist'); + cy.get('ds-object-detail').should('be.visible'); // Analyze for accessibility issues testA11y('ds-my-dspace-page', @@ -80,7 +66,7 @@ xdescribe('My DSpace page', () => { cy.get('ds-authorized-collection-selector input[type="search"]').type(TEST_SUBMIT_COLLECTION_NAME); // Click on the button matching that known Collection name - cy.get('ds-authorized-collection-selector button[title="' + TEST_SUBMIT_COLLECTION_NAME + '"]').click(); + cy.get('ds-authorized-collection-selector button[title="'.concat(TEST_SUBMIT_COLLECTION_NAME).concat('"]')).click(); // New URL should include /workspaceitems, as we've started a new submission cy.url().should('include', '/workspaceitems'); diff --git a/cypress/integration/pagenotfound.spec.ts b/cypress/e2e/pagenotfound.cy.ts similarity index 70% rename from cypress/integration/pagenotfound.spec.ts rename to cypress/e2e/pagenotfound.cy.ts index 48520bcaa32..d02aa8541c3 100644 --- a/cypress/integration/pagenotfound.spec.ts +++ b/cypress/e2e/pagenotfound.cy.ts @@ -1,8 +1,13 @@ +import { testA11y } from 'cypress/support/utils'; + describe('PageNotFound', () => { it('should contain element ds-pagenotfound when navigating to page that doesnt exist', () => { // request an invalid page (UUIDs at root path aren't valid) cy.visit('/e9019a69-d4f1-4773-b6a3-bd362caa46f2', { failOnStatusCode: false }); - cy.get('ds-pagenotfound').should('exist'); + cy.get('ds-pagenotfound').should('be.visible'); + + // Analyze for accessibility issues + testA11y('ds-pagenotfound'); }); it('should not contain element ds-pagenotfound when navigating to existing page', () => { diff --git a/cypress/integration/search-navbar.spec.ts b/cypress/e2e/search-navbar.cy.ts similarity index 92% rename from cypress/integration/search-navbar.spec.ts rename to cypress/e2e/search-navbar.cy.ts index 12604856ec6..5000dc206c5 100644 --- a/cypress/integration/search-navbar.spec.ts +++ b/cypress/e2e/search-navbar.cy.ts @@ -1,4 +1,4 @@ -import { TEST_SEARCH_TERM } from 'cypress/support'; +import { TEST_SEARCH_TERM } from 'cypress/support/e2e'; const page = { fillOutQueryInNavBar(query) { @@ -27,7 +27,7 @@ xdescribe('Search from Navigation Bar', () => { page.fillOutQueryInNavBar(query); page.submitQueryByPressingEnter(); // New URL should include query param - cy.url().should('include', 'query=' + query); + cy.url().should('include', 'query='.concat(query)); // Wait for search results to come back from the above GET command cy.wait('@search-results'); // At least one search result should be displayed @@ -42,7 +42,7 @@ xdescribe('Search from Navigation Bar', () => { page.fillOutQueryInNavBar(query); page.submitQueryByPressingEnter(); // New URL should include query param - cy.url().should('include', 'query=' + query); + cy.url().should('include', 'query='.concat(query)); // Wait for search results to come back from the above GET command cy.wait('@search-results'); // At least one search result should be displayed @@ -57,7 +57,7 @@ xdescribe('Search from Navigation Bar', () => { page.fillOutQueryInNavBar(query); page.submitQueryByPressingIcon(); // New URL should include query param - cy.url().should('include', 'query=' + query); + cy.url().should('include', 'query='.concat(query)); // Wait for search results to come back from the above GET command cy.wait('@search-results'); // At least one search result should be displayed diff --git a/cypress/integration/search-page.spec.ts b/cypress/e2e/search-page.cy.ts similarity index 69% rename from cypress/integration/search-page.spec.ts rename to cypress/e2e/search-page.cy.ts index 63ffbd95811..a8509e84de6 100644 --- a/cypress/integration/search-page.spec.ts +++ b/cypress/e2e/search-page.cy.ts @@ -1,5 +1,4 @@ -import { Options } from 'cypress-axe'; -import { TEST_SEARCH_TERM } from 'cypress/support'; +import { TEST_SEARCH_TERM } from 'cypress/support/e2e'; import { testA11y } from 'cypress/support/utils'; describe('Search Page', () => { @@ -13,11 +12,11 @@ describe('Search Page', () => { }); it('should load results and pass accessibility tests', () => { - cy.visit('/search?query=' + TEST_SEARCH_TERM); + cy.visit('/search?query='.concat(TEST_SEARCH_TERM)); cy.get('[data-test="search-box"]').should('have.value', TEST_SEARCH_TERM); // tag must be loaded - cy.get('ds-search-page').should('exist'); + cy.get('ds-search-page').should('be.visible'); // At least one search result should be displayed cy.get('[data-test="list-object"]').should('be.visible'); @@ -27,31 +26,17 @@ describe('Search Page', () => { cy.get('[data-test="filter-toggle"]').click({ multiple: true }); // Analyze for accessibility issues -/* testA11y( - { - include: ['ds-search-page'], - exclude: [ - ['nouislider'] // Date filter slider is missing ARIA labels. Will be fixed by #1175 - ], - }, - { - rules: { - // Search filters fail these two "moderate" impact rules - 'heading-order': { enabled: false }, - 'landmark-unique': { enabled: false } - } - } as Options - );*/ + testA11y('ds-search-page'); }); it('should have a working grid view that passes accessibility tests', () => { - cy.visit('/search?query=' + TEST_SEARCH_TERM); + cy.visit('/search?query='.concat(TEST_SEARCH_TERM)); // Click button in sidebar to display grid view cy.get('ds-search-sidebar [data-test="grid-view"]').click(); // tag must be loaded - cy.get('ds-search-page').should('exist'); + cy.get('ds-search-page').should('be.visible'); // At least one grid object (card) should be displayed cy.get('[data-test="grid-object"]').should('be.visible'); diff --git a/cypress/integration/submission.spec.ts b/cypress/e2e/submission.cy.ts similarity index 92% rename from cypress/integration/submission.spec.ts rename to cypress/e2e/submission.cy.ts index 7c9af9c3074..618d287fa54 100644 --- a/cypress/integration/submission.spec.ts +++ b/cypress/e2e/submission.cy.ts @@ -1,13 +1,11 @@ -import { Options } from 'cypress-axe'; -import { TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD, TEST_SUBMIT_COLLECTION_NAME, TEST_SUBMIT_COLLECTION_UUID } from 'cypress/support'; -import { testA11y } from 'cypress/support/utils'; +import { TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD, TEST_SUBMIT_COLLECTION_NAME, TEST_SUBMIT_COLLECTION_UUID } from 'cypress/support/e2e'; xdescribe('New Submission page', () => { // 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', () => { // 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='.concat(TEST_SUBMIT_COLLECTION_UUID).concat('&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); @@ -35,7 +33,7 @@ xdescribe('New Submission page', () => { it('should block submission & show errors if required fields are missing', () => { // Create a new submission - cy.visit('/submit?collection=' + TEST_SUBMIT_COLLECTION_UUID + '&entityType=none'); + cy.visit('/submit?collection='.concat(TEST_SUBMIT_COLLECTION_UUID).concat('&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); @@ -95,7 +93,7 @@ xdescribe('New Submission page', () => { it('should allow for deposit if all required fields completed & file uploaded', () => { // Create a new submission - cy.visit('/submit?collection=' + TEST_SUBMIT_COLLECTION_UUID + '&entityType=none'); + cy.visit('/submit?collection='.concat(TEST_SUBMIT_COLLECTION_UUID).concat('&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); @@ -124,8 +122,6 @@ xdescribe('New Submission page', () => { // Wait for upload to complete before proceeding cy.wait('@upload'); - // Close the upload success notice - cy.get('[data-dismiss="alert"]').click({multiple: true}); // Wait for deposit button to not be disabled & click it. cy.get('button#deposit').should('not.be.disabled').click(); diff --git a/cypress/integration/collection-statistics.spec.ts b/cypress/integration/collection-statistics.spec.ts deleted file mode 100644 index 1532de00175..00000000000 --- a/cypress/integration/collection-statistics.spec.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { TEST_COLLECTION } from 'cypress/support'; -import { testA11y } from 'cypress/support/utils'; - -xdescribe('Collection Statistics Page', () => { - const COLLECTIONSTATISTICSPAGE = '/statistics/collections/' + TEST_COLLECTION; - - it('should load if you click on "Statistics" from a Collection page', () => { - cy.visit('/collections/' + TEST_COLLECTION); - cy.get('ds-navbar ds-link-menu-item a[title="Statistics"]').click(); - cy.location('pathname').should('eq', COLLECTIONSTATISTICSPAGE); - }); - - it('should contain a "Total visits" section', () => { - cy.visit(COLLECTIONSTATISTICSPAGE); - cy.get('.' + TEST_COLLECTION + '_TotalVisits').should('exist'); - }); - - it('should contain a "Total visits per month" section', () => { - cy.visit(COLLECTIONSTATISTICSPAGE); - cy.get('.' + TEST_COLLECTION + '_TotalVisitsPerMonth').should('exist'); - }); - - it('should pass accessibility tests', () => { - cy.visit(COLLECTIONSTATISTICSPAGE); - - // tag must be loaded - cy.get('ds-collection-statistics-page').should('exist'); - - // Analyze for accessibility issues - testA11y('ds-collection-statistics-page'); - }); -}); diff --git a/cypress/integration/community-list.spec.ts b/cypress/integration/community-list.spec.ts deleted file mode 100644 index 9ff28bf2743..00000000000 --- a/cypress/integration/community-list.spec.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Options } from 'cypress-axe'; -import { testA11y } from 'cypress/support/utils'; - -describe('Community List Page', () => { - - it('should pass accessibility tests', () => { - cy.visit('/community-list'); - - // tag must be loaded - cy.get('ds-community-list-page').should('exist'); - - // Open first Community (to show Collections)...that way we scan sub-elements as well - cy.get('ds-community-list :nth-child(1) > .btn-group > .btn').click(); - - // Analyze for accessibility issues - // Disable heading-order checks until it is fixed - testA11y('ds-community-list-page', - { - rules: { - 'heading-order': { enabled: false }, - 'button-name': { enabled: false }, - } - } as Options - ); - }); -}); diff --git a/cypress/integration/community-statistics.spec.ts b/cypress/integration/community-statistics.spec.ts deleted file mode 100644 index ddd447cf213..00000000000 --- a/cypress/integration/community-statistics.spec.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { TEST_COMMUNITY } from 'cypress/support'; -import { testA11y } from 'cypress/support/utils'; - -xdescribe('Community Statistics Page', () => { - const COMMUNITYSTATISTICSPAGE = '/statistics/communities/' + TEST_COMMUNITY; - - it('should load if you click on "Statistics" from a Community page', () => { - cy.visit('/communities/' + TEST_COMMUNITY); - cy.get('ds-navbar ds-link-menu-item a[title="Statistics"]').click(); - cy.location('pathname').should('eq', COMMUNITYSTATISTICSPAGE); - }); - - it('should contain a "Total visits" section', () => { - cy.visit(COMMUNITYSTATISTICSPAGE); - cy.get('.' + TEST_COMMUNITY + '_TotalVisits').should('exist'); - }); - - it('should contain a "Total visits per month" section', () => { - cy.visit(COMMUNITYSTATISTICSPAGE); - cy.get('.' + TEST_COMMUNITY + '_TotalVisitsPerMonth').should('exist'); - }); - - it('should pass accessibility tests', () => { - cy.visit(COMMUNITYSTATISTICSPAGE); - - // tag must be loaded - cy.get('ds-community-statistics-page').should('exist'); - - // Analyze for accessibility issues - testA11y('ds-community-statistics-page'); - }); -}); diff --git a/cypress/integration/homepage-statistics.spec.ts b/cypress/integration/homepage-statistics.spec.ts deleted file mode 100644 index a24af59f053..00000000000 --- a/cypress/integration/homepage-statistics.spec.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { testA11y } from 'cypress/support/utils'; - -xdescribe('Site Statistics Page', () => { - it('should load if you click on "Statistics" from homepage', () => { - cy.visit('/'); - cy.get('ds-navbar ds-link-menu-item a[title="Statistics"]').click(); - cy.location('pathname').should('eq', '/statistics'); - }); - - it('should pass accessibility tests', () => { - cy.visit('/statistics'); - - // tag must be loaded - cy.get('ds-site-statistics-page').should('exist'); - - // Analyze for accessibility issues - testA11y('ds-site-statistics-page'); - }); -}); diff --git a/cypress/integration/item-page.spec.ts b/cypress/integration/item-page.spec.ts deleted file mode 100644 index 7104a731955..00000000000 --- a/cypress/integration/item-page.spec.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Options } from 'cypress-axe'; -import { TEST_ENTITY_PUBLICATION } from 'cypress/support'; -import { testA11y } from 'cypress/support/utils'; - -xdescribe('Item Page', () => { - const ITEMPAGE = '/items/' + TEST_ENTITY_PUBLICATION; - const ENTITYPAGE = '/entities/publication/' + TEST_ENTITY_PUBLICATION; - - // Test that entities will redirect to /entities/[type]/[uuid] when accessed via /items/[uuid] - it('should redirect to the entity page when navigating to an item page', () => { - cy.visit(ITEMPAGE); - cy.location('pathname').should('eq', ENTITYPAGE); - }); - - it('should pass accessibility tests', () => { - cy.visit(ENTITYPAGE); - - // tag must be loaded - cy.get('ds-item-page').should('exist'); - - // Analyze for accessibility issues - // Disable heading-order checks until it is fixed - testA11y('ds-item-page', - { - rules: { - 'heading-order': { enabled: false } - } - } as Options - ); - }); -}); diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index 04c217aa0f6..92f0b1aeeb6 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -4,12 +4,17 @@ // *********************************************** import { AuthTokenInfo, TOKENITEM } from 'src/app/core/auth/models/auth-token-info.model'; -import { FALLBACK_TEST_REST_BASE_URL } from '.'; +import { DSPACE_XSRF_COOKIE, XSRF_REQUEST_HEADER } from 'src/app/core/xsrf/xsrf.constants'; + +// NOTE: FALLBACK_TEST_REST_BASE_URL is only used if Cypress cannot read the REST API BaseURL +// from the Angular UI's config.json. See 'login()'. +export const FALLBACK_TEST_REST_BASE_URL = 'http://localhost:8080/server'; +export const FALLBACK_TEST_REST_DOMAIN = 'localhost'; // Declare Cypress namespace to help with Intellisense & code completion in IDEs // ALL custom commands MUST be listed here for code completion to work -// tslint:disable-next-line:no-namespace declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace namespace Cypress { interface Chainable { /** @@ -27,6 +32,15 @@ declare global { * @param password password to login as */ loginViaForm(email: string, password: string): typeof loginViaForm; + + /** + * Generate view event for given object. Useful for testing statistics pages with + * pre-generated statistics. This just generates a single "hit", but can be called multiple times to + * generate multiple hits. + * @param uuid UUID of object + * @param dsoType type of DSpace Object (e.g. "item", "collection", "community") + */ + generateViewEvent(uuid: string, dsoType: string): typeof generateViewEvent; } } } @@ -53,52 +67,57 @@ function login(email: string, password: string): void { if (!config.rest.baseUrl) { console.warn("Could not load 'rest.baseUrl' from config.json. Falling back to " + FALLBACK_TEST_REST_BASE_URL); } else { - console.log("Found 'rest.baseUrl' in config.json. Using this REST API for login: " + config.rest.baseUrl); + //console.log("Found 'rest.baseUrl' in config.json. Using this REST API for login: ".concat(config.rest.baseUrl)); baseRestUrl = config.rest.baseUrl; } - // To login via REST, first we have to do a GET to obtain a valid CSRF token - cy.request( baseRestUrl + '/api/authn/status' ) - .then((response) => { - // We should receive a CSRF token returned in a response header - expect(response.headers).to.have.property('dspace-xsrf-token'); - const csrfToken = response.headers['dspace-xsrf-token']; - - // Now, send login POST request including that CSRF token - cy.request({ - method: 'POST', - url: baseRestUrl + '/api/authn/login', - headers: { 'X-XSRF-TOKEN' : csrfToken}, - form: true, // indicates the body should be form urlencoded - body: { user: email, password: password } - }).then((resp) => { - // We expect a successful login - expect(resp.status).to.eq(200); - // We expect to have a valid authorization header returned (with our auth token) - expect(resp.headers).to.have.property('authorization'); - - // Initialize our AuthTokenInfo object from the authorization header. - const authheader = resp.headers.authorization as string; - const authinfo: AuthTokenInfo = new AuthTokenInfo(authheader); - - // Save our AuthTokenInfo object to our dsAuthInfo UI cookie - // This ensures the UI will recognize we are logged in on next "visit()" - cy.setCookie(TOKENITEM, JSON.stringify(authinfo)); - }); + // Now find domain of our REST API, again with a fallback. + let baseDomain = FALLBACK_TEST_REST_DOMAIN; + if (!config.rest.host) { + console.warn("Could not load 'rest.host' from config.json. Falling back to " + FALLBACK_TEST_REST_DOMAIN); + } else { + baseDomain = config.rest.host; + } + + // Create a fake CSRF Token. Set it in the required server-side cookie + const csrfToken = 'fakeLoginCSRFToken'; + cy.setCookie(DSPACE_XSRF_COOKIE, csrfToken, { 'domain': baseDomain }); + + // Now, send login POST request including that CSRF token + cy.request({ + method: 'POST', + url: baseRestUrl + '/api/authn/login', + headers: { [XSRF_REQUEST_HEADER]: csrfToken}, + form: true, // indicates the body should be form urlencoded + body: { user: email, password: password } + }).then((resp) => { + // We expect a successful login + expect(resp.status).to.eq(200); + // We expect to have a valid authorization header returned (with our auth token) + expect(resp.headers).to.have.property('authorization'); + + // Initialize our AuthTokenInfo object from the authorization header. + const authheader = resp.headers.authorization as string; + const authinfo: AuthTokenInfo = new AuthTokenInfo(authheader); + + // Save our AuthTokenInfo object to our dsAuthInfo UI cookie + // This ensures the UI will recognize we are logged in on next "visit()" + cy.setCookie(TOKENITEM, JSON.stringify(authinfo)); }); + // Remove cookie with fake CSRF token, as it's no longer needed + cy.clearCookie(DSPACE_XSRF_COOKIE); }); } // Add as a Cypress command (i.e. assign to 'cy.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 { +function loginViaForm(email: string, password: string): void { // Enter email cy.get('ds-log-in [data-test="email"]').type(email); // Enter password @@ -107,4 +126,71 @@ Cypress.Commands.add('login', login); 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); \ No newline at end of file +Cypress.Commands.add('loginViaForm', loginViaForm); + + +/** + * Generate statistic view event for given object. Useful for testing statistics pages with + * pre-generated statistics. This just generates a single "hit", but can be called multiple times to + * generate multiple hits. + * + * NOTE: This requires that "solr-statistics.autoCommit=false" be set on the DSpace backend + * (as it is in our docker-compose-ci.yml used in CI). + * Otherwise, by default, new statistical events won't be saved until Solr's autocommit next triggers. + * @param uuid UUID of object + * @param dsoType type of DSpace Object (e.g. "item", "collection", "community") + */ +function generateViewEvent(uuid: string, dsoType: string): void { + // Cypress doesn't have access to the running application in Node.js. + // So, it's not possible to inject or load the AppConfig or environment of the Angular UI. + // Instead, we'll read our running application's config.json, which contains the configs & + // is regenerated at runtime each time the Angular UI application starts up. + cy.task('readUIConfig').then((str: string) => { + // Parse config into a JSON object + const config = JSON.parse(str); + + // Find the URL of our REST API. Have a fallback ready, just in case 'rest.baseUrl' cannot be found. + let baseRestUrl = FALLBACK_TEST_REST_BASE_URL; + if (!config.rest.baseUrl) { + console.warn("Could not load 'rest.baseUrl' from config.json. Falling back to " + FALLBACK_TEST_REST_BASE_URL); + } else { + baseRestUrl = config.rest.baseUrl; + } + + // Now find domain of our REST API, again with a fallback. + let baseDomain = FALLBACK_TEST_REST_DOMAIN; + if (!config.rest.host) { + console.warn("Could not load 'rest.host' from config.json. Falling back to " + FALLBACK_TEST_REST_DOMAIN); + } else { + baseDomain = config.rest.host; + } + + // Create a fake CSRF Token. Set it in the required server-side cookie + const csrfToken = 'fakeGenerateViewEventCSRFToken'; + cy.setCookie(DSPACE_XSRF_COOKIE, csrfToken, { 'domain': baseDomain }); + + // Now, send 'statistics/viewevents' POST request including that fake CSRF token in required header + cy.request({ + method: 'POST', + url: baseRestUrl + '/api/statistics/viewevents', + headers: { + [XSRF_REQUEST_HEADER] : csrfToken, + // use a known public IP address to avoid being seen as a "bot" + 'X-Forwarded-For': '1.1.1.1', + // Use a user-agent of a Firefox browser on Windows. This again avoids being seen as a "bot" + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0', + }, + //form: true, // indicates the body should be form urlencoded + body: { targetId: uuid, targetType: dsoType }, + }).then((resp) => { + // We expect a 201 (which means statistics event was created) + expect(resp.status).to.eq(201); + }); + + // Remove cookie with fake CSRF token, as it's no longer needed + cy.clearCookie(DSPACE_XSRF_COOKIE); + }); +} +// Add as a Cypress command (i.e. assign to 'cy.generateViewEvent') +Cypress.Commands.add('generateViewEvent', generateViewEvent); + diff --git a/cypress/support/index.ts b/cypress/support/e2e.ts similarity index 92% rename from cypress/support/index.ts rename to cypress/support/e2e.ts index 70da23f0447..dd7ee1824c4 100644 --- a/cypress/support/index.ts +++ b/cypress/support/e2e.ts @@ -30,11 +30,11 @@ beforeEach(() => { // For better stability between tests, we visit "about:blank" (i.e. blank page) after each test. // This ensures any remaining/outstanding XHR requests are killed, so they don't affect the next test. // Borrowed from: https://glebbahmutov.com/blog/visit-blank-page-between-tests/ -afterEach(() => { +/*afterEach(() => { cy.window().then((win) => { win.location.href = 'about:blank'; }); -}); +});*/ // Global constants used in tests @@ -43,10 +43,6 @@ afterEach(() => { // https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data // (This is the data set used in our CI environment) -// NOTE: FALLBACK_TEST_REST_BASE_URL is only used if Cypress cannot read the REST API BaseURL -// from the Angular UI's config.json. See 'getBaseRESTUrl()' in commands.ts -export const FALLBACK_TEST_REST_BASE_URL = 'http://localhost:8080/server'; - // Admin account used for administrative tests export const TEST_ADMIN_USER = Cypress.env('DSPACE_TEST_ADMIN_USER') || 'dspacedemo+admin@gmail.com'; export const TEST_ADMIN_PASSWORD = Cypress.env('DSPACE_TEST_ADMIN_PASSWORD') || 'dspace'; @@ -61,3 +57,10 @@ export const TEST_SUBMIT_COLLECTION_NAME = Cypress.env('DSPACE_TEST_SUBMIT_COLLE export const TEST_SUBMIT_COLLECTION_UUID = Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_UUID') || '9d8334e9-25d3-4a67-9cea-3dffdef80144'; export const TEST_SUBMIT_USER = Cypress.env('DSPACE_TEST_SUBMIT_USER') || 'dspacedemo+submit@gmail.com'; export const TEST_SUBMIT_USER_PASSWORD = Cypress.env('DSPACE_TEST_SUBMIT_USER_PASSWORD') || 'dspace'; + + +// USEFUL REGEX for testing + +// Match any string that contains at least one non-space character +// Can be used with "contains()" to determine if an element has a non-empty text value +export const REGEX_MATCH_NON_EMPTY_TEXT = /^(?!\s*$).+/; diff --git a/docker/README.md b/docker/README.md index 1a9fee0a815..37d071a86f8 100644 --- a/docker/README.md +++ b/docker/README.md @@ -6,7 +6,20 @@ If you wish to run DSpace on Docker in production, we recommend building your own Docker images. You are welcome to borrow ideas/concepts from the below images in doing so. But, the below images should not be used "as is" in any production scenario. *** -## 'Dockerfile' in root directory +## Overview +The scripts in this directory can be used to start the DSpace User Interface (frontend) in Docker. +Optionally, the backend (REST API) might also be started in Docker. + +For additional options/settings in starting the backend (REST API) in Docker, see the Docker Compose +documentation for the backend: https://github.com/DSpace/DSpace/blob/main/dspace/src/main/docker-compose/README.md + +## Root directory + +The root directory of this project contains all the Dockerfiles which may be referenced by +the Docker compose scripts in this 'docker' folder. + +### Dockerfile + This Dockerfile is used to build a *development* DSpace 7 Angular UI image, published as 'dspace/dspace-angular' ``` @@ -20,7 +33,18 @@ Admins to our DockerHub repo can manually publish with the following command. docker push dspace/dspace-angular:dspace-7_x ``` -## docker directory +### Dockerfile.dist + +The `Dockerfile.dist` is used to generate a *production* build and runtime environment. + +```bash +# build the latest image +docker build -f Dockerfile.dist -t dspace/dspace-angular:dspace-7_x-dist . +``` + +A default/demo version of this image is built *automatically*. + +## 'docker' directory - docker-compose.yml - Starts DSpace Angular with Docker Compose from the current branch. This file assumes that a DSpace 7 REST instance will also be started in Docker. - docker-compose-rest.yml @@ -45,23 +69,47 @@ docker-compose -f docker/docker-compose.yml build ## To start DSpace (REST and Angular) from your branch +This command provides a quick way to start both the frontend & backend from this single codebase ``` docker-compose -p d7 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml up -d ``` +Keep in mind, you may also start the backend by cloning the 'DSpace/DSpace' GitHub repository separately. See the next section. + + ## Run DSpace REST and DSpace Angular from local branches. + +This section assumes that you have clones *both* the 'DSpace/DSpace' and 'DSpace/dspace-angular' GitHub +repositories. When both are available locally, you can spin up both in Docker and have them work together. + _The system will be started in 2 steps. Each step shares the same docker network._ -From DSpace/DSpace (build as needed) +From 'DSpace/DSpace' clone (build first as needed): ``` docker-compose -p d7 up -d ``` -From DSpace/DSpace-angular +NOTE: More detailed instructions on starting the backend via Docker can be found in the [Docker Compose instructions for the Backend](https://github.com/DSpace/DSpace/blob/main/dspace/src/main/docker-compose/README.md). + +From 'DSpace/dspace-angular' clone (build first as needed) ``` docker-compose -p d7 -f docker/docker-compose.yml up -d ``` +At this point, you should be able to access the UI from http://localhost:4000, +and the backend at http://localhost:8080/server/ + +## Run DSpace Angular dist build with DSpace Demo site backend + +This allows you to run the Angular UI in *production* mode, pointing it at the demo or sandbox backend +(https://demo.dspace.org/server/ or https://sandbox.dspace.org/server/). + +``` +docker-compose -f docker/docker-compose-dist.yml pull +docker-compose -f docker/docker-compose-dist.yml build +docker-compose -p d7 -f docker/docker-compose-dist.yml up -d +``` + ## Ingest test data from AIPDIR Create an administrator @@ -87,9 +135,10 @@ Load assetstore content and trigger a re-index of the repository docker-compose -p d7 -f docker/cli.yml -f docker/cli.assetstore.yml run --rm dspace-cli ``` -## End to end testing of the rest api (runs in travis). -_In this instance, only the REST api runs in Docker using the Entities dataset. Travis will perform CI testing of Angular using Node to drive the tests._ +## End to end testing of the REST API (runs in GitHub Actions CI). +_In this instance, only the REST api runs in Docker using the Entities dataset. GitHub Actions will perform CI testing of Angular using Node to drive the tests. See `.github/workflows/build.yml` for more details._ +This command is only really useful for testing our Continuous Integration process. ``` -docker-compose -p d7ci -f docker/docker-compose-travis.yml up -d +docker-compose -p d7ci -f docker/docker-compose-ci.yml up -d ``` diff --git a/docker/docker-compose-ci.yml b/docker/docker-compose-ci.yml index ef84c14f43f..9ec8fe664a3 100644 --- a/docker/docker-compose-ci.yml +++ b/docker/docker-compose-ci.yml @@ -30,6 +30,9 @@ services: db__P__url: 'jdbc:postgresql://dspacedb:5432/dspace' # solr.server: Ensure we are using the 'dspacesolr' image for Solr solr__P__server: http://dspacesolr:8983/solr + # Tell Statistics to commit all views immediately instead of waiting on Solr's autocommit. + # This allows us to generate statistics in e2e tests so that statistics pages can be tested thoroughly. + solr__D__statistics__P__autoCommit: 'false' depends_on: - dspacedb image: dspace/dspace:dspace-7_x-test diff --git a/docker/docker-compose-dist.yml b/docker/docker-compose-dist.yml new file mode 100644 index 00000000000..00225e8052a --- /dev/null +++ b/docker/docker-compose-dist.yml @@ -0,0 +1,40 @@ +# +# The contents of this file are subject to the license and copyright +# detailed in the LICENSE and NOTICE files at the root of the source +# tree and available online at +# +# http://www.dspace.org/license/ +# + +# Docker Compose for running the DSpace Angular UI dist build +# for previewing with the DSpace Demo site backend +version: '3.7' +networks: + dspacenet: +services: + dspace-angular: + container_name: dspace-angular + environment: + DSPACE_UI_SSL: 'false' + DSPACE_UI_HOST: dspace-angular + DSPACE_UI_PORT: '4000' + DSPACE_UI_NAMESPACE: / + # NOTE: When running the UI in production mode (which the -dist image does), + # these DSPACE_REST_* variables MUST point at a public, HTTPS URL. + # This is because Server Side Rendering (SSR) currently requires a public URL, + # see this bug: https://github.com/DSpace/dspace-angular/issues/1485 + DSPACE_REST_SSL: 'true' + DSPACE_REST_HOST: demo.dspace.org + DSPACE_REST_PORT: 443 + DSPACE_REST_NAMESPACE: /server + image: dspace/dspace-angular:dspace-7_x-dist + build: + context: .. + dockerfile: Dockerfile.dist + networks: + dspacenet: + ports: + - published: 4000 + target: 4000 + stdin_open: true + tty: true diff --git a/docker/docker-compose-rest.yml b/docker/docker-compose-rest.yml index b73f1b7a390..e5f62600e70 100644 --- a/docker/docker-compose-rest.yml +++ b/docker/docker-compose-rest.yml @@ -39,7 +39,7 @@ services: # proxies.trusted.ipranges: This setting is required for a REST API running in Docker to trust requests # from the host machine. This IP range MUST correspond to the 'dspacenet' subnet defined above. proxies__P__trusted__P__ipranges: '172.23.0' - image: dspace/dspace:dspace-7_x-test + image: "${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-dspace-7_x-test}" depends_on: - dspacedb networks: @@ -82,8 +82,7 @@ services: # DSpace Solr container dspacesolr: container_name: dspacesolr - # Uses official Solr image at https://hub.docker.com/_/solr/ - image: solr:8.11-slim + image: "${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-dspace-7_x}" # Needs main 'dspace' container to start first to guarantee access to solr_configs depends_on: - dspace @@ -96,28 +95,26 @@ services: tty: true working_dir: /var/solr/data volumes: - # Mount our "solr_configs" volume available under the Solr's configsets folder (in a 'dspace' subfolder) - # This copies the Solr configs from main 'dspace' container into 'dspacesolr' via that volume - - solr_configs:/opt/solr/server/solr/configsets/dspace # Keep Solr data directory between reboots - solr_data:/var/solr/data # Initialize all DSpace Solr cores using the mounted local configsets (see above), then start Solr # * First, run precreate-core to create the core (if it doesn't yet exist). If exists already, this is a no-op - # * Second, copy updated configs from mounted configsets to this core. If it already existed, this updates core - # to the latest configs. If it's a newly created core, this is a no-op. + # * Second, copy configsets to this core: + # Updates to Solr configs require the container to be rebuilt/restarted: + # `docker-compose -p d7 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml up -d --build dspacesolr` entrypoint: - /bin/bash - '-c' - | init-var-solr - precreate-core authority /opt/solr/server/solr/configsets/dspace/authority - cp -r -u /opt/solr/server/solr/configsets/dspace/authority/* authority - precreate-core oai /opt/solr/server/solr/configsets/dspace/oai - cp -r -u /opt/solr/server/solr/configsets/dspace/oai/* oai - precreate-core search /opt/solr/server/solr/configsets/dspace/search - cp -r -u /opt/solr/server/solr/configsets/dspace/search/* search - precreate-core statistics /opt/solr/server/solr/configsets/dspace/statistics - cp -r -u /opt/solr/server/solr/configsets/dspace/statistics/* statistics + precreate-core authority /opt/solr/server/solr/configsets/authority + cp -r /opt/solr/server/solr/configsets/authority/* authority + precreate-core oai /opt/solr/server/solr/configsets/oai + cp -r /opt/solr/server/solr/configsets/oai/* oai + precreate-core search /opt/solr/server/solr/configsets/search + cp -r /opt/solr/server/solr/configsets/search/* search + precreate-core statistics /opt/solr/server/solr/configsets/statistics + cp -r /opt/solr/server/solr/configsets/statistics/* statistics exec solr -f volumes: assetstore: diff --git a/docker/dspace-ui.json b/docker/dspace-ui.json new file mode 100644 index 00000000000..0758679ab81 --- /dev/null +++ b/docker/dspace-ui.json @@ -0,0 +1,11 @@ +{ + "apps": [ + { + "name": "dspace-ui", + "cwd": "/app", + "script": "dist/server/main.js", + "instances": "max", + "exec_mode": "cluster" + } + ] +} \ No newline at end of file diff --git a/docs/Configuration.md b/docs/Configuration.md index 62fa444cc0f..01fd83c94d1 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -48,7 +48,7 @@ dspace-angular connects to your DSpace installation by using its REST endpoint. ```yaml rest: ssl: true - host: api7.dspace.org + host: demo.dspace.org port: 443 nameSpace: /server } @@ -57,7 +57,7 @@ rest: Alternately you can set the following environment variables. If any of these are set, it will override all configuration files: ``` DSPACE_REST_SSL=true - DSPACE_REST_HOST=api7.dspace.org + DSPACE_REST_HOST=demo.dspace.org DSPACE_REST_PORT=443 DSPACE_REST_NAMESPACE=/server ``` diff --git a/nohup.out b/nohup.out new file mode 100644 index 00000000000..02e53eef693 --- /dev/null +++ b/nohup.out @@ -0,0 +1,7948 @@ +yarn run v1.22.19 +$ ng config cli.cache.environment local && nodemon --exec "cross-env NODE_ENV=development yarn run serve" +[nodemon] 2.0.22 +[nodemon] to restart at any time, enter `rs` +[nodemon] watching path(s): config/**/* +[nodemon] watching extensions: json +[nodemon] starting `cross-env NODE_ENV=development yarn run serve` +node:events:491 + throw er; // Unhandled 'error' event + ^ + +Error: EBADF: bad file descriptor, read +Emitted 'error' event on ReadStream instance at: + at emitErrorNT (node:internal/streams/destroy:157:8) + at errorOrDestroy (node:internal/streams/destroy:220:7) + at node:internal/fs/streams:262:9 + at FSReqCallback.wrapper [as oncomplete] (node:fs:671:5) { + errno: -9, + code: 'EBADF', + syscall: 'read' +} +error Command failed with exit code 1. +info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command. +$ yarn base-href +$ ts-node --project ./tsconfig.ts-node.json scripts/base-href.ts +Building development app config +Overriding app config with /home/giuseppe/development/nodejs/dspace-angular-cris-7/config/config.yml +Overriding app config with /home/giuseppe/development/nodejs/dspace-angular-cris-7/config/config.dev.yml +Setting baseHref to / in angular.json +$ ts-node --project ./tsconfig.ts-node.json scripts/serve.ts +Building development app config +Overriding app config with /home/giuseppe/development/nodejs/dspace-angular-cris-7/config/config.yml +Overriding app config with /home/giuseppe/development/nodejs/dspace-angular-cris-7/config/config.dev.yml + +> dspace-angular@2023.02.00-SNAPSHOT ng-high-memory +> node --max_old_space_size=8192 node_modules/@angular/cli/bin/ng serve --host localhost --port 4000 --serve-path / --ssl false --configuration development + +- Generating browser application bundles (phase: setup)... +Building development app config +Overriding app config with /home/giuseppe/development/nodejs/dspace-angular-cris-7/config/config.yml +Overriding app config with /home/giuseppe/development/nodejs/dspace-angular-cris-7/config/config.dev.yml +Angular config.json file generated correctly at /home/giuseppe/development/nodejs/dspace-angular-cris-7/src/assets/config.json + +✔ Browser application bundle generation complete. + +Initial Chunk Files | Names | Raw Size +main.js | main | 8.51 MB | +vendor.js | vendor | 7.00 MB | +polyfills.js | polyfills | 1.03 MB | +styles.css, styles.js | styles | 215.99 kB | +runtime.js | runtime | 14.46 kB | + +| Initial Total | 16.76 MB + +Lazy Chunk Files | Names | Raw Size +src_app_core_export-service_browser-export_service_ts.js | core-export-service-browser-export-service | 4.68 MB | +node_modules_markdown-it-mathjax3_index_js.js | markdown-it-mathjax3 | 3.16 MB | +default-src_app_item-page_item-page_module_ts.js | item-page-item-page-module | 1.42 MB | +default-src_app_entity-groups_journal-entities_journal-entities_module_ts-src_app_entity-grou-77d389.js | home-page-home-page-module | 1.17 MB | +default-src_app_submission_submission_module_ts.js | submit-page-submit-page-module | 1.09 MB | +default-src_app_core_breadcrumbs_dso-breadcrumb_resolver_ts-src_app_item-page_edit-item-page_-d4e0bc.js | collection-page-collection-page-module | 982.64 kB | +default-src_app_shared_form_builder_ds-dynamic-form-ui_ds-dynamic-form-control-container_comp-f03307.js | home-page-home-page-module | 842.27 kB | +dspace-theme.css, dspace-theme.js | dspace-theme | 735.56 kB | +base-theme.css, base-theme.js | base-theme | 733.32 kB | +custom-theme.css, custom-theme.js | custom-theme | 733.14 kB | +src_app_admin_admin_module_ts-src_app_shared_access-control-form-container_access-control-for-9ad954.js | admin-admin-module | 669.73 kB | +default-src_app_my-dspace-page_my-dspace-search_module_ts.js | item-page-item-page-module | 644.91 kB | +default-src_app_shared_search_search_module_ts.js | home-page-home-page-module | 516.17 kB | +src_app_statistics-page_statistics-page-routing_module_ts.js | statistics-page-statistics-page-routing-module | 515.51 kB | +node_modules_canvg_lib_index_es_js.js | canvg | 444.14 kB | +node_modules_sanitize-html_index_js.js | sanitize-html | 369.70 kB | +src_app_process-page_process-page_module_ts.js | process-page-process-page-module | 360.18 kB | +default-src_app_openaire_openaire_module_ts.js | home-page-home-page-module | 343.72 kB | +node_modules_markdown-it_index_js.js | markdown-it | 256.82 kB | +node_modules_klaro_dist_klaro-no-translations_js.js | klaro-dist-klaro-no-translations | 238.82 kB | +src_app_collection-page_collection-page_module_ts-src_app_core_submission_metadatasecuritycon-e12537.js | collection-page-collection-page-module | 232.04 kB | +default-src_app_shared_context-menu_context-menu_module_ts.js | home-page-home-page-module | 228.58 kB | +default-src_app_item-page_full_full-item-page_component_ts.js | item-page-item-page-module | 221.71 kB | +src_app_community-page_community-page_module_ts-src_app_core_resolving_resolver_actions_ts.js | community-page-community-page-module | 201.16 kB | +src_app_info_info_module_ts.js | info-info-module | 176.82 kB | +src_app_collection-page_edit-collection-page_edit-collection-page_module_ts.js | edit-collection-page-edit-collection-page-module | 168.77 kB | +default-src_app_shared_form_form_module_ts.js | register-page-register-page-module | 155.65 kB | +default-src_app_profile-page_profile-page_module_ts.js | register-page-register-page-module | 154.85 kB | +src_app_health-page_health-page_module_ts.js | health-page-health-page-module | 141.52 kB | +src_app_request-copy_request-copy_module_ts.js | request-copy-request-copy-module | 128.18 kB | +default-src_app_external-log-in_external-login_module_ts.js | external-login-page-external-login-page-module | 124.10 kB | +default-src_app_shared_resource-policies_resource-policies_module_ts.js | collection-page-collection-page-module | 121.96 kB | +src_app_bitstream-page_bitstream-page_module_ts.js | bitstream-page-bitstream-page-module | 116.96 kB | +src_app_browse-by_browse-by-page_module_ts.js | browse-by-browse-by-page-module | 116.38 kB | +default-src_app_shared_comcol_comcol_module_ts.js | community-page-community-page-module | 92.28 kB | +default-src_app_item-page_versions_item-versions_component_ts-src_app_item-page_versions_noti-4172bf.js | collection-page-collection-page-module | 91.84 kB | +default-src_app_my-dspace-page_my-dspace-new-submission_my-dspace-new-bulk-import_my-dspace-n-3c519e.js | item-page-item-page-module | 84.56 kB | +default-src_app_shared_upload_uploader_uploader_component_ts.js | community-page-community-page-module | 84.18 kB | +default-src_app_openaire_reciter-suggestions_suggestions-notification_suggestions-notificatio-a79ff0.js | home-page-home-page-module | 80.90 kB | +src_app_register-page_register-page_module_ts.js | register-page-register-page-module | 75.59 kB | +src_app_external-login-review-account-info-page_external-login-review-account-info-page_module_ts.js | external-login-review-account-info-page-external-login-review-account-info-page-module | 72.50 kB | +src_app_workflowitems-edit-page_workflowitems-edit-page_module_ts.js | workflowitems-edit-page-workflowitems-edit-page-module | 70.18 kB | +default-src_app_shared_search_search-filters_search-filters_component_ts.js | home-page-home-page-module | 69.62 kB | +src_app_community-page_edit-community-page_edit-community-page_module_ts.js | edit-community-page-edit-community-page-module | 66.31 kB | +src_app_audit-page_audit-page_module_ts.js | audit-page-audit-page-module | 66.27 kB | +node_modules_jspdf_node_modules_dompurify_dist_purify_js.js | dompurify | 64.67 kB | +src_app_forgot-password_forgot-password_module_ts.js | forgot-password-forgot-password-module | 62.76 kB | +src_app_community-list-page_community-list-page_module_ts.js | community-list-page-community-list-page-module | 62.38 kB | +common.js | common | 58.95 kB | +default-src_app_shared_form_builder_ds-dynamic-form-ui_relation-lookup-modal_external-source--7b94eb.js | register-page-register-page-module | 58.91 kB | +default-src_app_item-page_full_field-components_file-section_full-file-section_component_ts.js | item-page-item-page-module | 58.65 kB | +default-src_app_shared_search_search-sidebar_search-sidebar_component_ts.js | home-page-home-page-module | 55.29 kB | +src_app_lookup-by-id_lookup-by-id_module_ts.js | lookup-by-id-lookup-by-id-module | 54.03 kB | +default-src_app_shared_dso-page_dso-page_module_ts.js | home-page-home-page-module | 53.50 kB | +src_app_suggestions-page_suggestions-page_module_ts.js | suggestions-page-suggestions-page-module | 51.34 kB | +src_app_workspaceitems-edit-page_workspaceitems-edit-page_module_ts.js | workspaceitems-edit-page-workspaceitems-edit-page-module | 49.57 kB | +src_themes_custom_app_search-page_configuration-search-page_component_ts.js | - | 47.86 kB | +src_app_home-page_home-page_module_ts.js | home-page-home-page-module | 47.18 kB | +src_themes_custom_app_dso-shared_dso-edit-metadata_dso-edit-metadata_component_ts.js | - | 45.99 kB | +default-node_modules_angular_cdk_fesm2020_tree_mjs.js | home-page-home-page-module | 44.19 kB | +default-src_app_shared_form_builder_ds-dynamic-form-ui_relation-lookup-modal_search-tab_dynam-156c97.js | register-page-register-page-module | 42.85 kB | +src_themes_custom_app_admin_admin-sidebar_admin-sidebar_component_ts.js | - | 40.25 kB | +default-src_app_shared_form_builder_ds-dynamic-form-ui_relation-lookup-modal_external-source--518ca3.js | register-page-register-page-module | 39.08 kB | +src_themes_custom_app_register-page_create-profile_create-profile_component_ts.js | - | 37.55 kB | +src_app_subscriptions-page_subscriptions-page-routing_module_ts.js | subscriptions-page-subscriptions-page-routing-module | 36.68 kB | +src_themes_custom_app_item-page_full_field-components_file-section_full-file-section_component_ts.js | - | 36.54 kB | +src_app_admin_admin-notifications_admin-notifications_module_ts.js | admin-notifications-admin-notifications-module | 35.54 kB | +src_themes_custom_app_profile-page_profile-page_component_ts.js | - | 33.49 kB | +src_themes_custom_app_shared_form_builder_ds-dynamic-form-ui_relation-lookup-modal_search-tab-b4dc26.js | - | 33.04 kB | +src_themes_custom_app_collection-page_collection-page_component_ts.js | - | 32.54 kB | +src_themes_custom_app_submission_import-external_submission-import-external_component_ts.js | - | 32.35 kB | +src_themes_custom_app_item-page_full_full-item-page_component_ts.js | - | 31.86 kB | +default-src_app_shared_subscriptions_subscriptions_module_ts.js | home-page-home-page-module | 31.65 kB | +src_themes_custom_app_shared_search_search-filters_search-filters_component_ts.js | - | 31.25 kB | +src_app_external-login-page_external-login-page_module_ts.js | external-login-page-external-login-page-module | 30.59 kB | +src_themes_custom_app_browse-by_browse-by-metadata-page_browse-by-metadata-page_component_ts.js | - | 29.97 kB | +src_themes_custom_app_shared_form_builder_ds-dynamic-form-ui_relation-lookup-modal_external-s-ff25a5.js | - | 29.88 kB | +src_themes_custom_app_browse-by_browse-by-title-page_browse-by-title-page_component_ts.js | - | 29.83 kB | +src_themes_custom_app_browse-by_browse-by-date-page_browse-by-date-page_component_ts.js | - | 29.78 kB | +src_app_lucky-search_lucky-search_module_ts.js | lucky-search-lucky-search-module | 29.60 kB | +src_themes_custom_app_submission_sections_upload_file_section-upload-file_component_ts.js | - | 29.00 kB | +src_themes_custom_app_footer_footer_component_ts.js | - | 28.97 kB | +src_themes_custom_app_shared_auth-nav-menu_auth-nav-menu_component_ts.js | - | 28.61 kB | +src_themes_custom_app_home-page_home-page_component_ts.js | - | 28.46 kB | +default-src_app_shared_upload_upload_module_ts.js | community-page-community-page-module | 27.89 kB | +src_themes_custom_app_community-page_community-page_component_ts.js | - | 27.85 kB | +src_app_invitation_invitation_module_ts.js | invitation-invitation-module | 27.59 kB | +src_themes_custom_app_shared_object-list_object-list_component_ts.js | - | 27.40 kB | +src_app_login-page_login-page_module_ts.js | login-page-login-page-module | 27.27 kB | +src_themes_custom_app_shared_form_builder_ds-dynamic-form-ui_relation-lookup-modal_external-s-db3f14.js | - | 27.19 kB | +src_app_bulk-import_bulk-import-page_module_ts.js | bulk-import-bulk-import-page-module | 26.96 kB | +src_themes_custom_app_item-page_media-viewer_media-viewer_component_ts.js | - | 26.66 kB | +src_themes_custom_app_breadcrumbs_breadcrumbs_component_ts.js | - | 26.57 kB | +src_themes_custom_app_info_feedback_feedback-form_feedback-form_component_ts.js | - | 26.27 kB | +src_themes_custom_app_bitstream-page_edit-bitstream-page_edit-bitstream-page_component_ts.js | - | 26.25 kB | +default-src_app_shared_comcol_comcol-page-browse-by_comcol-page-browse-by_component_ts.js | community-page-community-page-module | 26.19 kB | +src_themes_custom_app_navbar_expandable-navbar-section_expandable-navbar-section_component_ts.js | - | 25.97 kB | +src_themes_custom_app_item-page_simple_item-page_component_ts.js | - | 25.84 kB | +src_themes_custom_app_shared_collection-dropdown_collection-dropdown_component_ts.js | - | 24.99 kB | +default-src_themes_custom_app_thumbnail_thumbnail_component_ts.js | - | 24.12 kB | +src_themes_custom_app_shared_search_search-sidebar_search-sidebar_component_ts.js | - | 23.97 kB | +default-src_themes_custom_app_shared_loading_loading_component_ts.js | - | 23.37 kB | +src_themes_custom_app_item-page_media-viewer_media-viewer-video_media-viewer-video_component_ts.js | - | 23.25 kB | +src_app_logout-page_logout-page_module_ts.js | logout-page-logout-page-module | 22.78 kB | +src_themes_custom_app_navbar_navbar_component_ts.js | - | 22.69 kB | +src_themes_custom_app_collection-page_edit-item-template-page_edit-item-template-page_component_ts.js | - | 22.24 kB | +src_themes_custom_app_shared_comcol-page-browse-by_comcol-page-browse-by_component_ts.js | - | 21.72 kB | +src_themes_custom_app_root_root_component_ts.js | - | 21.66 kB | +default-src_app_shared_search_search-settings_search-settings_component_ts.js | home-page-home-page-module | 21.22 kB | +src_themes_custom_app_community-list-page_community-list_community-list_component_ts.js | - | 21.19 kB | +src_themes_custom_app_forgot-password_forgot-password-form_forgot-password-form_component_ts.js | - | 21.10 kB | +src_themes_custom_app_info_end-user-agreement_end-user-agreement_component_ts.js | - | 20.97 kB | +src_themes_custom_app_shared_search-form_search-form_component_ts.js | - | 20.85 kB | +src_app_explore-page_explore-page_module_ts.js | explore-page-explore-page-module | 20.55 kB | +src_themes_custom_app_community-page_sub-collection-list_community-page-sub-collection-list_c-b4080d.js | - | 20.28 kB | +src_themes_custom_app_community-page_sub-community-list_community-page-sub-community-list_com-047b39.js | - | 20.24 kB | +src_themes_custom_app_shared_search_search-settings_search-settings_component_ts.js | - | 20.19 kB | +src_themes_custom_app_browse-most-elements_browse-most-elements_component_ts.js | - | 20.10 kB | +src_themes_custom_app_search-navbar_search-navbar_component_ts.js | - | 20.09 kB | +src_themes_custom_app_home-page_home-news_home-news_component_ts.js | - | 19.98 kB | +src_themes_custom_app_header_header_component_ts.js | - | 19.87 kB | +src_themes_custom_app_my-dspace-page_my-dspace-page_component_ts.js | - | 19.84 kB | +src_themes_custom_app_shared_dso-selector_modal-wrappers_create-community-parent-selector_cre-612817.js | - | 19.57 kB | +src_app_import-external-page_import-external-page_module_ts.js | import-external-page-import-external-page-module | 19.23 kB | +src_app_external-login-email-confirmation-page_external-login-email-confirmation-page_module_ts.js | external-login-email-confirmation-page-external-login-email-confirmation-page-module | 18.97 kB | +src_themes_custom_app_shared_object-collection_shared_badges_badges_component_ts.js | - | 18.27 kB | +default-src_app_shared_search_search-results_search-results_component_ts.js | home-page-home-page-module | 18.22 kB | +src_themes_custom_app_shared_file-download-link_file-download-link_component_ts.js | - | 18.08 kB | +src_themes_custom_app_item-page_alerts_item-alerts_component_ts.js | - | 17.83 kB | +default-src_app_core_breadcrumbs_dso-context-breadcrumb_resolver_ts.js | statistics-page-statistics-page-routing-module | 17.42 kB | +src_themes_custom_app_browse-by_browse-by-taxonomy-page_browse-by-taxonomy-page_component_ts.js | - | 16.85 kB | +default-src_app_shared_comcol_comcol-page-handle_comcol-page-handle_component_ts.js | community-page-community-page-module | 16.63 kB | +default-src_app_my-dspace-page_collection-selector_collection-selector_component_ts.js | item-page-item-page-module | 16.35 kB | +src_themes_custom_app_pagenotfound_pagenotfound_component_ts.js | - | 16.31 kB | +src_themes_custom_app_item-page_media-viewer_media-viewer-image_media-viewer-image_component_ts.js | - | 16.30 kB | +src_themes_custom_app_forbidden_forbidden_component_ts.js | - | 16.19 kB | +src_themes_custom_app_login-page_login-page_component_ts.js | - | 16.08 kB | +src_themes_custom_app_lookup-by-id_objectnotfound_objectnotfound_component_ts.js | - | 16.07 kB | +src_themes_custom_app_header-nav-wrapper_header-navbar-wrapper_component_ts.js | - | 15.76 kB | +src_themes_custom_app_logout-page_logout-page_component_ts.js | - | 15.59 kB | +src_app_search-page_search-page-routing_module_ts.js | search-page-search-page-routing-module | 15.42 kB | +src_themes_custom_app_shared_object-collection_shared_badges_my-dspace-status-badge_my-dspace-ffcbf3.js | - | 15.36 kB | +src_themes_custom_app_submission_edit_submission-edit_component_ts.js | - | 15.25 kB | +src_themes_custom_app_shared_results-back-button_results-back-button_component_ts.js | - | 15.21 kB | +src_themes_custom_app_statistics-page_collection-statistics-page_collection-statistics-page_c-9ddab3.js | - | 15.04 kB | +src_themes_custom_app_statistics-page_community-statistics-page_community-statistics-page_com-097481.js | - | 15.02 kB | +src_themes_custom_app_statistics-page_item-statistics-page_item-statistics-page_component_ts.js | - | 14.85 kB | +src_themes_custom_app_statistics-page_site-statistics-page_site-statistics-page_component_ts.js | - | 14.85 kB | +src_themes_custom_app_register-page_register-email_register-email_component_ts.js | - | 14.70 kB | +src_themes_custom_app_forgot-password_forgot-password-email_forgot-email_component_ts.js | - | 14.70 kB | +src_themes_custom_app_info_feedback_feedback_component_ts.js | - | 14.39 kB | +src_themes_custom_app_info_privacy_privacy_component_ts.js | - | 14.33 kB | +src_themes_custom_app_submission_submit_submission-submit_component_ts.js | - | 13.97 kB | +default-node_modules_punycode_punycode_es6_js.js | markdown-it | 13.93 kB | +src_themes_custom_app_item-page_simple_field-components_file-section_file-section_component_ts.js | - | 13.57 kB | +src_themes_custom_app_item-page_simple_metadata-representation-list_metadata-representation-l-dac313.js | - | 12.84 kB | +src_themes_custom_app_item-page_edit-item-page_item-status_item-status_component_ts.js | - | 11.48 kB | +src_app_edit-item-relationships_edit-item-relationships_module_ts.js | edit-item-relationships-edit-item-relationships-module | 10.55 kB | +src_themes_custom_app_request-copy_grant-request-copy_grant-request-copy_component_ts.js | - | 10.01 kB | +src_themes_custom_app_request-copy_email-request-copy_email-request-copy_component_ts.js | - | 9.72 kB | +src_themes_custom_app_workflowitems-edit-page_workflow-item-send-back_workflow-item-send-back-25d77c.js | - | 7.60 kB | +src_themes_custom_app_workflowitems-edit-page_workflow-item-delete_workflow-item-delete_compo-e22128.js | - | 7.50 kB | +src_themes_custom_app_request-copy_deny-request-copy_deny-request-copy_component_ts.js | - | 7.49 kB | +src_themes_custom_app_shared_dso-selector_modal-wrappers_create-collection-parent-selector_cr-5094a7.js | - | 6.55 kB | +src_themes_custom_app_shared_dso-selector_modal-wrappers_create-item-parent-selector_create-i-f7c57c.js | - | 6.52 kB | +src_themes_custom_app_shared_dso-selector_modal-wrappers_edit-collection-selector_edit-collec-d105b5.js | - | 6.30 kB | +src_themes_custom_app_shared_dso-selector_modal-wrappers_edit-community-selector_edit-communi-84936c.js | - | 6.26 kB | +src_themes_custom_app_shared_dso-selector_modal-wrappers_edit-item-selector_edit-item-selecto-557c03.js | - | 6.10 kB | +src_app_my-dspace-page_my-dspace-page_module_ts.js | my-dspace-page-my-dspace-page-module | 5.69 kB | +src_app_submit-page_submit-page_module_ts.js | submit-page-submit-page-module | 4.85 kB | +src_themes_custom_app_shared_object-collection_shared_badges_access-status-badge_access-statu-f5960a.js | - | 4.65 kB | +src_themes_custom_app_shared_object-collection_shared_badges_status-badge_status-badge_component_ts.js | - | 4.59 kB | +src_themes_custom_app_item-page_simple_field-components_specific-field_title_item-page-title--1d1caa.js | - | 4.50 kB | +src_app_edit-item_edit-item_module_ts.js | edit-item-edit-item-module | 4.44 kB | +src_app_core_submission_metadatasecurityconfig-data_service_ts.js | item-page-item-page-module | 4.42 kB | +src_app_shared_access-control-form-container_access-control-form_module_ts.js | access-control-access-control-module | 3.85 kB | +src_themes_custom_app_community-list-page_community-list-page_component_ts.js | - | 3.64 kB | +src_themes_custom_app_shared_object-collection_shared_badges_type-badge_type-badge_component_ts.js | - | 3.57 kB | +src_themes_custom_app_search-page_search-page_component_ts.js | - | 3.40 kB | +src_themes_custom_app_browse-by_browse-by-switcher_browse-by-switcher_component_ts.js | - | 3.21 kB | +src_app_core_export-service_server-export_service_ts.js | core-export-service-server-export-service | 1.75 kB | + +Build at: 2023-11-08T18:27:15.526Z - Hash: c711f95358772a11 - Time: 82304ms + +Warning: /home/giuseppe/development/nodejs/dspace-angular-cris-7/node_modules/canvg/lib/index.es.js depends on 'raf'. CommonJS or AMD dependencies can cause optimization bailouts. +For more info see: https://angular.io/guide/build#configuring-commonjs-dependencies + +Warning: /home/giuseppe/development/nodejs/dspace-angular-cris-7/node_modules/canvg/lib/index.es.js depends on 'rgbcolor'. CommonJS or AMD dependencies can cause optimization bailouts. +For more info see: https://angular.io/guide/build#configuring-commonjs-dependencies + +Warning: /home/giuseppe/development/nodejs/dspace-angular-cris-7/node_modules/markdown-it-mathjax3/index.js depends on 'juice/client'. CommonJS or AMD dependencies can cause optimization bailouts. +For more info see: https://angular.io/guide/build#configuring-commonjs-dependencies + +Warning: /home/giuseppe/development/nodejs/dspace-angular-cris-7/node_modules/markdown-it-mathjax3/index.js depends on 'mathjax-full/js/adaptors/liteAdaptor.js'. CommonJS or AMD dependencies can cause optimization bailouts. +For more info see: https://angular.io/guide/build#configuring-commonjs-dependencies + +Warning: /home/giuseppe/development/nodejs/dspace-angular-cris-7/node_modules/markdown-it-mathjax3/index.js depends on 'mathjax-full/js/a11y/assistive-mml.js'. CommonJS or AMD dependencies can cause optimization bailouts. +For more info see: https://angular.io/guide/build#configuring-commonjs-dependencies + +Warning: /home/giuseppe/development/nodejs/dspace-angular-cris-7/node_modules/markdown-it-mathjax3/index.js depends on 'mathjax-full/js/handlers/html.js'. CommonJS or AMD dependencies can cause optimization bailouts. +For more info see: https://angular.io/guide/build#configuring-commonjs-dependencies + +Warning: /home/giuseppe/development/nodejs/dspace-angular-cris-7/node_modules/markdown-it-mathjax3/index.js depends on 'mathjax-full/js/input/tex.js'. CommonJS or AMD dependencies can cause optimization bailouts. +For more info see: https://angular.io/guide/build#configuring-commonjs-dependencies + +Warning: /home/giuseppe/development/nodejs/dspace-angular-cris-7/node_modules/markdown-it-mathjax3/index.js depends on 'mathjax-full/js/input/tex/AllPackages.js'. CommonJS or AMD dependencies can cause optimization bailouts. +For more info see: https://angular.io/guide/build#configuring-commonjs-dependencies + +Warning: /home/giuseppe/development/nodejs/dspace-angular-cris-7/node_modules/markdown-it-mathjax3/index.js depends on 'mathjax-full/js/mathjax.js'. CommonJS or AMD dependencies can cause optimization bailouts. +For more info see: https://angular.io/guide/build#configuring-commonjs-dependencies + +Warning: /home/giuseppe/development/nodejs/dspace-angular-cris-7/node_modules/markdown-it-mathjax3/index.js depends on 'mathjax-full/js/output/svg.js'. CommonJS or AMD dependencies can cause optimization bailouts. +For more info see: https://angular.io/guide/build#configuring-commonjs-dependencies + +Warning: /home/giuseppe/development/nodejs/dspace-angular-cris-7/node_modules/sanitize-html/index.js depends on 'escape-string-regexp'. CommonJS or AMD dependencies can cause optimization bailouts. +For more info see: https://angular.io/guide/build#configuring-commonjs-dependencies + +Warning: /home/giuseppe/development/nodejs/dspace-angular-cris-7/node_modules/sanitize-html/index.js depends on 'htmlparser2'. CommonJS or AMD dependencies can cause optimization bailouts. +For more info see: https://angular.io/guide/build#configuring-commonjs-dependencies + +Warning: /home/giuseppe/development/nodejs/dspace-angular-cris-7/node_modules/sanitize-html/index.js depends on 'is-plain-object'. CommonJS or AMD dependencies can cause optimization bailouts. +For more info see: https://angular.io/guide/build#configuring-commonjs-dependencies + +Warning: /home/giuseppe/development/nodejs/dspace-angular-cris-7/node_modules/sanitize-html/index.js depends on 'parse-srcset'. CommonJS or AMD dependencies can cause optimization bailouts. +For more info see: https://angular.io/guide/build#configuring-commonjs-dependencies + +Warning: /home/giuseppe/development/nodejs/dspace-angular-cris-7/node_modules/sanitize-html/index.js depends on 'postcss'. CommonJS or AMD dependencies can cause optimization bailouts. +For more info see: https://angular.io/guide/build#configuring-commonjs-dependencies + +Warning: /home/giuseppe/development/nodejs/dspace-angular-cris-7/src/app/shared/date.util.ts depends on 'date-fns-tz'. CommonJS or AMD dependencies can cause optimization bailouts. +For more info see: https://angular.io/guide/build#configuring-commonjs-dependencies + +Warning: /home/giuseppe/development/nodejs/dspace-angular-cris-7/src/app/shared/utils/file-size-pipe.ts depends on 'filesize'. CommonJS or AMD dependencies can cause optimization bailouts. +For more info see: https://angular.io/guide/build#configuring-commonjs-dependencies + +Warning: /home/giuseppe/development/nodejs/dspace-angular-cris-7/src/app/shared/utils/markdown.pipe.ts depends on 'markdown-it-mathjax3'. CommonJS or AMD dependencies can cause optimization bailouts. +For more info see: https://angular.io/guide/build#configuring-commonjs-dependencies + +Warning: /home/giuseppe/development/nodejs/dspace-angular-cris-7/src/app/shared/utils/markdown.pipe.ts depends on 'sanitize-html'. CommonJS or AMD dependencies can cause optimization bailouts. +For more info see: https://angular.io/guide/build#configuring-commonjs-dependencies + + + +Error: src/app/access-control/access-control.module.ts:33:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +33 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/access-control/access-control.module.ts:36:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +36 FormModule, + ~~~~~~~~~~ + + +Error: src/app/access-control/access-control.module.ts:38:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +38 SearchModule, + ~~~~~~~~~~~~ + + +Error: src/app/access-control/access-control.module.ts:39:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +39 AccessControlFormModule, + ~~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/admin/admin-notifications/admin-notifications.module.ts:14:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +14 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/admin/admin-notifications/admin-notifications.module.ts:17:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +17 OpenaireModule + ~~~~~~~~~~~~~~ + + +Error: src/app/admin/admin-registries/admin-registries.module.ts:16:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +16 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/admin/admin-registries/admin-registries.module.ts:18:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +18 BitstreamFormatsModule, + ~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/admin/admin-registries/admin-registries.module.ts:20:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +20 FormModule + ~~~~~~~~~~ + + +Error: src/app/admin/admin-registries/bitstream-formats/bitstream-formats.module.ts:15:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +15 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/admin/admin-registries/bitstream-formats/bitstream-formats.module.ts:18:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +18 FormModule + ~~~~~~~~~~ + + +Error: src/app/admin/admin-search-page/admin-search.module.ts:27:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 27 imports: [ + ~ + 28 SearchModule, + ~~~~~~~~~~~~~~~~~ +... + 31 ResearchEntitiesModule.withEntryComponents() + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 32 ], + ~~~ + + +Error: src/app/admin/admin-search-page/admin-search.module.ts:27:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 27 imports: [ + ~ + 28 SearchModule, + ~~~~~~~~~~~~~~~~~ +... + 31 ResearchEntitiesModule.withEntryComponents() + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 32 ], + ~~~ + + +Error: src/app/admin/admin-search-page/admin-search.module.ts:27:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 27 imports: [ + ~ + 28 SearchModule, + ~~~~~~~~~~~~~~~~~ +... + 31 ResearchEntitiesModule.withEntryComponents() + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 32 ], + ~~~ + + +Error: src/app/admin/admin-search-page/admin-search.module.ts:28:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +28 SearchModule, + ~~~~~~~~~~~~ + + +Error: src/app/admin/admin-workflow-page/admin-workflow.module.ts:40:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +40 imports: [ + ~ +41 SearchModule, + ~~~~~~~~~~~~~~~~~ +42 SharedModule.withEntryComponents() + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +43 ], + ~~~ + + +Error: src/app/admin/admin-workflow-page/admin-workflow.module.ts:41:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +41 SearchModule, + ~~~~~~~~~~~~ + + +Error: src/app/admin/admin.module.ts:26:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 26 imports: [ + ~ + 27 AdminRoutingModule, + ~~~~~~~~~~~~~~~~~~~~~~~ +... + 34 UploadModule, + ~~~~~~~~~~~~~~~~~ + 35 ], + ~~~ + + +Error: src/app/admin/admin.module.ts:26:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 26 imports: [ + ~ + 27 AdminRoutingModule, + ~~~~~~~~~~~~~~~~~~~~~~~ +... + 34 UploadModule, + ~~~~~~~~~~~~~~~~~ + 35 ], + ~~~ + + +Error: src/app/admin/admin.module.ts:28:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +28 AdminRegistriesModule, + ~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/admin/admin.module.ts:29:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +29 AccessControlModule, + ~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/admin/admin.module.ts:32:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +32 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/admin/admin.module.ts:34:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +34 UploadModule, + ~~~~~~~~~~~~ + + +Error: src/app/app.module.ts:123:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +123 imports: [ + ~ +124 BrowserModule.withServerTransition({ appId: 'dspace-angular' }), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +125 ...IMPORTS + ~~~~~~~~~~~~~~ +126 ], + ~~~ + + +Error: src/app/app.module.ts:123:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +123 imports: [ + ~ +124 BrowserModule.withServerTransition({ appId: 'dspace-angular' }), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +125 ...IMPORTS + ~~~~~~~~~~~~~~ +126 ], + ~~~ + + +Error: src/app/app.module.ts:123:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +123 imports: [ + ~ +124 BrowserModule.withServerTransition({ appId: 'dspace-angular' }), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +125 ...IMPORTS + ~~~~~~~~~~~~~~ +126 ], + ~~~ + + +Error: src/app/app.module.ts:123:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +123 imports: [ + ~ +124 BrowserModule.withServerTransition({ appId: 'dspace-angular' }), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +125 ...IMPORTS + ~~~~~~~~~~~~~~ +126 ], + ~~~ + + +Error: src/app/app.module.ts:123:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +123 imports: [ + ~ +124 BrowserModule.withServerTransition({ appId: 'dspace-angular' }), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +125 ...IMPORTS + ~~~~~~~~~~~~~~ +126 ], + ~~~ + + +Error: src/app/audit-page/audit-page.module.ts:11:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +11 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/bitstream-page/bitstream-page.module.ts:18:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +18 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/bitstream-page/bitstream-page.module.ts:20:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +20 FormModule, + ~~~~~~~~~~ + + +Error: src/app/bitstream-page/bitstream-page.module.ts:21:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +21 ResourcePoliciesModule + ~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/browse-by/browse-by-page.module.ts:10:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 10 imports: [ + ~ + 11 SharedBrowseByModule, + ~~~~~~~~~~~~~~~~~~~~~~~~~ +... + 13 BrowseByModule.withEntryComponents(), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 14 ], + ~~~ + + +Error: src/app/browse-by/browse-by-page.module.ts:11:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +11 SharedBrowseByModule, + ~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/browse-by/browse-by.module.ts:33:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +33 SharedBrowseByModule, + ~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/browse-by/browse-by.module.ts:35:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +35 ComcolModule, + ~~~~~~~~~~~~ + + +Error: src/app/browse-by/browse-by.module.ts:37:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +37 FormModule, + ~~~~~~~~~~ + + +Error: src/app/bulk-import/bulk-import-page.module.ts:14:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +14 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/collection-page/collection-form/collection-form.module.ts:10:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +10 ComcolModule, + ~~~~~~~~~~~~ + + +Error: src/app/collection-page/collection-form/collection-form.module.ts:11:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +11 FormModule, + ~~~~~~~~~~ + + +Error: src/app/collection-page/collection-form/collection-form.module.ts:12:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +12 SharedModule + ~~~~~~~~~~~~ + + +Error: src/app/collection-page/collection-page.module.ts:25:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 25 imports: [ + ~ + 26 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 35 ContextMenuModule.withEntryComponents(), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 36 ], + ~~~ + + +Error: src/app/collection-page/collection-page.module.ts:25:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 25 imports: [ + ~ + 26 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 35 ContextMenuModule.withEntryComponents(), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 36 ], + ~~~ + + +Error: src/app/collection-page/collection-page.module.ts:27:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +27 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/collection-page/collection-page.module.ts:30:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +30 EditItemPageModule, + ~~~~~~~~~~~~~~~~~~ + + +Error: src/app/collection-page/collection-page.module.ts:31:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +31 CollectionFormModule, + ~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/collection-page/collection-page.module.ts:32:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +32 ComcolModule, + ~~~~~~~~~~~~ + + +Error: src/app/collection-page/collection-page.module.ts:33:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +33 DsoSharedModule, + ~~~~~~~~~~~~~~~ + + +Error: src/app/collection-page/edit-collection-page/edit-collection-page.module.ts:27:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +27 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/collection-page/edit-collection-page/edit-collection-page.module.ts:29:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +29 CollectionFormModule, + ~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/collection-page/edit-collection-page/edit-collection-page.module.ts:30:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +30 ResourcePoliciesModule, + ~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/collection-page/edit-collection-page/edit-collection-page.module.ts:31:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +31 FormModule, + ~~~~~~~~~~ + + +Error: src/app/collection-page/edit-collection-page/edit-collection-page.module.ts:32:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +32 ComcolModule, + ~~~~~~~~~~~~ + + +Error: src/app/collection-page/edit-collection-page/edit-collection-page.module.ts:33:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +33 AccessControlFormModule, + ~~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/community-list-page/community-list-page.module.ts:24:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +24 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/community-page/community-form/community-form.module.ts:10:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +10 ComcolModule, + ~~~~~~~~~~~~ + + +Error: src/app/community-page/community-form/community-form.module.ts:11:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +11 FormModule, + ~~~~~~~~~~ + + +Error: src/app/community-page/community-form/community-form.module.ts:12:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +12 SharedModule + ~~~~~~~~~~~~ + + +Error: src/app/community-page/community-page.module.ts:35:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 35 imports: [ + ~ + 36 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 43 ContextMenuModule.withEntryComponents(), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 44 ], + ~~~ + + +Error: src/app/community-page/community-page.module.ts:35:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 35 imports: [ + ~ + 36 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 43 ContextMenuModule.withEntryComponents(), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 44 ], + ~~~ + + +Error: src/app/community-page/community-page.module.ts:37:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +37 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/community-page/community-page.module.ts:40:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +40 CommunityFormModule, + ~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/community-page/community-page.module.ts:41:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +41 ComcolModule, + ~~~~~~~~~~~~ + + +Error: src/app/community-page/edit-community-page/edit-community-page.module.ts:24:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +24 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/community-page/edit-community-page/edit-community-page.module.ts:26:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +26 CommunityFormModule, + ~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/community-page/edit-community-page/edit-community-page.module.ts:27:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +27 ComcolModule, + ~~~~~~~~~~~~ + + +Error: src/app/community-page/edit-community-page/edit-community-page.module.ts:28:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +28 ResourcePoliciesModule, + ~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/community-page/edit-community-page/edit-community-page.module.ts:29:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +29 AccessControlFormModule, + ~~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/cris-item-page/cris-item-page.module.ts:17:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +17 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/cris-item-page/cris-item-page.module.ts:18:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +18 CrisLayoutModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/app/cris-item-page/cris-item-page.module.ts:19:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +19 StatisticsModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/app/cris-item-page/cris-item-page.module.ts:21:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +21 ItemSharedModule + ~~~~~~~~~~~~~~~~ + + +Error: src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metadata/rendering-types/advanced-attachment/bitstream-attachment/attachment-render/attachment-rendering.module.ts:18:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +18 SearchModule, + ~~~~~~~~~~~~ + + +Error: src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metadata/rendering-types/advanced-attachment/bitstream-attachment/attachment-render/attachment-rendering.module.ts:19:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +19 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/cris-layout/cris-layout.module.ts:166:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +166 imports: [ + ~ +167 CommonModule, + ~~~~~~~~~~~~~~~~~ +... +177 FormModule + ~~~~~~~~~~~~~~ +178 ], + ~~~ + + +Error: src/app/cris-layout/cris-layout.module.ts:166:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +166 imports: [ + ~ +167 CommonModule, + ~~~~~~~~~~~~~~~~~ +... +177 FormModule + ~~~~~~~~~~~~~~ +178 ], + ~~~ + + +Error: src/app/cris-layout/cris-layout.module.ts:168:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +168 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/cris-layout/cris-layout.module.ts:170:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +170 MyDSpacePageModule, + ~~~~~~~~~~~~~~~~~~ + + +Error: src/app/cris-layout/cris-layout.module.ts:173:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +173 ComcolModule, + ~~~~~~~~~~~~ + + +Error: src/app/cris-layout/cris-layout.module.ts:176:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +176 AttachmentRenderingModule, + ~~~~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/cris-layout/cris-layout.module.ts:177:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +177 FormModule + ~~~~~~~~~~ + + +Error: src/app/dso-shared/dso-shared.module.ts:13:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +13 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/edit-item-relationships/edit-item-relationships.module.ts:13:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 13 imports: [ + ~ + 14 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 18 SearchModule.withEntryComponents() + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 19 ] + ~~~ + + +Error: src/app/edit-item-relationships/edit-item-relationships.module.ts:13:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 13 imports: [ + ~ + 14 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 18 SearchModule.withEntryComponents() + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 19 ] + ~~~ + + +Error: src/app/edit-item-relationships/edit-item-relationships.module.ts:13:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 13 imports: [ + ~ + 14 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 18 SearchModule.withEntryComponents() + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 19 ] + ~~~ + + +Error: src/app/edit-item/edit-item.module.ts:11:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +11 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/edit-item/edit-item.module.ts:12:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +12 SubmissionModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/app/entity-groups/journal-entities/journal-entities.module.ts:50:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 50 imports: [ + ~ + 51 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 56 ContextMenuModule.withEntryComponents(), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 57 ], + ~~~ + + +Error: src/app/entity-groups/journal-entities/journal-entities.module.ts:52:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +52 ItemSharedModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/app/entity-groups/journal-entities/journal-entities.module.ts:53:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +53 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/entity-groups/research-entities/research-entities.module.ts:73:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 73 imports: [ + ~ + 74 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 81 ContextMenuModule.withEntryComponents(), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 82 ], + ~~~ + + +Error: src/app/entity-groups/research-entities/research-entities.module.ts:75:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +75 ItemSharedModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/app/entity-groups/research-entities/research-entities.module.ts:76:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +76 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/explore-page/explore-page.module.ts:13:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +13 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/explore-page/explore-page.module.ts:14:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +14 ExploreModule + ~~~~~~~~~~~~~ + + +Error: src/app/external-log-in/external-login.module.ts:21:27 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +21 imports: [CommonModule, SharedModule], + ~~~~~~~~~~~~ + + +Error: src/app/external-login-email-confirmation-page/external-login-email-confirmation-page.module.ts:18:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +18 ExternalLoginModule, + ~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/external-login-page/external-login-page.module.ts:22:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +22 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/external-login-page/external-login-page.module.ts:23:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +23 ExternalLoginModule + ~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/external-login-review-account-info-page/external-login-review-account-info-page.module.ts:25:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +25 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/external-login-review-account-info-page/external-login-review-account-info-page.module.ts:27:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +27 ExternalLoginModule + ~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/footer/footer.module.ts:19:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +19 ExploreModule, + ~~~~~~~~~~~~~ + + +Error: src/app/footer/footer.module.ts:20:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +20 SharedModule + ~~~~~~~~~~~~ + + +Error: src/app/forgot-password/forgot-password.module.ts:15:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +15 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/forgot-password/forgot-password.module.ts:17:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +17 RegisterEmailFormModule, + ~~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/forgot-password/forgot-password.module.ts:18:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +18 ProfilePageModule, + ~~~~~~~~~~~~~~~~~ + + +Error: src/app/health-page/health-page.module.ts:22:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +22 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/home-page/home-page.module.ts:24:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 24 imports: [ + ~ + 25 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 32 StatisticsModule.forRoot() + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 33 ], + ~~~ + + +Error: src/app/home-page/home-page.module.ts:24:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 24 imports: [ + ~ + 25 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 32 StatisticsModule.forRoot() + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 33 ], + ~~~ + + +Error: src/app/home-page/home-page.module.ts:24:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 24 imports: [ + ~ + 25 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 32 StatisticsModule.forRoot() + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 33 ], + ~~~ + + +Error: src/app/home-page/home-page.module.ts:24:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 24 imports: [ + ~ + 25 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 32 StatisticsModule.forRoot() + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 33 ], + ~~~ + + +Error: src/app/home-page/home-page.module.ts:29:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +29 ExploreModule, + ~~~~~~~~~~~~~ + + +Error: src/app/home-page/home-page.module.ts:31:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +31 OpenaireModule, + ~~~~~~~~~~~~~~ + + +Error: src/app/home-page/top-level-community-list/top-level-community-list.component.html:4:45 - error NG8004: No pipe found with name 'translate'. + +4 {{'home.top-level-communities.head' | translate}} + ~~~~~~~~~ + + src/app/home-page/top-level-community-list/top-level-community-list.component.ts:23:16 + 23 templateUrl: './top-level-community-list.component.html', + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Error occurs in the template of component TopLevelCommunityListComponent. + + +Error: src/app/home-page/top-level-community-list/top-level-community-list.component.html:6:57 - error NG8004: No pipe found with name 'translate'. + +6

{{'home.top-level-communities.help' | translate}}

+ ~~~~~~~~~ + + src/app/home-page/top-level-community-list/top-level-community-list.component.ts:23:16 + 23 templateUrl: './top-level-community-list.component.html', + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Error occurs in the template of component TopLevelCommunityListComponent. + + +Error: src/app/home-page/top-level-community-list/top-level-community-list.component.html:7:3 - error NG8001: 'ds-viewable-collection' is not a known element: +1. If 'ds-viewable-collection' is an Angular component, then verify that it is part of this module. +2. If 'ds-viewable-collection' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message. + + 7 + ~~~~~~~~~~~~~~~~~~~~~~ + + src/app/home-page/top-level-community-list/top-level-community-list.component.ts:23:16 + 23 templateUrl: './top-level-community-list.component.html', + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Error occurs in the template of component TopLevelCommunityListComponent. + + +Error: src/app/home-page/top-level-community-list/top-level-community-list.component.html:8:5 - error NG8002: Can't bind to 'config' since it isn't a known property of 'ds-viewable-collection'. +1. If 'ds-viewable-collection' is an Angular component and it has 'config' input, then verify that it is part of this module. +2. If 'ds-viewable-collection' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message. +3. To allow any property add 'NO_ERRORS_SCHEMA' to the '@NgModule.schemas' of this component. + +8 [config]="config" + ~~~~~~~~~~~~~~~~~ + + src/app/home-page/top-level-community-list/top-level-community-list.component.ts:23:16 + 23 templateUrl: './top-level-community-list.component.html', + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Error occurs in the template of component TopLevelCommunityListComponent. + + +Error: src/app/home-page/top-level-community-list/top-level-community-list.component.html:9:5 - error NG8002: Can't bind to 'sortConfig' since it isn't a known property of 'ds-viewable-collection'. +1. If 'ds-viewable-collection' is an Angular component and it has 'sortConfig' input, then verify that it is part of this module. +2. If 'ds-viewable-collection' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message. +3. To allow any property add 'NO_ERRORS_SCHEMA' to the '@NgModule.schemas' of this component. + +9 [sortConfig]="sortConfig" + ~~~~~~~~~~~~~~~~~~~~~~~~~ + + src/app/home-page/top-level-community-list/top-level-community-list.component.ts:23:16 + 23 templateUrl: './top-level-community-list.component.html', + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Error occurs in the template of component TopLevelCommunityListComponent. + + +Error: src/app/home-page/top-level-community-list/top-level-community-list.component.html:10:5 - error NG8002: Can't bind to 'objects' since it isn't a known property of 'ds-viewable-collection'. +1. If 'ds-viewable-collection' is an Angular component and it has 'objects' input, then verify that it is part of this module. +2. If 'ds-viewable-collection' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message. +3. To allow any property add 'NO_ERRORS_SCHEMA' to the '@NgModule.schemas' of this component. + +10 [objects]="communitiesRD$ | async" + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + src/app/home-page/top-level-community-list/top-level-community-list.component.ts:23:16 + 23 templateUrl: './top-level-community-list.component.html', + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Error occurs in the template of component TopLevelCommunityListComponent. + + +Error: src/app/home-page/top-level-community-list/top-level-community-list.component.html:10:33 - error NG8004: No pipe found with name 'async'. + +10 [objects]="communitiesRD$ | async" + ~~~~~ + + src/app/home-page/top-level-community-list/top-level-community-list.component.ts:23:16 + 23 templateUrl: './top-level-community-list.component.html', + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Error occurs in the template of component TopLevelCommunityListComponent. + + +Error: src/app/home-page/top-level-community-list/top-level-community-list.component.html:11:5 - error NG8002: Can't bind to 'hideGear' since it isn't a known property of 'ds-viewable-collection'. +1. If 'ds-viewable-collection' is an Angular component and it has 'hideGear' input, then verify that it is part of this module. +2. If 'ds-viewable-collection' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message. +3. To allow any property add 'NO_ERRORS_SCHEMA' to the '@NgModule.schemas' of this component. + +11 [hideGear]="true"> + ~~~~~~~~~~~~~~~~~ + + src/app/home-page/top-level-community-list/top-level-community-list.component.ts:23:16 + 23 templateUrl: './top-level-community-list.component.html', + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Error occurs in the template of component TopLevelCommunityListComponent. + + +Error: src/app/home-page/top-level-community-list/top-level-community-list.component.html:14:1 - error NG8001: 'ds-error' is not a known element: +1. If 'ds-error' is an Angular component, then verify that it is part of this module. +2. If 'ds-error' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message. + +14 + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + src/app/home-page/top-level-community-list/top-level-community-list.component.ts:23:16 + 23 templateUrl: './top-level-community-list.component.html', + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Error occurs in the template of component TopLevelCommunityListComponent. + + +Error: src/app/home-page/top-level-community-list/top-level-community-list.component.html:14:45 - error NG8002: Can't bind to 'message' since it isn't a known property of 'ds-error'. +1. If 'ds-error' is an Angular component and it has 'message' input, then verify that it is part of this module. +2. If 'ds-error' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message. +3. To allow any property add 'NO_ERRORS_SCHEMA' to the '@NgModule.schemas' of this component. + +14 + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + src/app/home-page/top-level-community-list/top-level-community-list.component.ts:23:16 + 23 templateUrl: './top-level-community-list.component.html', + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Error occurs in the template of component TopLevelCommunityListComponent. + + +Error: src/app/home-page/top-level-community-list/top-level-community-list.component.html:14:87 - error NG8004: No pipe found with name 'translate'. + +14 + ~~~~~~~~~ + + src/app/home-page/top-level-community-list/top-level-community-list.component.ts:23:16 + 23 templateUrl: './top-level-community-list.component.html', + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Error occurs in the template of component TopLevelCommunityListComponent. + + +Error: src/app/home-page/top-level-community-list/top-level-community-list.component.html:15:1 - error NG8001: 'ds-themed-loading' is not a known element: +1. If 'ds-themed-loading' is an Angular component, then verify that it is part of this module. +2. If 'ds-themed-loading' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message. + +15 + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + src/app/home-page/top-level-community-list/top-level-community-list.component.ts:23:16 + 23 templateUrl: './top-level-community-list.component.html', + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Error occurs in the template of component TopLevelCommunityListComponent. + + +Error: src/app/home-page/top-level-community-list/top-level-community-list.component.html:15:54 - error NG8002: Can't bind to 'message' since it isn't a known property of 'ds-themed-loading'. +1. If 'ds-themed-loading' is an Angular component and it has 'message' input, then verify that it is part of this module. +2. If 'ds-themed-loading' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message. +3. To allow any property add 'NO_ERRORS_SCHEMA' to the '@NgModule.schemas' of this component. + +15 + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + src/app/home-page/top-level-community-list/top-level-community-list.component.ts:23:16 + 23 templateUrl: './top-level-community-list.component.html', + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Error occurs in the template of component TopLevelCommunityListComponent. + + +Error: src/app/home-page/top-level-community-list/top-level-community-list.component.html:15:99 - error NG8004: No pipe found with name 'translate'. + +15 + ~~~~~~~~~ + + src/app/home-page/top-level-community-list/top-level-community-list.component.ts:23:16 + 23 templateUrl: './top-level-community-list.component.html', + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Error occurs in the template of component TopLevelCommunityListComponent. + + +Error: src/app/import-external-page/import-external-page.module.ts:13:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 13 imports: [ + ~ + 14 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 20 ResearchEntitiesModule.withEntryComponents() + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 21 ], + ~~~ + + +Error: src/app/import-external-page/import-external-page.module.ts:13:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 13 imports: [ + ~ + 14 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 20 ResearchEntitiesModule.withEntryComponents() + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 21 ], + ~~~ + + +Error: src/app/import-external-page/import-external-page.module.ts:13:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 13 imports: [ + ~ + 14 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 20 ResearchEntitiesModule.withEntryComponents() + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 21 ], + ~~~ + + +Error: src/app/import-external-page/import-external-page.module.ts:18:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +18 SubmissionModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/app/info/info.module.ts:35:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +35 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/invitation/invitation.module.ts:12:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +12 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/item-page/alerts/item-alerts.component.ts:13:14 - error NG6007: The Component 'ItemAlertsComponent' is declared by more than one NgModule. + +13 export class ItemAlertsComponent { + ~~~~~~~~~~~~~~~~~~~ + + src/app/item-page/item-shared.module.ts:60:14 + 60 export class ItemSharedModule { } + ~~~~~~~~~~~~~~~~ + 'ItemAlertsComponent' is listed in the declarations of the NgModule 'ItemSharedModule'. + src/app/item-page/item-page.module.ts:137:14 + 137 export class ItemPageModule { + ~~~~~~~~~~~~~~ + 'ItemAlertsComponent' is listed in the declarations of the NgModule 'ItemPageModule'. + + +Error: src/app/item-page/edit-item-page/edit-item-page.module.ts:59:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +59 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/item-page/edit-item-page/edit-item-page.module.ts:62:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +62 SearchPageModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/app/item-page/edit-item-page/edit-item-page.module.ts:64:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +64 ResourcePoliciesModule, + ~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/item-page/edit-item-page/edit-item-page.module.ts:66:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +66 ItemVersionsModule, + ~~~~~~~~~~~~~~~~~~ + + +Error: src/app/item-page/edit-item-page/edit-item-page.module.ts:67:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +67 DsoSharedModule, + ~~~~~~~~~~~~~~~ + + +Error: src/app/item-page/edit-item-page/edit-item-page.module.ts:69:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +69 AccessControlFormModule, + ~~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/item-page/item-page.module.ts:110:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +110 imports: [ + ~ +111 CommonModule, + ~~~~~~~~~~~~~~~~~ +... +127 MiradorViewerModule, + ~~~~~~~~~~~~~~~~~~~~~~~~ +128 ], + ~~~ + + +Error: src/app/item-page/item-page.module.ts:110:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +110 imports: [ + ~ +111 CommonModule, + ~~~~~~~~~~~~~~~~~ +... +127 MiradorViewerModule, + ~~~~~~~~~~~~~~~~~~~~~~~~ +128 ], + ~~~ + + +Error: src/app/item-page/item-page.module.ts:110:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +110 imports: [ + ~ +111 CommonModule, + ~~~~~~~~~~~~~~~~~ +... +127 MiradorViewerModule, + ~~~~~~~~~~~~~~~~~~~~~~~~ +128 ], + ~~~ + + +Error: src/app/item-page/item-page.module.ts:110:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +110 imports: [ + ~ +111 CommonModule, + ~~~~~~~~~~~~~~~~~ +... +127 MiradorViewerModule, + ~~~~~~~~~~~~~~~~~~~~~~~~ +128 ], + ~~~ + + +Error: src/app/item-page/item-page.module.ts:110:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +110 imports: [ + ~ +111 CommonModule, + ~~~~~~~~~~~~~~~~~ +... +127 MiradorViewerModule, + ~~~~~~~~~~~~~~~~~~~~~~~~ +128 ], + ~~~ + + +Error: src/app/item-page/item-page.module.ts:114:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +114 EditItemPageModule, + ~~~~~~~~~~~~~~~~~~ + + +Error: src/app/item-page/item-page.module.ts:115:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +115 ItemVersionsModule, + ~~~~~~~~~~~~~~~~~~ + + +Error: src/app/item-page/item-page.module.ts:116:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +116 ItemSharedModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/app/item-page/item-page.module.ts:123:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +123 UploadModule, + ~~~~~~~~~~~~ + + +Error: src/app/item-page/item-page.module.ts:125:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +125 CrisItemPageModule, + ~~~~~~~~~~~~~~~~~~ + + +Error: src/app/item-page/item-shared.module.ts:45:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +45 SearchModule, + ~~~~~~~~~~~~ + + +Error: src/app/item-page/item-shared.module.ts:46:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +46 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/item-page/versions/item-versions.module.ts:21:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +21 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/login-page/login-page.module.ts:12:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +12 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/logout-page/logout-page.module.ts:12:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +12 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/lookup-by-id/lookup-by-id.module.ts:15:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +15 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/lucky-search/lucky-search.module.ts:12:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 12 imports: [ + ~ + 13 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 16 LuckySearchRoutingModule + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 17 ] + ~~~ + + +Error: src/app/lucky-search/lucky-search.module.ts:12:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 12 imports: [ + ~ + 13 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 16 LuckySearchRoutingModule + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 17 ] + ~~~ + + +Error: src/app/my-dspace-page/my-dspace-page.module.ts:38:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 38 imports: [ + ~ + 39 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 45 UploadModule, + ~~~~~~~~~~~~~~~~~ + 46 ], + ~~~ + + +Error: src/app/my-dspace-page/my-dspace-page.module.ts:40:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +40 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/my-dspace-page/my-dspace-page.module.ts:41:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +41 SearchModule, + ~~~~~~~~~~~~ + + +Error: src/app/my-dspace-page/my-dspace-page.module.ts:44:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +44 OpenaireModule, + ~~~~~~~~~~~~~~ + + +Error: src/app/my-dspace-page/my-dspace-page.module.ts:45:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +45 UploadModule, + ~~~~~~~~~~~~ + + +Error: src/app/my-dspace-page/my-dspace-search.module.ts:53:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 53 imports: [ + ~ + 54 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 59 JournalEntitiesModule.withEntryComponents(), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 60 ], + ~~~ + + +Error: src/app/my-dspace-page/my-dspace-search.module.ts:53:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 53 imports: [ + ~ + 54 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 59 JournalEntitiesModule.withEntryComponents(), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 60 ], + ~~~ + + +Error: src/app/my-dspace-page/my-dspace-search.module.ts:55:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +55 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/my-dspace-page/my-dspace-search.module.ts:57:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +57 MyDSpaceActionsModule, + ~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/navbar/navbar.module.ts:31:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +31 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/openaire/openaire.module.ts:87:14 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +87 imports: [ + ~ +88 ...MODULES, + ~~~~~~~~~~~~~~~~~~~ +89 SearchModule + ~~~~~~~~~~~~~~~~~~~~ +90 ], + ~~~~~ + + +Error: src/app/openaire/openaire.module.ts:89:9 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +89 SearchModule + ~~~~~~~~~~~~ + + +Error: src/app/process-page/process-page-shared.module.ts:22:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +22 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/process-page/process-page.module.ts:10:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +10 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/process-page/process-page.module.ts:11:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +11 ProcessPageSharedModule, + ~~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/profile-page/profile-page.module.ts:22:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +22 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/profile-page/profile-page.module.ts:23:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +23 FormModule, + ~~~~~~~~~~ + + +Error: src/app/profile-page/profile-page.module.ts:25:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +25 OpenaireModule + ~~~~~~~~~~~~~~ + + +Error: src/app/register-email-form/register-email-form.module.ts:9:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +9 SharedModule + ~~~~~~~~~~~~ + + +Error: src/app/register-page/register-page.module.ts:15:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +15 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/register-page/register-page.module.ts:17:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +17 RegisterEmailFormModule, + ~~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/register-page/register-page.module.ts:18:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +18 ProfilePageModule, + ~~~~~~~~~~~~~~~~~ + + +Error: src/app/request-copy/request-copy.module.ts:16:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +16 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/root.module.ts:98:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 98 imports: [ + ~ + 99 ...IMPORTS + ~~~~~~~~~~~~~~ +100 ], + ~~~ + + +Error: src/app/root.module.ts:98:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 98 imports: [ + ~ + 99 ...IMPORTS + ~~~~~~~~~~~~~~ +100 ], + ~~~ + + +Error: src/app/root.module.ts:98:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 98 imports: [ + ~ + 99 ...IMPORTS + ~~~~~~~~~~~~~~ +100 ], + ~~~ + + +Error: src/app/root.module.ts:98:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 98 imports: [ + ~ + 99 ...IMPORTS + ~~~~~~~~~~~~~~ +100 ], + ~~~ + + +Error: src/app/root.module.ts:98:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 98 imports: [ + ~ + 99 ...IMPORTS + ~~~~~~~~~~~~~~ +100 ], + ~~~ + + +Error: src/app/search-page/search-page-routing.module.ts:13:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +13 SearchPageModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/app/search-page/search-page.module.ts:22:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 22 imports: [ + ~ + 23 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 29 ResearchEntitiesModule.withEntryComponents() + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 30 ], + ~~~ + + +Error: src/app/search-page/search-page.module.ts:22:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 22 imports: [ + ~ + 23 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 29 ResearchEntitiesModule.withEntryComponents() + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 30 ], + ~~~ + + +Error: src/app/search-page/search-page.module.ts:22:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 22 imports: [ + ~ + 23 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 29 ResearchEntitiesModule.withEntryComponents() + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 30 ], + ~~~ + + +Error: src/app/search-page/search-page.module.ts:22:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 22 imports: [ + ~ + 23 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 29 ResearchEntitiesModule.withEntryComponents() + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 30 ], + ~~~ + + +Error: src/app/search-page/search-page.module.ts:24:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +24 SearchModule, + ~~~~~~~~~~~~ + + +Error: src/app/shared/access-control-form-container/access-control-form.module.ts:19:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +19 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/shared/browse-by/shared-browse-by.module.ts:18:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +18 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/shared/browse-by/shared-browse-by.module.ts:22:5 - error NG6003: This export contains errors, which may affect components that depend on this NgModule. + +22 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/shared/comcol/comcol.module.ts:41:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +41 FormModule, + ~~~~~~~~~~ + + +Error: src/app/shared/comcol/comcol.module.ts:42:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +42 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/shared/comcol/comcol.module.ts:43:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +43 UploadModule, + ~~~~~~~~~~~~ + + +Error: src/app/shared/comcol/comcol.module.ts:47:5 - error NG6003: This export contains errors, which may affect components that depend on this NgModule. + +47 UploadModule, + ~~~~~~~~~~~~ + + +Error: src/app/shared/context-menu/context-menu.module.ts:70:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +70 imports: [ + ~ +71 MODULE, + ~~~~~~~~~~~ +72 ], + ~~~ + + +Error: src/app/shared/context-menu/context-menu.module.ts:70:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +70 imports: [ + ~ +71 MODULE, + ~~~~~~~~~~~ +72 ], + ~~~ + + +Error: src/app/shared/explore/explore.module.ts:47:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +47 SharedModule + ~~~~~~~~~~~~ + + +Error: src/app/shared/form/form.module.ts:95:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +95 SearchModule, + ~~~~~~~~~~~~ + + +Error: src/app/shared/form/form.module.ts:96:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +96 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/shared/mydspace-actions/mydspace-actions.module.ts:45:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +45 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.ts:22:14 - error NG6007: The Component 'ItemListPreviewComponent' is declared by more than one NgModule. + +22 export class ItemListPreviewComponent implements OnInit { + ~~~~~~~~~~~~~~~~~~~~~~~~ + + src/app/shared/shared.module.ts:642:14 + 642 export class SharedModule { + ~~~~~~~~~~~~ + 'ItemListPreviewComponent' is listed in the declarations of the NgModule 'SharedModule'. + src/app/my-dspace-page/my-dspace-search.module.ts:72:14 + 72 export class MyDspaceSearchModule { + ~~~~~~~~~~~~~~~~~~~~ + 'ItemListPreviewComponent' is listed in the declarations of the NgModule 'MyDspaceSearchModule'. + + +Error: src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/themed-item-list-preview.component.ts:18:14 - error NG6007: The Component 'ThemedItemListPreviewComponent' is declared by more than one NgModule. + +18 export class ThemedItemListPreviewComponent extends ThemedComponent { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + src/app/shared/shared.module.ts:642:14 + 642 export class SharedModule { + ~~~~~~~~~~~~ + 'ThemedItemListPreviewComponent' is listed in the declarations of the NgModule 'SharedModule'. + src/app/my-dspace-page/my-dspace-search.module.ts:72:14 + 72 export class MyDspaceSearchModule { + ~~~~~~~~~~~~~~~~~~~~ + 'ThemedItemListPreviewComponent' is listed in the declarations of the NgModule 'MyDspaceSearchModule'. + + +Error: src/app/shared/resource-policies/resource-policies.module.ts:37:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +37 FormModule, + ~~~~~~~~~~ + + +Error: src/app/shared/resource-policies/resource-policies.module.ts:39:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +39 SharedModule + ~~~~~~~~~~~~ + + +Error: src/app/shared/search/search.module.ts:103:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +103 imports: [ + ~ +104 CommonModule, + ~~~~~~~~~~~~~~~~~ +... +110 NouisliderModule, + ~~~~~~~~~~~~~~~~~~~~~ +111 ], + ~~~ + + +Error: src/app/shared/subscriptions/subscriptions.module.ts:27:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +27 SharedModule + ~~~~~~~~~~~~ + + +Error: src/app/shared/upload/upload.module.ts:23:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +23 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/statistics-page/cris-statistics-page/cris-statistics-page.module.ts:18:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 18 imports: [ + ~ + 19 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 24 Ng2GoogleChartsModule + ~~~~~~~~~~~~~~~~~~~~~~~~~ + 25 ], + ~~~ + + +Error: src/app/statistics-page/cris-statistics-page/cris-statistics-page.module.ts:18:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 18 imports: [ + ~ + 19 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 24 Ng2GoogleChartsModule + ~~~~~~~~~~~~~~~~~~~~~~~~~ + 25 ], + ~~~ + + +Error: src/app/statistics-page/cris-statistics-page/statistics-chart/statistics-chart.module.ts:35:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +35 imports: [ + ~ +36 ...IMPORTS + ~~~~~~~~~~~~~~ +37 ], + ~~~ + + +Error: src/app/statistics-page/statistics-page-routing.module.ts:22:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +22 StatisticsPageModule, + ~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/statistics-page/statistics-page.module.ts:42:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 42 imports: [ + ~ + 43 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 47 StatisticsModule.forRoot() + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 48 ], + ~~~ + + +Error: src/app/statistics-page/statistics-page.module.ts:44:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +44 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/statistics-page/statistics-page.module.ts:45:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +45 CrisStatisticsPageModule, + ~~~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/statistics/statistics.module.ts:19:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +19 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/submission/submission.module.ts:122:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +122 imports: [ + ~ +123 CommonModule, + ~~~~~~~~~~~~~~~~~ +... +135 UploadModule, + ~~~~~~~~~~~~~~~~~ +136 ], + ~~~ + + +Error: src/app/submission/submission.module.ts:122:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +122 imports: [ + ~ +123 CommonModule, + ~~~~~~~~~~~~~~~~~ +... +135 UploadModule, + ~~~~~~~~~~~~~~~~~ +136 ], + ~~~ + + +Error: src/app/submission/submission.module.ts:122:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +122 imports: [ + ~ +123 CommonModule, + ~~~~~~~~~~~~~~~~~ +... +135 UploadModule, + ~~~~~~~~~~~~~~~~~ +136 ], + ~~~ + + +Error: src/app/submission/submission.module.ts:125:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +125 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/submission/submission.module.ts:131:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +131 FormModule, + ~~~~~~~~~~ + + +Error: src/app/submission/submission.module.ts:135:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +135 UploadModule, + ~~~~~~~~~~~~ + + +Error: src/app/submission/submission.module.ts:140:5 - error NG6003: This export contains errors, which may affect components that depend on this NgModule. + +140 FormModule, + ~~~~~~~~~~ + + +Error: src/app/submit-page/submit-page.module.ts:12:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +12 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/submit-page/submit-page.module.ts:13:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +13 SubmissionModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/app/submit-page/submit-page.module.ts:14:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +14 FormModule, + ~~~~~~~~~~ + + +Error: src/app/subscriptions-page/subscriptions-page-routing.module.ts:9:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +9 SubscriptionsPageModule, + ~~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/subscriptions-page/subscriptions-page.module.ts:11:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +11 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/subscriptions-page/subscriptions-page.module.ts:12:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +12 SubscriptionsModule, + ~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/suggestions-page/suggestions-page.module.ts:13:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +13 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/suggestions-page/suggestions-page.module.ts:14:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +14 OpenaireModule, + ~~~~~~~~~~~~~~ + + +Error: src/app/system-wide-alert/system-wide-alert.module.ts:14:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +14 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/workflowitems-edit-page/workflowitems-edit-page.module.ts:40:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +40 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/workflowitems-edit-page/workflowitems-edit-page.module.ts:41:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +41 SubmissionModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/app/workflowitems-edit-page/workflowitems-edit-page.module.ts:42:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +42 StatisticsModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/app/workflowitems-edit-page/workflowitems-edit-page.module.ts:43:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +43 ItemPageModule, + ~~~~~~~~~~~~~~ + + +Error: src/app/workflowitems-edit-page/workflowitems-edit-page.module.ts:44:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +44 AccessControlModule, + ~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/workflowitems-edit-page/workflowitems-edit-page.module.ts:45:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +45 FormModule, + ~~~~~~~~~~ + + +Error: src/app/workspaceitems-edit-page/workspaceitems-edit-page.module.ts:13:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +13 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/workspaceitems-edit-page/workspaceitems-edit-page.module.ts:14:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +14 SubmissionModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/modules/app/browser-app.module.ts:49:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 49 imports: [ + ~ + 50 BrowserModule.withServerTransition({ + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +... + 67 AppModule + ~~~~~~~~~~~~~ + 68 ], + ~~~ + + +Error: src/modules/app/browser-app.module.ts:67:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +67 AppModule + ~~~~~~~~~ + + +Error: src/themes/custom/eager-theme.module.ts:106:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +106 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/themes/custom/eager-theme.module.ts:107:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +107 RootModule, + ~~~~~~~~~~ + + +Error: src/themes/custom/eager-theme.module.ts:108:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +108 NavbarModule, + ~~~~~~~~~~~~ + + +Error: src/themes/custom/eager-theme.module.ts:109:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +109 SharedBrowseByModule, + ~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/eager-theme.module.ts:111:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +111 ItemPageModule, + ~~~~~~~~~~~~~~ + + +Error: src/themes/custom/eager-theme.module.ts:112:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +112 ItemSharedModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/eager-theme.module.ts:114:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +114 AttachmentRenderingModule, + ~~~~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/eager-theme.module.ts:115:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +115 ContextMenuModule, + ~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/eager-theme.module.ts:116:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +116 FooterModule, + ~~~~~~~~~~~~ + + +Error: src/themes/custom/eager-theme.module.ts:117:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +117 ExploreModule, + ~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:258:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +258 AdminRegistriesModule, + ~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:259:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +259 AdminSearchModule, + ~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:260:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +260 AdminWorkflowModuleModule, + ~~~~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:261:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +261 AppModule, + ~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:262:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +262 RootModule, + ~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:263:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +263 BitstreamFormatsModule, + ~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:264:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +264 BrowseByModule, + ~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:265:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +265 CollectionFormModule, + ~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:266:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +266 CollectionPageModule, + ~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:268:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +268 CommunityFormModule, + ~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:269:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +269 CommunityListPageModule, + ~~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:270:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +270 CommunityPageModule, + ~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:273:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +273 ItemSharedModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:274:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +274 ItemPageModule, + ~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:275:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +275 EditItemPageModule, + ~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:276:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +276 ItemVersionsModule, + ~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:278:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +278 HomePageModule, + ~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:281:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +281 InfoModule, + ~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:282:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +282 JournalEntitiesModule, + ~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:285:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +285 MyDspaceSearchModule, + ~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:286:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +286 NavbarModule, + ~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:288:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +288 ProfilePageModule, + ~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:289:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +289 RegisterEmailFormModule, + ~~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:290:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +290 ResearchEntitiesModule, + ~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:293:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +293 SearchPageModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:294:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +294 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:295:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +295 SharedBrowseByModule, + ~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:297:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +297 StatisticsModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:298:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +298 StatisticsPageModule, + ~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:302:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +302 SubmissionModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:303:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +303 MyDSpacePageModule, + ~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:304:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +304 MyDspaceSearchModule, + ~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:305:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +305 SearchModule, + ~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:307:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +307 ResourcePoliciesModule, + ~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:308:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +308 ComcolModule, + ~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:309:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +309 DsoSharedModule, + ~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:310:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +310 SystemWideAlertModule, + ~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:312:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +312 FormModule, + ~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:313:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +313 RequestCopyModule, + ~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:314:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +314 ContextMenuModule, + ~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:315:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +315 OpenaireModule, + ~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:316:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +316 CrisItemPageModule, + ~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:317:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +317 CrisStatisticsPageModule, + ~~~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:319:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +319 FooterModule, + ~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:320:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +320 ExploreModule, + ~~~~~~~~~~~~~ + + +Error: src/themes/dspace/eager-theme.module.ts:31:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +31 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/themes/dspace/eager-theme.module.ts:32:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +32 SharedBrowseByModule, + ~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/eager-theme.module.ts:34:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +34 RootModule, + ~~~~~~~~~~ + + +Error: src/themes/dspace/eager-theme.module.ts:35:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +35 NavbarModule, + ~~~~~~~~~~~~ + + +Error: src/themes/dspace/eager-theme.module.ts:36:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +36 ExploreModule + ~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:62:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +62 AdminRegistriesModule, + ~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:63:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +63 AdminSearchModule, + ~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:64:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +64 AdminWorkflowModuleModule, + ~~~~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:65:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +65 AppModule, + ~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:66:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +66 RootModule, + ~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:67:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +67 BitstreamFormatsModule, + ~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:68:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +68 BrowseByModule, + ~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:69:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +69 BrowseByPageModule, + ~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:71:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +71 CollectionFormModule, + ~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:72:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +72 CollectionPageModule, + ~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:74:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +74 CommunityFormModule, + ~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:75:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +75 CommunityListPageModule, + ~~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:76:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +76 CommunityPageModule, + ~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:79:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +79 ItemSharedModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:80:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +80 ItemPageModule, + ~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:81:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +81 EditItemPageModule, + ~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:82:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +82 ItemVersionsModule, + ~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:84:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +84 HomePageModule, + ~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:87:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +87 InfoModule, + ~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:88:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +88 JournalEntitiesModule, + ~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:90:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +90 MyDspaceSearchModule, + ~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:91:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +91 NavbarModule, + ~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:93:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +93 ProfilePageModule, + ~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:94:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +94 RegisterEmailFormModule, + ~~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:95:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +95 ResearchEntitiesModule, + ~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:98:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +98 SearchPageModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:99:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +99 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:100:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +100 SharedBrowseByModule, + ~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:101:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +101 StatisticsModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:102:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +102 StatisticsPageModule, + ~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:106:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +106 SubmissionModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:107:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +107 MyDSpacePageModule, + ~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:108:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +108 MyDspaceSearchModule, + ~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:109:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +109 SearchModule, + ~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:111:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +111 ResourcePoliciesModule, + ~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:112:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +112 ComcolModule, + ~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:113:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +113 ContextMenuModule, + ~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:115:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +115 FooterModule, + ~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:116:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +116 ExploreModule, + ~~~~~~~~~~~~~ + + +Error: src/themes/eager-themes.module.ts:14:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +14 DSpaceEagerThemeModule, + ~~~~~~~~~~~~~~~~~~~~~~ + + + + +** Angular Live Development Server is listening on localhost:4000, open your browser on http://localhost:4000/ ** + + +✖ Failed to compile. +✔ Browser application bundle generation complete. + +Initial Chunk Files | Names | Raw Size +runtime.js | runtime | 14.46 kB | + +185 unchanged chunks + +Build at: 2023-11-08T18:27:48.846Z - Hash: ff30762585b30dfd - Time: 31506ms + +Error: src/app/access-control/access-control.module.ts:33:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +33 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/access-control/access-control.module.ts:36:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +36 FormModule, + ~~~~~~~~~~ + + +Error: src/app/access-control/access-control.module.ts:38:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +38 SearchModule, + ~~~~~~~~~~~~ + + +Error: src/app/access-control/access-control.module.ts:39:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +39 AccessControlFormModule, + ~~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/admin/admin-notifications/admin-notifications.module.ts:14:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +14 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/admin/admin-notifications/admin-notifications.module.ts:17:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +17 OpenaireModule + ~~~~~~~~~~~~~~ + + +Error: src/app/admin/admin-registries/admin-registries.module.ts:16:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +16 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/admin/admin-registries/admin-registries.module.ts:18:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +18 BitstreamFormatsModule, + ~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/admin/admin-registries/admin-registries.module.ts:20:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +20 FormModule + ~~~~~~~~~~ + + +Error: src/app/admin/admin-registries/bitstream-formats/bitstream-formats.module.ts:15:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +15 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/admin/admin-registries/bitstream-formats/bitstream-formats.module.ts:18:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +18 FormModule + ~~~~~~~~~~ + + +Error: src/app/admin/admin-search-page/admin-search.module.ts:27:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 27 imports: [ + ~ + 28 SearchModule, + ~~~~~~~~~~~~~~~~~ +... + 31 ResearchEntitiesModule.withEntryComponents() + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 32 ], + ~~~ + + +Error: src/app/admin/admin-search-page/admin-search.module.ts:27:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 27 imports: [ + ~ + 28 SearchModule, + ~~~~~~~~~~~~~~~~~ +... + 31 ResearchEntitiesModule.withEntryComponents() + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 32 ], + ~~~ + + +Error: src/app/admin/admin-search-page/admin-search.module.ts:27:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 27 imports: [ + ~ + 28 SearchModule, + ~~~~~~~~~~~~~~~~~ +... + 31 ResearchEntitiesModule.withEntryComponents() + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 32 ], + ~~~ + + +Error: src/app/admin/admin-search-page/admin-search.module.ts:28:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +28 SearchModule, + ~~~~~~~~~~~~ + + +Error: src/app/admin/admin-workflow-page/admin-workflow.module.ts:40:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +40 imports: [ + ~ +41 SearchModule, + ~~~~~~~~~~~~~~~~~ +42 SharedModule.withEntryComponents() + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +43 ], + ~~~ + + +Error: src/app/admin/admin-workflow-page/admin-workflow.module.ts:41:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +41 SearchModule, + ~~~~~~~~~~~~ + + +Error: src/app/admin/admin.module.ts:26:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 26 imports: [ + ~ + 27 AdminRoutingModule, + ~~~~~~~~~~~~~~~~~~~~~~~ +... + 34 UploadModule, + ~~~~~~~~~~~~~~~~~ + 35 ], + ~~~ + + +Error: src/app/admin/admin.module.ts:26:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 26 imports: [ + ~ + 27 AdminRoutingModule, + ~~~~~~~~~~~~~~~~~~~~~~~ +... + 34 UploadModule, + ~~~~~~~~~~~~~~~~~ + 35 ], + ~~~ + + +Error: src/app/admin/admin.module.ts:28:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +28 AdminRegistriesModule, + ~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/admin/admin.module.ts:29:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +29 AccessControlModule, + ~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/admin/admin.module.ts:32:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +32 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/admin/admin.module.ts:34:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +34 UploadModule, + ~~~~~~~~~~~~ + + +Error: src/app/app.module.ts:123:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +123 imports: [ + ~ +124 BrowserModule.withServerTransition({ appId: 'dspace-angular' }), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +125 ...IMPORTS + ~~~~~~~~~~~~~~ +126 ], + ~~~ + + +Error: src/app/app.module.ts:123:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +123 imports: [ + ~ +124 BrowserModule.withServerTransition({ appId: 'dspace-angular' }), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +125 ...IMPORTS + ~~~~~~~~~~~~~~ +126 ], + ~~~ + + +Error: src/app/app.module.ts:123:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +123 imports: [ + ~ +124 BrowserModule.withServerTransition({ appId: 'dspace-angular' }), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +125 ...IMPORTS + ~~~~~~~~~~~~~~ +126 ], + ~~~ + + +Error: src/app/app.module.ts:123:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +123 imports: [ + ~ +124 BrowserModule.withServerTransition({ appId: 'dspace-angular' }), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +125 ...IMPORTS + ~~~~~~~~~~~~~~ +126 ], + ~~~ + + +Error: src/app/app.module.ts:123:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +123 imports: [ + ~ +124 BrowserModule.withServerTransition({ appId: 'dspace-angular' }), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +125 ...IMPORTS + ~~~~~~~~~~~~~~ +126 ], + ~~~ + + +Error: src/app/audit-page/audit-page.module.ts:11:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +11 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/bitstream-page/bitstream-page.module.ts:18:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +18 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/bitstream-page/bitstream-page.module.ts:20:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +20 FormModule, + ~~~~~~~~~~ + + +Error: src/app/bitstream-page/bitstream-page.module.ts:21:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +21 ResourcePoliciesModule + ~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/browse-by/browse-by-page.module.ts:10:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 10 imports: [ + ~ + 11 SharedBrowseByModule, + ~~~~~~~~~~~~~~~~~~~~~~~~~ +... + 13 BrowseByModule.withEntryComponents(), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 14 ], + ~~~ + + +Error: src/app/browse-by/browse-by-page.module.ts:11:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +11 SharedBrowseByModule, + ~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/browse-by/browse-by.module.ts:33:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +33 SharedBrowseByModule, + ~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/browse-by/browse-by.module.ts:35:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +35 ComcolModule, + ~~~~~~~~~~~~ + + +Error: src/app/browse-by/browse-by.module.ts:37:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +37 FormModule, + ~~~~~~~~~~ + + +Error: src/app/bulk-import/bulk-import-page.module.ts:14:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +14 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/collection-page/collection-form/collection-form.module.ts:10:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +10 ComcolModule, + ~~~~~~~~~~~~ + + +Error: src/app/collection-page/collection-form/collection-form.module.ts:11:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +11 FormModule, + ~~~~~~~~~~ + + +Error: src/app/collection-page/collection-form/collection-form.module.ts:12:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +12 SharedModule + ~~~~~~~~~~~~ + + +Error: src/app/collection-page/collection-page.module.ts:25:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 25 imports: [ + ~ + 26 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 35 ContextMenuModule.withEntryComponents(), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 36 ], + ~~~ + + +Error: src/app/collection-page/collection-page.module.ts:25:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 25 imports: [ + ~ + 26 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 35 ContextMenuModule.withEntryComponents(), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 36 ], + ~~~ + + +Error: src/app/collection-page/collection-page.module.ts:27:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +27 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/collection-page/collection-page.module.ts:30:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +30 EditItemPageModule, + ~~~~~~~~~~~~~~~~~~ + + +Error: src/app/collection-page/collection-page.module.ts:31:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +31 CollectionFormModule, + ~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/collection-page/collection-page.module.ts:32:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +32 ComcolModule, + ~~~~~~~~~~~~ + + +Error: src/app/collection-page/collection-page.module.ts:33:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +33 DsoSharedModule, + ~~~~~~~~~~~~~~~ + + +Error: src/app/collection-page/edit-collection-page/edit-collection-page.module.ts:27:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +27 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/collection-page/edit-collection-page/edit-collection-page.module.ts:29:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +29 CollectionFormModule, + ~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/collection-page/edit-collection-page/edit-collection-page.module.ts:30:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +30 ResourcePoliciesModule, + ~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/collection-page/edit-collection-page/edit-collection-page.module.ts:31:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +31 FormModule, + ~~~~~~~~~~ + + +Error: src/app/collection-page/edit-collection-page/edit-collection-page.module.ts:32:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +32 ComcolModule, + ~~~~~~~~~~~~ + + +Error: src/app/collection-page/edit-collection-page/edit-collection-page.module.ts:33:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +33 AccessControlFormModule, + ~~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/community-list-page/community-list-page.module.ts:24:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +24 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/community-page/community-form/community-form.module.ts:10:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +10 ComcolModule, + ~~~~~~~~~~~~ + + +Error: src/app/community-page/community-form/community-form.module.ts:11:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +11 FormModule, + ~~~~~~~~~~ + + +Error: src/app/community-page/community-form/community-form.module.ts:12:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +12 SharedModule + ~~~~~~~~~~~~ + + +Error: src/app/community-page/community-page.module.ts:35:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 35 imports: [ + ~ + 36 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 43 ContextMenuModule.withEntryComponents(), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 44 ], + ~~~ + + +Error: src/app/community-page/community-page.module.ts:35:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 35 imports: [ + ~ + 36 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 43 ContextMenuModule.withEntryComponents(), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 44 ], + ~~~ + + +Error: src/app/community-page/community-page.module.ts:37:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +37 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/community-page/community-page.module.ts:40:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +40 CommunityFormModule, + ~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/community-page/community-page.module.ts:41:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +41 ComcolModule, + ~~~~~~~~~~~~ + + +Error: src/app/community-page/edit-community-page/edit-community-page.module.ts:24:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +24 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/community-page/edit-community-page/edit-community-page.module.ts:26:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +26 CommunityFormModule, + ~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/community-page/edit-community-page/edit-community-page.module.ts:27:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +27 ComcolModule, + ~~~~~~~~~~~~ + + +Error: src/app/community-page/edit-community-page/edit-community-page.module.ts:28:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +28 ResourcePoliciesModule, + ~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/community-page/edit-community-page/edit-community-page.module.ts:29:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +29 AccessControlFormModule, + ~~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/cris-item-page/cris-item-page.module.ts:17:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +17 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/cris-item-page/cris-item-page.module.ts:18:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +18 CrisLayoutModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/app/cris-item-page/cris-item-page.module.ts:19:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +19 StatisticsModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/app/cris-item-page/cris-item-page.module.ts:21:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +21 ItemSharedModule + ~~~~~~~~~~~~~~~~ + + +Error: src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metadata/rendering-types/advanced-attachment/bitstream-attachment/attachment-render/attachment-rendering.module.ts:18:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +18 SearchModule, + ~~~~~~~~~~~~ + + +Error: src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metadata/rendering-types/advanced-attachment/bitstream-attachment/attachment-render/attachment-rendering.module.ts:19:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +19 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/cris-layout/cris-layout.module.ts:166:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +166 imports: [ + ~ +167 CommonModule, + ~~~~~~~~~~~~~~~~~ +... +177 FormModule + ~~~~~~~~~~~~~~ +178 ], + ~~~ + + +Error: src/app/cris-layout/cris-layout.module.ts:166:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +166 imports: [ + ~ +167 CommonModule, + ~~~~~~~~~~~~~~~~~ +... +177 FormModule + ~~~~~~~~~~~~~~ +178 ], + ~~~ + + +Error: src/app/cris-layout/cris-layout.module.ts:168:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +168 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/cris-layout/cris-layout.module.ts:170:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +170 MyDSpacePageModule, + ~~~~~~~~~~~~~~~~~~ + + +Error: src/app/cris-layout/cris-layout.module.ts:173:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +173 ComcolModule, + ~~~~~~~~~~~~ + + +Error: src/app/cris-layout/cris-layout.module.ts:176:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +176 AttachmentRenderingModule, + ~~~~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/cris-layout/cris-layout.module.ts:177:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +177 FormModule + ~~~~~~~~~~ + + +Error: src/app/dso-shared/dso-shared.module.ts:13:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +13 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/edit-item-relationships/edit-item-relationships.module.ts:13:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 13 imports: [ + ~ + 14 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 18 SearchModule.withEntryComponents() + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 19 ] + ~~~ + + +Error: src/app/edit-item-relationships/edit-item-relationships.module.ts:13:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 13 imports: [ + ~ + 14 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 18 SearchModule.withEntryComponents() + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 19 ] + ~~~ + + +Error: src/app/edit-item-relationships/edit-item-relationships.module.ts:13:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 13 imports: [ + ~ + 14 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 18 SearchModule.withEntryComponents() + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 19 ] + ~~~ + + +Error: src/app/edit-item/edit-item.module.ts:11:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +11 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/edit-item/edit-item.module.ts:12:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +12 SubmissionModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/app/entity-groups/journal-entities/journal-entities.module.ts:50:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 50 imports: [ + ~ + 51 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 56 ContextMenuModule.withEntryComponents(), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 57 ], + ~~~ + + +Error: src/app/entity-groups/journal-entities/journal-entities.module.ts:52:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +52 ItemSharedModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/app/entity-groups/journal-entities/journal-entities.module.ts:53:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +53 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/entity-groups/research-entities/research-entities.module.ts:73:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 73 imports: [ + ~ + 74 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 81 ContextMenuModule.withEntryComponents(), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 82 ], + ~~~ + + +Error: src/app/entity-groups/research-entities/research-entities.module.ts:75:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +75 ItemSharedModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/app/entity-groups/research-entities/research-entities.module.ts:76:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +76 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/explore-page/explore-page.module.ts:13:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +13 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/explore-page/explore-page.module.ts:14:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +14 ExploreModule + ~~~~~~~~~~~~~ + + +Error: src/app/external-log-in/external-login.module.ts:21:27 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +21 imports: [CommonModule, SharedModule], + ~~~~~~~~~~~~ + + +Error: src/app/external-login-email-confirmation-page/external-login-email-confirmation-page.module.ts:18:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +18 ExternalLoginModule, + ~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/external-login-page/external-login-page.module.ts:22:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +22 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/external-login-page/external-login-page.module.ts:23:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +23 ExternalLoginModule + ~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/external-login-review-account-info-page/external-login-review-account-info-page.module.ts:25:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +25 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/external-login-review-account-info-page/external-login-review-account-info-page.module.ts:27:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +27 ExternalLoginModule + ~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/footer/footer.module.ts:19:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +19 ExploreModule, + ~~~~~~~~~~~~~ + + +Error: src/app/footer/footer.module.ts:20:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +20 SharedModule + ~~~~~~~~~~~~ + + +Error: src/app/forgot-password/forgot-password.module.ts:15:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +15 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/forgot-password/forgot-password.module.ts:17:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +17 RegisterEmailFormModule, + ~~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/forgot-password/forgot-password.module.ts:18:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +18 ProfilePageModule, + ~~~~~~~~~~~~~~~~~ + + +Error: src/app/health-page/health-page.module.ts:22:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +22 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/home-page/home-page.module.ts:24:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 24 imports: [ + ~ + 25 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 32 StatisticsModule.forRoot() + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 33 ], + ~~~ + + +Error: src/app/home-page/home-page.module.ts:24:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 24 imports: [ + ~ + 25 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 32 StatisticsModule.forRoot() + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 33 ], + ~~~ + + +Error: src/app/home-page/home-page.module.ts:24:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 24 imports: [ + ~ + 25 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 32 StatisticsModule.forRoot() + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 33 ], + ~~~ + + +Error: src/app/home-page/home-page.module.ts:24:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 24 imports: [ + ~ + 25 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 32 StatisticsModule.forRoot() + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 33 ], + ~~~ + + +Error: src/app/home-page/home-page.module.ts:29:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +29 ExploreModule, + ~~~~~~~~~~~~~ + + +Error: src/app/home-page/home-page.module.ts:31:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +31 OpenaireModule, + ~~~~~~~~~~~~~~ + + +Error: src/app/home-page/top-level-community-list/top-level-community-list.component.html:4:45 - error NG8004: No pipe found with name 'translate'. + +4 {{'home.top-level-communities.head' | translate}} + ~~~~~~~~~ + + src/app/home-page/top-level-community-list/top-level-community-list.component.ts:23:16 + 23 templateUrl: './top-level-community-list.component.html', + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Error occurs in the template of component TopLevelCommunityListComponent. + + +Error: src/app/home-page/top-level-community-list/top-level-community-list.component.html:6:57 - error NG8004: No pipe found with name 'translate'. + +6

{{'home.top-level-communities.help' | translate}}

+ ~~~~~~~~~ + + src/app/home-page/top-level-community-list/top-level-community-list.component.ts:23:16 + 23 templateUrl: './top-level-community-list.component.html', + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Error occurs in the template of component TopLevelCommunityListComponent. + + +Error: src/app/home-page/top-level-community-list/top-level-community-list.component.html:7:3 - error NG8001: 'ds-viewable-collection' is not a known element: +1. If 'ds-viewable-collection' is an Angular component, then verify that it is part of this module. +2. If 'ds-viewable-collection' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message. + + 7 + ~~~~~~~~~~~~~~~~~~~~~~ + + src/app/home-page/top-level-community-list/top-level-community-list.component.ts:23:16 + 23 templateUrl: './top-level-community-list.component.html', + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Error occurs in the template of component TopLevelCommunityListComponent. + + +Error: src/app/home-page/top-level-community-list/top-level-community-list.component.html:8:5 - error NG8002: Can't bind to 'config' since it isn't a known property of 'ds-viewable-collection'. +1. If 'ds-viewable-collection' is an Angular component and it has 'config' input, then verify that it is part of this module. +2. If 'ds-viewable-collection' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message. +3. To allow any property add 'NO_ERRORS_SCHEMA' to the '@NgModule.schemas' of this component. + +8 [config]="config" + ~~~~~~~~~~~~~~~~~ + + src/app/home-page/top-level-community-list/top-level-community-list.component.ts:23:16 + 23 templateUrl: './top-level-community-list.component.html', + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Error occurs in the template of component TopLevelCommunityListComponent. + + +Error: src/app/home-page/top-level-community-list/top-level-community-list.component.html:9:5 - error NG8002: Can't bind to 'sortConfig' since it isn't a known property of 'ds-viewable-collection'. +1. If 'ds-viewable-collection' is an Angular component and it has 'sortConfig' input, then verify that it is part of this module. +2. If 'ds-viewable-collection' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message. +3. To allow any property add 'NO_ERRORS_SCHEMA' to the '@NgModule.schemas' of this component. + +9 [sortConfig]="sortConfig" + ~~~~~~~~~~~~~~~~~~~~~~~~~ + + src/app/home-page/top-level-community-list/top-level-community-list.component.ts:23:16 + 23 templateUrl: './top-level-community-list.component.html', + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Error occurs in the template of component TopLevelCommunityListComponent. + + +Error: src/app/home-page/top-level-community-list/top-level-community-list.component.html:10:5 - error NG8002: Can't bind to 'objects' since it isn't a known property of 'ds-viewable-collection'. +1. If 'ds-viewable-collection' is an Angular component and it has 'objects' input, then verify that it is part of this module. +2. If 'ds-viewable-collection' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message. +3. To allow any property add 'NO_ERRORS_SCHEMA' to the '@NgModule.schemas' of this component. + +10 [objects]="communitiesRD$ | async" + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + src/app/home-page/top-level-community-list/top-level-community-list.component.ts:23:16 + 23 templateUrl: './top-level-community-list.component.html', + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Error occurs in the template of component TopLevelCommunityListComponent. + + +Error: src/app/home-page/top-level-community-list/top-level-community-list.component.html:10:33 - error NG8004: No pipe found with name 'async'. + +10 [objects]="communitiesRD$ | async" + ~~~~~ + + src/app/home-page/top-level-community-list/top-level-community-list.component.ts:23:16 + 23 templateUrl: './top-level-community-list.component.html', + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Error occurs in the template of component TopLevelCommunityListComponent. + + +Error: src/app/home-page/top-level-community-list/top-level-community-list.component.html:11:5 - error NG8002: Can't bind to 'hideGear' since it isn't a known property of 'ds-viewable-collection'. +1. If 'ds-viewable-collection' is an Angular component and it has 'hideGear' input, then verify that it is part of this module. +2. If 'ds-viewable-collection' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message. +3. To allow any property add 'NO_ERRORS_SCHEMA' to the '@NgModule.schemas' of this component. + +11 [hideGear]="true"> + ~~~~~~~~~~~~~~~~~ + + src/app/home-page/top-level-community-list/top-level-community-list.component.ts:23:16 + 23 templateUrl: './top-level-community-list.component.html', + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Error occurs in the template of component TopLevelCommunityListComponent. + + +Error: src/app/home-page/top-level-community-list/top-level-community-list.component.html:14:1 - error NG8001: 'ds-error' is not a known element: +1. If 'ds-error' is an Angular component, then verify that it is part of this module. +2. If 'ds-error' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message. + +14 + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + src/app/home-page/top-level-community-list/top-level-community-list.component.ts:23:16 + 23 templateUrl: './top-level-community-list.component.html', + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Error occurs in the template of component TopLevelCommunityListComponent. + + +Error: src/app/home-page/top-level-community-list/top-level-community-list.component.html:14:45 - error NG8002: Can't bind to 'message' since it isn't a known property of 'ds-error'. +1. If 'ds-error' is an Angular component and it has 'message' input, then verify that it is part of this module. +2. If 'ds-error' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message. +3. To allow any property add 'NO_ERRORS_SCHEMA' to the '@NgModule.schemas' of this component. + +14 + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + src/app/home-page/top-level-community-list/top-level-community-list.component.ts:23:16 + 23 templateUrl: './top-level-community-list.component.html', + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Error occurs in the template of component TopLevelCommunityListComponent. + + +Error: src/app/home-page/top-level-community-list/top-level-community-list.component.html:14:87 - error NG8004: No pipe found with name 'translate'. + +14 + ~~~~~~~~~ + + src/app/home-page/top-level-community-list/top-level-community-list.component.ts:23:16 + 23 templateUrl: './top-level-community-list.component.html', + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Error occurs in the template of component TopLevelCommunityListComponent. + + +Error: src/app/home-page/top-level-community-list/top-level-community-list.component.html:15:1 - error NG8001: 'ds-themed-loading' is not a known element: +1. If 'ds-themed-loading' is an Angular component, then verify that it is part of this module. +2. If 'ds-themed-loading' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message. + +15 + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + src/app/home-page/top-level-community-list/top-level-community-list.component.ts:23:16 + 23 templateUrl: './top-level-community-list.component.html', + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Error occurs in the template of component TopLevelCommunityListComponent. + + +Error: src/app/home-page/top-level-community-list/top-level-community-list.component.html:15:54 - error NG8002: Can't bind to 'message' since it isn't a known property of 'ds-themed-loading'. +1. If 'ds-themed-loading' is an Angular component and it has 'message' input, then verify that it is part of this module. +2. If 'ds-themed-loading' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message. +3. To allow any property add 'NO_ERRORS_SCHEMA' to the '@NgModule.schemas' of this component. + +15 + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + src/app/home-page/top-level-community-list/top-level-community-list.component.ts:23:16 + 23 templateUrl: './top-level-community-list.component.html', + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Error occurs in the template of component TopLevelCommunityListComponent. + + +Error: src/app/home-page/top-level-community-list/top-level-community-list.component.html:15:99 - error NG8004: No pipe found with name 'translate'. + +15 + ~~~~~~~~~ + + src/app/home-page/top-level-community-list/top-level-community-list.component.ts:23:16 + 23 templateUrl: './top-level-community-list.component.html', + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Error occurs in the template of component TopLevelCommunityListComponent. + + +Error: src/app/import-external-page/import-external-page.module.ts:13:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 13 imports: [ + ~ + 14 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 20 ResearchEntitiesModule.withEntryComponents() + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 21 ], + ~~~ + + +Error: src/app/import-external-page/import-external-page.module.ts:13:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 13 imports: [ + ~ + 14 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 20 ResearchEntitiesModule.withEntryComponents() + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 21 ], + ~~~ + + +Error: src/app/import-external-page/import-external-page.module.ts:13:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 13 imports: [ + ~ + 14 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 20 ResearchEntitiesModule.withEntryComponents() + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 21 ], + ~~~ + + +Error: src/app/import-external-page/import-external-page.module.ts:18:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +18 SubmissionModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/app/info/info.module.ts:35:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +35 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/invitation/invitation.module.ts:12:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +12 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/item-page/alerts/item-alerts.component.ts:13:14 - error NG6007: The Component 'ItemAlertsComponent' is declared by more than one NgModule. + +13 export class ItemAlertsComponent { + ~~~~~~~~~~~~~~~~~~~ + + src/app/item-page/item-shared.module.ts:60:14 + 60 export class ItemSharedModule { } + ~~~~~~~~~~~~~~~~ + 'ItemAlertsComponent' is listed in the declarations of the NgModule 'ItemSharedModule'. + src/app/item-page/item-page.module.ts:137:14 + 137 export class ItemPageModule { + ~~~~~~~~~~~~~~ + 'ItemAlertsComponent' is listed in the declarations of the NgModule 'ItemPageModule'. + + +Error: src/app/item-page/edit-item-page/edit-item-page.module.ts:59:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +59 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/item-page/edit-item-page/edit-item-page.module.ts:62:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +62 SearchPageModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/app/item-page/edit-item-page/edit-item-page.module.ts:64:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +64 ResourcePoliciesModule, + ~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/item-page/edit-item-page/edit-item-page.module.ts:66:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +66 ItemVersionsModule, + ~~~~~~~~~~~~~~~~~~ + + +Error: src/app/item-page/edit-item-page/edit-item-page.module.ts:67:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +67 DsoSharedModule, + ~~~~~~~~~~~~~~~ + + +Error: src/app/item-page/edit-item-page/edit-item-page.module.ts:69:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +69 AccessControlFormModule, + ~~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/item-page/item-page.module.ts:110:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +110 imports: [ + ~ +111 CommonModule, + ~~~~~~~~~~~~~~~~~ +... +127 MiradorViewerModule, + ~~~~~~~~~~~~~~~~~~~~~~~~ +128 ], + ~~~ + + +Error: src/app/item-page/item-page.module.ts:110:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +110 imports: [ + ~ +111 CommonModule, + ~~~~~~~~~~~~~~~~~ +... +127 MiradorViewerModule, + ~~~~~~~~~~~~~~~~~~~~~~~~ +128 ], + ~~~ + + +Error: src/app/item-page/item-page.module.ts:110:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +110 imports: [ + ~ +111 CommonModule, + ~~~~~~~~~~~~~~~~~ +... +127 MiradorViewerModule, + ~~~~~~~~~~~~~~~~~~~~~~~~ +128 ], + ~~~ + + +Error: src/app/item-page/item-page.module.ts:110:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +110 imports: [ + ~ +111 CommonModule, + ~~~~~~~~~~~~~~~~~ +... +127 MiradorViewerModule, + ~~~~~~~~~~~~~~~~~~~~~~~~ +128 ], + ~~~ + + +Error: src/app/item-page/item-page.module.ts:110:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +110 imports: [ + ~ +111 CommonModule, + ~~~~~~~~~~~~~~~~~ +... +127 MiradorViewerModule, + ~~~~~~~~~~~~~~~~~~~~~~~~ +128 ], + ~~~ + + +Error: src/app/item-page/item-page.module.ts:114:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +114 EditItemPageModule, + ~~~~~~~~~~~~~~~~~~ + + +Error: src/app/item-page/item-page.module.ts:115:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +115 ItemVersionsModule, + ~~~~~~~~~~~~~~~~~~ + + +Error: src/app/item-page/item-page.module.ts:116:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +116 ItemSharedModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/app/item-page/item-page.module.ts:123:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +123 UploadModule, + ~~~~~~~~~~~~ + + +Error: src/app/item-page/item-page.module.ts:125:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +125 CrisItemPageModule, + ~~~~~~~~~~~~~~~~~~ + + +Error: src/app/item-page/item-shared.module.ts:45:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +45 SearchModule, + ~~~~~~~~~~~~ + + +Error: src/app/item-page/item-shared.module.ts:46:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +46 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/item-page/versions/item-versions.module.ts:21:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +21 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/login-page/login-page.module.ts:12:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +12 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/logout-page/logout-page.module.ts:12:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +12 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/lookup-by-id/lookup-by-id.module.ts:15:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +15 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/lucky-search/lucky-search.module.ts:12:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 12 imports: [ + ~ + 13 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 16 LuckySearchRoutingModule + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 17 ] + ~~~ + + +Error: src/app/lucky-search/lucky-search.module.ts:12:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 12 imports: [ + ~ + 13 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 16 LuckySearchRoutingModule + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 17 ] + ~~~ + + +Error: src/app/my-dspace-page/my-dspace-page.module.ts:38:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 38 imports: [ + ~ + 39 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 45 UploadModule, + ~~~~~~~~~~~~~~~~~ + 46 ], + ~~~ + + +Error: src/app/my-dspace-page/my-dspace-page.module.ts:40:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +40 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/my-dspace-page/my-dspace-page.module.ts:41:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +41 SearchModule, + ~~~~~~~~~~~~ + + +Error: src/app/my-dspace-page/my-dspace-page.module.ts:44:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +44 OpenaireModule, + ~~~~~~~~~~~~~~ + + +Error: src/app/my-dspace-page/my-dspace-page.module.ts:45:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +45 UploadModule, + ~~~~~~~~~~~~ + + +Error: src/app/my-dspace-page/my-dspace-search.module.ts:53:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 53 imports: [ + ~ + 54 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 59 JournalEntitiesModule.withEntryComponents(), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 60 ], + ~~~ + + +Error: src/app/my-dspace-page/my-dspace-search.module.ts:53:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 53 imports: [ + ~ + 54 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 59 JournalEntitiesModule.withEntryComponents(), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 60 ], + ~~~ + + +Error: src/app/my-dspace-page/my-dspace-search.module.ts:55:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +55 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/my-dspace-page/my-dspace-search.module.ts:57:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +57 MyDSpaceActionsModule, + ~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/navbar/navbar.module.ts:31:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +31 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/openaire/openaire.module.ts:87:14 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +87 imports: [ + ~ +88 ...MODULES, + ~~~~~~~~~~~~~~~~~~~ +89 SearchModule + ~~~~~~~~~~~~~~~~~~~~ +90 ], + ~~~~~ + + +Error: src/app/openaire/openaire.module.ts:89:9 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +89 SearchModule + ~~~~~~~~~~~~ + + +Error: src/app/process-page/process-page-shared.module.ts:22:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +22 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/process-page/process-page.module.ts:10:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +10 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/process-page/process-page.module.ts:11:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +11 ProcessPageSharedModule, + ~~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/profile-page/profile-page.module.ts:22:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +22 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/profile-page/profile-page.module.ts:23:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +23 FormModule, + ~~~~~~~~~~ + + +Error: src/app/profile-page/profile-page.module.ts:25:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +25 OpenaireModule + ~~~~~~~~~~~~~~ + + +Error: src/app/register-email-form/register-email-form.module.ts:9:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +9 SharedModule + ~~~~~~~~~~~~ + + +Error: src/app/register-page/register-page.module.ts:15:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +15 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/register-page/register-page.module.ts:17:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +17 RegisterEmailFormModule, + ~~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/register-page/register-page.module.ts:18:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +18 ProfilePageModule, + ~~~~~~~~~~~~~~~~~ + + +Error: src/app/request-copy/request-copy.module.ts:16:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +16 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/root.module.ts:98:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 98 imports: [ + ~ + 99 ...IMPORTS + ~~~~~~~~~~~~~~ +100 ], + ~~~ + + +Error: src/app/root.module.ts:98:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 98 imports: [ + ~ + 99 ...IMPORTS + ~~~~~~~~~~~~~~ +100 ], + ~~~ + + +Error: src/app/root.module.ts:98:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 98 imports: [ + ~ + 99 ...IMPORTS + ~~~~~~~~~~~~~~ +100 ], + ~~~ + + +Error: src/app/root.module.ts:98:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 98 imports: [ + ~ + 99 ...IMPORTS + ~~~~~~~~~~~~~~ +100 ], + ~~~ + + +Error: src/app/root.module.ts:98:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 98 imports: [ + ~ + 99 ...IMPORTS + ~~~~~~~~~~~~~~ +100 ], + ~~~ + + +Error: src/app/search-page/search-page-routing.module.ts:13:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +13 SearchPageModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/app/search-page/search-page.module.ts:22:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 22 imports: [ + ~ + 23 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 29 ResearchEntitiesModule.withEntryComponents() + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 30 ], + ~~~ + + +Error: src/app/search-page/search-page.module.ts:22:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 22 imports: [ + ~ + 23 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 29 ResearchEntitiesModule.withEntryComponents() + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 30 ], + ~~~ + + +Error: src/app/search-page/search-page.module.ts:22:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 22 imports: [ + ~ + 23 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 29 ResearchEntitiesModule.withEntryComponents() + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 30 ], + ~~~ + + +Error: src/app/search-page/search-page.module.ts:22:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 22 imports: [ + ~ + 23 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 29 ResearchEntitiesModule.withEntryComponents() + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 30 ], + ~~~ + + +Error: src/app/search-page/search-page.module.ts:24:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +24 SearchModule, + ~~~~~~~~~~~~ + + +Error: src/app/shared/access-control-form-container/access-control-form.module.ts:19:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +19 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/shared/browse-by/shared-browse-by.module.ts:18:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +18 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/shared/browse-by/shared-browse-by.module.ts:22:5 - error NG6003: This export contains errors, which may affect components that depend on this NgModule. + +22 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/shared/comcol/comcol.module.ts:41:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +41 FormModule, + ~~~~~~~~~~ + + +Error: src/app/shared/comcol/comcol.module.ts:42:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +42 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/shared/comcol/comcol.module.ts:43:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +43 UploadModule, + ~~~~~~~~~~~~ + + +Error: src/app/shared/comcol/comcol.module.ts:47:5 - error NG6003: This export contains errors, which may affect components that depend on this NgModule. + +47 UploadModule, + ~~~~~~~~~~~~ + + +Error: src/app/shared/context-menu/context-menu.module.ts:70:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +70 imports: [ + ~ +71 MODULE, + ~~~~~~~~~~~ +72 ], + ~~~ + + +Error: src/app/shared/context-menu/context-menu.module.ts:70:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +70 imports: [ + ~ +71 MODULE, + ~~~~~~~~~~~ +72 ], + ~~~ + + +Error: src/app/shared/explore/explore.module.ts:47:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +47 SharedModule + ~~~~~~~~~~~~ + + +Error: src/app/shared/form/form.module.ts:95:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +95 SearchModule, + ~~~~~~~~~~~~ + + +Error: src/app/shared/form/form.module.ts:96:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +96 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/shared/mydspace-actions/mydspace-actions.module.ts:45:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +45 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.ts:22:14 - error NG6007: The Component 'ItemListPreviewComponent' is declared by more than one NgModule. + +22 export class ItemListPreviewComponent implements OnInit { + ~~~~~~~~~~~~~~~~~~~~~~~~ + + src/app/shared/shared.module.ts:642:14 + 642 export class SharedModule { + ~~~~~~~~~~~~ + 'ItemListPreviewComponent' is listed in the declarations of the NgModule 'SharedModule'. + src/app/my-dspace-page/my-dspace-search.module.ts:72:14 + 72 export class MyDspaceSearchModule { + ~~~~~~~~~~~~~~~~~~~~ + 'ItemListPreviewComponent' is listed in the declarations of the NgModule 'MyDspaceSearchModule'. + + +Error: src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/themed-item-list-preview.component.ts:18:14 - error NG6007: The Component 'ThemedItemListPreviewComponent' is declared by more than one NgModule. + +18 export class ThemedItemListPreviewComponent extends ThemedComponent { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + src/app/shared/shared.module.ts:642:14 + 642 export class SharedModule { + ~~~~~~~~~~~~ + 'ThemedItemListPreviewComponent' is listed in the declarations of the NgModule 'SharedModule'. + src/app/my-dspace-page/my-dspace-search.module.ts:72:14 + 72 export class MyDspaceSearchModule { + ~~~~~~~~~~~~~~~~~~~~ + 'ThemedItemListPreviewComponent' is listed in the declarations of the NgModule 'MyDspaceSearchModule'. + + +Error: src/app/shared/resource-policies/resource-policies.module.ts:37:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +37 FormModule, + ~~~~~~~~~~ + + +Error: src/app/shared/resource-policies/resource-policies.module.ts:39:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +39 SharedModule + ~~~~~~~~~~~~ + + +Error: src/app/shared/search/search.module.ts:103:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +103 imports: [ + ~ +104 CommonModule, + ~~~~~~~~~~~~~~~~~ +... +110 NouisliderModule, + ~~~~~~~~~~~~~~~~~~~~~ +111 ], + ~~~ + + +Error: src/app/shared/subscriptions/subscriptions.module.ts:27:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +27 SharedModule + ~~~~~~~~~~~~ + + +Error: src/app/shared/upload/upload.module.ts:23:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +23 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/statistics-page/cris-statistics-page/cris-statistics-page.module.ts:18:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 18 imports: [ + ~ + 19 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 24 Ng2GoogleChartsModule + ~~~~~~~~~~~~~~~~~~~~~~~~~ + 25 ], + ~~~ + + +Error: src/app/statistics-page/cris-statistics-page/cris-statistics-page.module.ts:18:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 18 imports: [ + ~ + 19 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 24 Ng2GoogleChartsModule + ~~~~~~~~~~~~~~~~~~~~~~~~~ + 25 ], + ~~~ + + +Error: src/app/statistics-page/cris-statistics-page/statistics-chart/statistics-chart.module.ts:35:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +35 imports: [ + ~ +36 ...IMPORTS + ~~~~~~~~~~~~~~ +37 ], + ~~~ + + +Error: src/app/statistics-page/statistics-page-routing.module.ts:22:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +22 StatisticsPageModule, + ~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/statistics-page/statistics-page.module.ts:42:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 42 imports: [ + ~ + 43 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 47 StatisticsModule.forRoot() + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 48 ], + ~~~ + + +Error: src/app/statistics-page/statistics-page.module.ts:44:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +44 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/statistics-page/statistics-page.module.ts:45:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +45 CrisStatisticsPageModule, + ~~~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/statistics/statistics.module.ts:19:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +19 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/submission/submission.module.ts:122:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +122 imports: [ + ~ +123 CommonModule, + ~~~~~~~~~~~~~~~~~ +... +135 UploadModule, + ~~~~~~~~~~~~~~~~~ +136 ], + ~~~ + + +Error: src/app/submission/submission.module.ts:122:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +122 imports: [ + ~ +123 CommonModule, + ~~~~~~~~~~~~~~~~~ +... +135 UploadModule, + ~~~~~~~~~~~~~~~~~ +136 ], + ~~~ + + +Error: src/app/submission/submission.module.ts:122:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +122 imports: [ + ~ +123 CommonModule, + ~~~~~~~~~~~~~~~~~ +... +135 UploadModule, + ~~~~~~~~~~~~~~~~~ +136 ], + ~~~ + + +Error: src/app/submission/submission.module.ts:125:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +125 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/submission/submission.module.ts:131:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +131 FormModule, + ~~~~~~~~~~ + + +Error: src/app/submission/submission.module.ts:135:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +135 UploadModule, + ~~~~~~~~~~~~ + + +Error: src/app/submission/submission.module.ts:140:5 - error NG6003: This export contains errors, which may affect components that depend on this NgModule. + +140 FormModule, + ~~~~~~~~~~ + + +Error: src/app/submit-page/submit-page.module.ts:12:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +12 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/submit-page/submit-page.module.ts:13:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +13 SubmissionModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/app/submit-page/submit-page.module.ts:14:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +14 FormModule, + ~~~~~~~~~~ + + +Error: src/app/subscriptions-page/subscriptions-page-routing.module.ts:9:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +9 SubscriptionsPageModule, + ~~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/subscriptions-page/subscriptions-page.module.ts:11:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +11 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/subscriptions-page/subscriptions-page.module.ts:12:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +12 SubscriptionsModule, + ~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/suggestions-page/suggestions-page.module.ts:13:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +13 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/suggestions-page/suggestions-page.module.ts:14:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +14 OpenaireModule, + ~~~~~~~~~~~~~~ + + +Error: src/app/system-wide-alert/system-wide-alert.module.ts:14:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +14 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/workflowitems-edit-page/workflowitems-edit-page.module.ts:40:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +40 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/workflowitems-edit-page/workflowitems-edit-page.module.ts:41:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +41 SubmissionModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/app/workflowitems-edit-page/workflowitems-edit-page.module.ts:42:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +42 StatisticsModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/app/workflowitems-edit-page/workflowitems-edit-page.module.ts:43:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +43 ItemPageModule, + ~~~~~~~~~~~~~~ + + +Error: src/app/workflowitems-edit-page/workflowitems-edit-page.module.ts:44:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +44 AccessControlModule, + ~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/workflowitems-edit-page/workflowitems-edit-page.module.ts:45:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +45 FormModule, + ~~~~~~~~~~ + + +Error: src/app/workspaceitems-edit-page/workspaceitems-edit-page.module.ts:13:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +13 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/workspaceitems-edit-page/workspaceitems-edit-page.module.ts:14:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +14 SubmissionModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/modules/app/browser-app.module.ts:49:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 49 imports: [ + ~ + 50 BrowserModule.withServerTransition({ + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +... + 67 AppModule + ~~~~~~~~~~~~~ + 68 ], + ~~~ + + +Error: src/modules/app/browser-app.module.ts:67:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +67 AppModule + ~~~~~~~~~ + + +Error: src/themes/custom/eager-theme.module.ts:106:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +106 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/themes/custom/eager-theme.module.ts:107:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +107 RootModule, + ~~~~~~~~~~ + + +Error: src/themes/custom/eager-theme.module.ts:108:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +108 NavbarModule, + ~~~~~~~~~~~~ + + +Error: src/themes/custom/eager-theme.module.ts:109:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +109 SharedBrowseByModule, + ~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/eager-theme.module.ts:111:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +111 ItemPageModule, + ~~~~~~~~~~~~~~ + + +Error: src/themes/custom/eager-theme.module.ts:112:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +112 ItemSharedModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/eager-theme.module.ts:114:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +114 AttachmentRenderingModule, + ~~~~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/eager-theme.module.ts:115:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +115 ContextMenuModule, + ~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/eager-theme.module.ts:116:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +116 FooterModule, + ~~~~~~~~~~~~ + + +Error: src/themes/custom/eager-theme.module.ts:117:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +117 ExploreModule, + ~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:258:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +258 AdminRegistriesModule, + ~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:259:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +259 AdminSearchModule, + ~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:260:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +260 AdminWorkflowModuleModule, + ~~~~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:261:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +261 AppModule, + ~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:262:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +262 RootModule, + ~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:263:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +263 BitstreamFormatsModule, + ~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:264:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +264 BrowseByModule, + ~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:265:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +265 CollectionFormModule, + ~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:266:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +266 CollectionPageModule, + ~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:268:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +268 CommunityFormModule, + ~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:269:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +269 CommunityListPageModule, + ~~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:270:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +270 CommunityPageModule, + ~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:273:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +273 ItemSharedModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:274:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +274 ItemPageModule, + ~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:275:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +275 EditItemPageModule, + ~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:276:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +276 ItemVersionsModule, + ~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:278:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +278 HomePageModule, + ~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:281:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +281 InfoModule, + ~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:282:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +282 JournalEntitiesModule, + ~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:285:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +285 MyDspaceSearchModule, + ~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:286:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +286 NavbarModule, + ~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:288:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +288 ProfilePageModule, + ~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:289:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +289 RegisterEmailFormModule, + ~~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:290:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +290 ResearchEntitiesModule, + ~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:293:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +293 SearchPageModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:294:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +294 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:295:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +295 SharedBrowseByModule, + ~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:297:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +297 StatisticsModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:298:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +298 StatisticsPageModule, + ~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:302:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +302 SubmissionModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:303:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +303 MyDSpacePageModule, + ~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:304:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +304 MyDspaceSearchModule, + ~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:305:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +305 SearchModule, + ~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:307:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +307 ResourcePoliciesModule, + ~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:308:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +308 ComcolModule, + ~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:309:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +309 DsoSharedModule, + ~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:310:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +310 SystemWideAlertModule, + ~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:312:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +312 FormModule, + ~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:313:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +313 RequestCopyModule, + ~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:314:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +314 ContextMenuModule, + ~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:315:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +315 OpenaireModule, + ~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:316:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +316 CrisItemPageModule, + ~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:317:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +317 CrisStatisticsPageModule, + ~~~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:319:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +319 FooterModule, + ~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:320:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +320 ExploreModule, + ~~~~~~~~~~~~~ + + +Error: src/themes/dspace/eager-theme.module.ts:31:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +31 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/themes/dspace/eager-theme.module.ts:32:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +32 SharedBrowseByModule, + ~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/eager-theme.module.ts:34:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +34 RootModule, + ~~~~~~~~~~ + + +Error: src/themes/dspace/eager-theme.module.ts:35:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +35 NavbarModule, + ~~~~~~~~~~~~ + + +Error: src/themes/dspace/eager-theme.module.ts:36:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +36 ExploreModule + ~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:62:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +62 AdminRegistriesModule, + ~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:63:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +63 AdminSearchModule, + ~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:64:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +64 AdminWorkflowModuleModule, + ~~~~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:65:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +65 AppModule, + ~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:66:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +66 RootModule, + ~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:67:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +67 BitstreamFormatsModule, + ~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:68:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +68 BrowseByModule, + ~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:69:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +69 BrowseByPageModule, + ~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:71:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +71 CollectionFormModule, + ~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:72:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +72 CollectionPageModule, + ~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:74:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +74 CommunityFormModule, + ~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:75:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +75 CommunityListPageModule, + ~~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:76:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +76 CommunityPageModule, + ~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:79:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +79 ItemSharedModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:80:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +80 ItemPageModule, + ~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:81:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +81 EditItemPageModule, + ~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:82:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +82 ItemVersionsModule, + ~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:84:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +84 HomePageModule, + ~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:87:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +87 InfoModule, + ~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:88:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +88 JournalEntitiesModule, + ~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:90:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +90 MyDspaceSearchModule, + ~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:91:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +91 NavbarModule, + ~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:93:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +93 ProfilePageModule, + ~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:94:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +94 RegisterEmailFormModule, + ~~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:95:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +95 ResearchEntitiesModule, + ~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:98:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +98 SearchPageModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:99:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +99 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:100:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +100 SharedBrowseByModule, + ~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:101:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +101 StatisticsModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:102:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +102 StatisticsPageModule, + ~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:106:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +106 SubmissionModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:107:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +107 MyDSpacePageModule, + ~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:108:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +108 MyDspaceSearchModule, + ~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:109:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +109 SearchModule, + ~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:111:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +111 ResourcePoliciesModule, + ~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:112:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +112 ComcolModule, + ~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:113:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +113 ContextMenuModule, + ~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:115:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +115 FooterModule, + ~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:116:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +116 ExploreModule, + ~~~~~~~~~~~~~ + + +Error: src/themes/eager-themes.module.ts:14:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +14 DSpaceEagerThemeModule, + ~~~~~~~~~~~~~~~~~~~~~~ + + + + +✖ Failed to compile. +✔ Browser application bundle generation complete. + +Initial Chunk Files | Names | Raw Size +main.js | main | 8.51 MB | +runtime.js | runtime | 14.46 kB | + +Lazy Chunk Files | Names | Raw Size +src_app_home-page_home-page_module_ts.js | home-page-home-page-module | 103.16 kB | +src_themes_custom_app_home-page_top-level-community-list_top-level-community-list_component_ts.js | - | 20.38 kB | + +183 unchanged chunks + +Build at: 2023-11-08T18:34:35.241Z - Hash: d3c8858cfe612f69 - Time: 44053ms + +Error: src/app/access-control/access-control.module.ts:33:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +33 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/access-control/access-control.module.ts:36:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +36 FormModule, + ~~~~~~~~~~ + + +Error: src/app/access-control/access-control.module.ts:38:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +38 SearchModule, + ~~~~~~~~~~~~ + + +Error: src/app/access-control/access-control.module.ts:39:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +39 AccessControlFormModule, + ~~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/admin/admin-notifications/admin-notifications.module.ts:14:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +14 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/admin/admin-notifications/admin-notifications.module.ts:17:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +17 OpenaireModule + ~~~~~~~~~~~~~~ + + +Error: src/app/admin/admin-registries/admin-registries.module.ts:16:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +16 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/admin/admin-registries/admin-registries.module.ts:18:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +18 BitstreamFormatsModule, + ~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/admin/admin-registries/admin-registries.module.ts:20:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +20 FormModule + ~~~~~~~~~~ + + +Error: src/app/admin/admin-registries/bitstream-formats/bitstream-formats.module.ts:15:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +15 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/admin/admin-registries/bitstream-formats/bitstream-formats.module.ts:18:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +18 FormModule + ~~~~~~~~~~ + + +Error: src/app/admin/admin-search-page/admin-search.module.ts:27:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 27 imports: [ + ~ + 28 SearchModule, + ~~~~~~~~~~~~~~~~~ +... + 31 ResearchEntitiesModule.withEntryComponents() + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 32 ], + ~~~ + + +Error: src/app/admin/admin-search-page/admin-search.module.ts:27:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 27 imports: [ + ~ + 28 SearchModule, + ~~~~~~~~~~~~~~~~~ +... + 31 ResearchEntitiesModule.withEntryComponents() + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 32 ], + ~~~ + + +Error: src/app/admin/admin-search-page/admin-search.module.ts:27:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 27 imports: [ + ~ + 28 SearchModule, + ~~~~~~~~~~~~~~~~~ +... + 31 ResearchEntitiesModule.withEntryComponents() + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 32 ], + ~~~ + + +Error: src/app/admin/admin-search-page/admin-search.module.ts:28:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +28 SearchModule, + ~~~~~~~~~~~~ + + +Error: src/app/admin/admin-workflow-page/admin-workflow.module.ts:40:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +40 imports: [ + ~ +41 SearchModule, + ~~~~~~~~~~~~~~~~~ +42 SharedModule.withEntryComponents() + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +43 ], + ~~~ + + +Error: src/app/admin/admin-workflow-page/admin-workflow.module.ts:41:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +41 SearchModule, + ~~~~~~~~~~~~ + + +Error: src/app/admin/admin.module.ts:26:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 26 imports: [ + ~ + 27 AdminRoutingModule, + ~~~~~~~~~~~~~~~~~~~~~~~ +... + 34 UploadModule, + ~~~~~~~~~~~~~~~~~ + 35 ], + ~~~ + + +Error: src/app/admin/admin.module.ts:26:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 26 imports: [ + ~ + 27 AdminRoutingModule, + ~~~~~~~~~~~~~~~~~~~~~~~ +... + 34 UploadModule, + ~~~~~~~~~~~~~~~~~ + 35 ], + ~~~ + + +Error: src/app/admin/admin.module.ts:28:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +28 AdminRegistriesModule, + ~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/admin/admin.module.ts:29:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +29 AccessControlModule, + ~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/admin/admin.module.ts:32:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +32 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/admin/admin.module.ts:34:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +34 UploadModule, + ~~~~~~~~~~~~ + + +Error: src/app/app.module.ts:123:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +123 imports: [ + ~ +124 BrowserModule.withServerTransition({ appId: 'dspace-angular' }), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +125 ...IMPORTS + ~~~~~~~~~~~~~~ +126 ], + ~~~ + + +Error: src/app/app.module.ts:123:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +123 imports: [ + ~ +124 BrowserModule.withServerTransition({ appId: 'dspace-angular' }), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +125 ...IMPORTS + ~~~~~~~~~~~~~~ +126 ], + ~~~ + + +Error: src/app/app.module.ts:123:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +123 imports: [ + ~ +124 BrowserModule.withServerTransition({ appId: 'dspace-angular' }), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +125 ...IMPORTS + ~~~~~~~~~~~~~~ +126 ], + ~~~ + + +Error: src/app/app.module.ts:123:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +123 imports: [ + ~ +124 BrowserModule.withServerTransition({ appId: 'dspace-angular' }), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +125 ...IMPORTS + ~~~~~~~~~~~~~~ +126 ], + ~~~ + + +Error: src/app/app.module.ts:123:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +123 imports: [ + ~ +124 BrowserModule.withServerTransition({ appId: 'dspace-angular' }), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +125 ...IMPORTS + ~~~~~~~~~~~~~~ +126 ], + ~~~ + + +Error: src/app/audit-page/audit-page.module.ts:11:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +11 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/bitstream-page/bitstream-page.module.ts:18:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +18 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/bitstream-page/bitstream-page.module.ts:20:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +20 FormModule, + ~~~~~~~~~~ + + +Error: src/app/bitstream-page/bitstream-page.module.ts:21:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +21 ResourcePoliciesModule + ~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/browse-by/browse-by-page.module.ts:10:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 10 imports: [ + ~ + 11 SharedBrowseByModule, + ~~~~~~~~~~~~~~~~~~~~~~~~~ +... + 13 BrowseByModule.withEntryComponents(), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 14 ], + ~~~ + + +Error: src/app/browse-by/browse-by-page.module.ts:11:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +11 SharedBrowseByModule, + ~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/browse-by/browse-by.module.ts:33:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +33 SharedBrowseByModule, + ~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/browse-by/browse-by.module.ts:35:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +35 ComcolModule, + ~~~~~~~~~~~~ + + +Error: src/app/browse-by/browse-by.module.ts:37:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +37 FormModule, + ~~~~~~~~~~ + + +Error: src/app/bulk-import/bulk-import-page.module.ts:14:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +14 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/collection-page/collection-form/collection-form.module.ts:10:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +10 ComcolModule, + ~~~~~~~~~~~~ + + +Error: src/app/collection-page/collection-form/collection-form.module.ts:11:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +11 FormModule, + ~~~~~~~~~~ + + +Error: src/app/collection-page/collection-form/collection-form.module.ts:12:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +12 SharedModule + ~~~~~~~~~~~~ + + +Error: src/app/collection-page/collection-page.module.ts:25:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 25 imports: [ + ~ + 26 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 35 ContextMenuModule.withEntryComponents(), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 36 ], + ~~~ + + +Error: src/app/collection-page/collection-page.module.ts:25:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 25 imports: [ + ~ + 26 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 35 ContextMenuModule.withEntryComponents(), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 36 ], + ~~~ + + +Error: src/app/collection-page/collection-page.module.ts:27:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +27 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/collection-page/collection-page.module.ts:30:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +30 EditItemPageModule, + ~~~~~~~~~~~~~~~~~~ + + +Error: src/app/collection-page/collection-page.module.ts:31:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +31 CollectionFormModule, + ~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/collection-page/collection-page.module.ts:32:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +32 ComcolModule, + ~~~~~~~~~~~~ + + +Error: src/app/collection-page/collection-page.module.ts:33:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +33 DsoSharedModule, + ~~~~~~~~~~~~~~~ + + +Error: src/app/collection-page/edit-collection-page/edit-collection-page.module.ts:27:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +27 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/collection-page/edit-collection-page/edit-collection-page.module.ts:29:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +29 CollectionFormModule, + ~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/collection-page/edit-collection-page/edit-collection-page.module.ts:30:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +30 ResourcePoliciesModule, + ~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/collection-page/edit-collection-page/edit-collection-page.module.ts:31:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +31 FormModule, + ~~~~~~~~~~ + + +Error: src/app/collection-page/edit-collection-page/edit-collection-page.module.ts:32:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +32 ComcolModule, + ~~~~~~~~~~~~ + + +Error: src/app/collection-page/edit-collection-page/edit-collection-page.module.ts:33:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +33 AccessControlFormModule, + ~~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/community-list-page/community-list-page.module.ts:24:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +24 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/community-page/community-form/community-form.module.ts:10:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +10 ComcolModule, + ~~~~~~~~~~~~ + + +Error: src/app/community-page/community-form/community-form.module.ts:11:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +11 FormModule, + ~~~~~~~~~~ + + +Error: src/app/community-page/community-form/community-form.module.ts:12:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +12 SharedModule + ~~~~~~~~~~~~ + + +Error: src/app/community-page/community-page.module.ts:35:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 35 imports: [ + ~ + 36 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 43 ContextMenuModule.withEntryComponents(), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 44 ], + ~~~ + + +Error: src/app/community-page/community-page.module.ts:35:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 35 imports: [ + ~ + 36 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 43 ContextMenuModule.withEntryComponents(), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 44 ], + ~~~ + + +Error: src/app/community-page/community-page.module.ts:37:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +37 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/community-page/community-page.module.ts:40:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +40 CommunityFormModule, + ~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/community-page/community-page.module.ts:41:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +41 ComcolModule, + ~~~~~~~~~~~~ + + +Error: src/app/community-page/edit-community-page/edit-community-page.module.ts:24:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +24 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/community-page/edit-community-page/edit-community-page.module.ts:26:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +26 CommunityFormModule, + ~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/community-page/edit-community-page/edit-community-page.module.ts:27:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +27 ComcolModule, + ~~~~~~~~~~~~ + + +Error: src/app/community-page/edit-community-page/edit-community-page.module.ts:28:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +28 ResourcePoliciesModule, + ~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/community-page/edit-community-page/edit-community-page.module.ts:29:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +29 AccessControlFormModule, + ~~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/cris-item-page/cris-item-page.module.ts:17:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +17 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/cris-item-page/cris-item-page.module.ts:18:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +18 CrisLayoutModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/app/cris-item-page/cris-item-page.module.ts:19:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +19 StatisticsModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/app/cris-item-page/cris-item-page.module.ts:21:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +21 ItemSharedModule + ~~~~~~~~~~~~~~~~ + + +Error: src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metadata/rendering-types/advanced-attachment/bitstream-attachment/attachment-render/attachment-rendering.module.ts:18:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +18 SearchModule, + ~~~~~~~~~~~~ + + +Error: src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metadata/rendering-types/advanced-attachment/bitstream-attachment/attachment-render/attachment-rendering.module.ts:19:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +19 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/cris-layout/cris-layout.module.ts:166:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +166 imports: [ + ~ +167 CommonModule, + ~~~~~~~~~~~~~~~~~ +... +177 FormModule + ~~~~~~~~~~~~~~ +178 ], + ~~~ + + +Error: src/app/cris-layout/cris-layout.module.ts:166:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +166 imports: [ + ~ +167 CommonModule, + ~~~~~~~~~~~~~~~~~ +... +177 FormModule + ~~~~~~~~~~~~~~ +178 ], + ~~~ + + +Error: src/app/cris-layout/cris-layout.module.ts:168:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +168 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/cris-layout/cris-layout.module.ts:170:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +170 MyDSpacePageModule, + ~~~~~~~~~~~~~~~~~~ + + +Error: src/app/cris-layout/cris-layout.module.ts:173:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +173 ComcolModule, + ~~~~~~~~~~~~ + + +Error: src/app/cris-layout/cris-layout.module.ts:176:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +176 AttachmentRenderingModule, + ~~~~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/cris-layout/cris-layout.module.ts:177:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +177 FormModule + ~~~~~~~~~~ + + +Error: src/app/dso-shared/dso-shared.module.ts:13:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +13 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/edit-item-relationships/edit-item-relationships.module.ts:13:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 13 imports: [ + ~ + 14 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 18 SearchModule.withEntryComponents() + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 19 ] + ~~~ + + +Error: src/app/edit-item-relationships/edit-item-relationships.module.ts:13:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 13 imports: [ + ~ + 14 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 18 SearchModule.withEntryComponents() + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 19 ] + ~~~ + + +Error: src/app/edit-item-relationships/edit-item-relationships.module.ts:13:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 13 imports: [ + ~ + 14 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 18 SearchModule.withEntryComponents() + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 19 ] + ~~~ + + +Error: src/app/edit-item/edit-item.module.ts:11:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +11 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/edit-item/edit-item.module.ts:12:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +12 SubmissionModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/app/entity-groups/journal-entities/journal-entities.module.ts:50:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 50 imports: [ + ~ + 51 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 56 ContextMenuModule.withEntryComponents(), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 57 ], + ~~~ + + +Error: src/app/entity-groups/journal-entities/journal-entities.module.ts:52:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +52 ItemSharedModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/app/entity-groups/journal-entities/journal-entities.module.ts:53:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +53 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/entity-groups/research-entities/research-entities.module.ts:73:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 73 imports: [ + ~ + 74 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 81 ContextMenuModule.withEntryComponents(), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 82 ], + ~~~ + + +Error: src/app/entity-groups/research-entities/research-entities.module.ts:75:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +75 ItemSharedModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/app/entity-groups/research-entities/research-entities.module.ts:76:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +76 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/explore-page/explore-page.module.ts:13:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +13 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/explore-page/explore-page.module.ts:14:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +14 ExploreModule + ~~~~~~~~~~~~~ + + +Error: src/app/external-log-in/external-login.module.ts:21:27 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +21 imports: [CommonModule, SharedModule], + ~~~~~~~~~~~~ + + +Error: src/app/external-login-email-confirmation-page/external-login-email-confirmation-page.module.ts:18:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +18 ExternalLoginModule, + ~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/external-login-page/external-login-page.module.ts:22:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +22 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/external-login-page/external-login-page.module.ts:23:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +23 ExternalLoginModule + ~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/external-login-review-account-info-page/external-login-review-account-info-page.module.ts:25:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +25 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/external-login-review-account-info-page/external-login-review-account-info-page.module.ts:27:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +27 ExternalLoginModule + ~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/footer/footer.module.ts:19:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +19 ExploreModule, + ~~~~~~~~~~~~~ + + +Error: src/app/footer/footer.module.ts:20:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +20 SharedModule + ~~~~~~~~~~~~ + + +Error: src/app/forgot-password/forgot-password.module.ts:15:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +15 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/forgot-password/forgot-password.module.ts:17:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +17 RegisterEmailFormModule, + ~~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/forgot-password/forgot-password.module.ts:18:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +18 ProfilePageModule, + ~~~~~~~~~~~~~~~~~ + + +Error: src/app/health-page/health-page.module.ts:22:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +22 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/home-page/home-page.module.ts:30:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 30 imports: [ + ~ + 31 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 38 StatisticsModule.forRoot() + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 39 ], + ~~~ + + +Error: src/app/home-page/home-page.module.ts:30:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 30 imports: [ + ~ + 31 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 38 StatisticsModule.forRoot() + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 39 ], + ~~~ + + +Error: src/app/home-page/home-page.module.ts:30:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 30 imports: [ + ~ + 31 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 38 StatisticsModule.forRoot() + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 39 ], + ~~~ + + +Error: src/app/home-page/home-page.module.ts:30:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 30 imports: [ + ~ + 31 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 38 StatisticsModule.forRoot() + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 39 ], + ~~~ + + +Error: src/app/home-page/home-page.module.ts:35:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +35 ExploreModule, + ~~~~~~~~~~~~~ + + +Error: src/app/home-page/home-page.module.ts:37:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +37 OpenaireModule, + ~~~~~~~~~~~~~~ + + +Error: src/app/import-external-page/import-external-page.module.ts:13:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 13 imports: [ + ~ + 14 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 20 ResearchEntitiesModule.withEntryComponents() + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 21 ], + ~~~ + + +Error: src/app/import-external-page/import-external-page.module.ts:13:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 13 imports: [ + ~ + 14 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 20 ResearchEntitiesModule.withEntryComponents() + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 21 ], + ~~~ + + +Error: src/app/import-external-page/import-external-page.module.ts:13:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 13 imports: [ + ~ + 14 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 20 ResearchEntitiesModule.withEntryComponents() + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 21 ], + ~~~ + + +Error: src/app/import-external-page/import-external-page.module.ts:18:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +18 SubmissionModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/app/info/info.module.ts:35:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +35 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/invitation/invitation.module.ts:12:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +12 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/item-page/alerts/item-alerts.component.ts:13:14 - error NG6007: The Component 'ItemAlertsComponent' is declared by more than one NgModule. + +13 export class ItemAlertsComponent { + ~~~~~~~~~~~~~~~~~~~ + + src/app/item-page/item-shared.module.ts:60:14 + 60 export class ItemSharedModule { } + ~~~~~~~~~~~~~~~~ + 'ItemAlertsComponent' is listed in the declarations of the NgModule 'ItemSharedModule'. + src/app/item-page/item-page.module.ts:137:14 + 137 export class ItemPageModule { + ~~~~~~~~~~~~~~ + 'ItemAlertsComponent' is listed in the declarations of the NgModule 'ItemPageModule'. + + +Error: src/app/item-page/edit-item-page/edit-item-page.module.ts:59:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +59 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/item-page/edit-item-page/edit-item-page.module.ts:62:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +62 SearchPageModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/app/item-page/edit-item-page/edit-item-page.module.ts:64:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +64 ResourcePoliciesModule, + ~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/item-page/edit-item-page/edit-item-page.module.ts:66:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +66 ItemVersionsModule, + ~~~~~~~~~~~~~~~~~~ + + +Error: src/app/item-page/edit-item-page/edit-item-page.module.ts:67:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +67 DsoSharedModule, + ~~~~~~~~~~~~~~~ + + +Error: src/app/item-page/edit-item-page/edit-item-page.module.ts:69:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +69 AccessControlFormModule, + ~~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/item-page/item-page.module.ts:110:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +110 imports: [ + ~ +111 CommonModule, + ~~~~~~~~~~~~~~~~~ +... +127 MiradorViewerModule, + ~~~~~~~~~~~~~~~~~~~~~~~~ +128 ], + ~~~ + + +Error: src/app/item-page/item-page.module.ts:110:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +110 imports: [ + ~ +111 CommonModule, + ~~~~~~~~~~~~~~~~~ +... +127 MiradorViewerModule, + ~~~~~~~~~~~~~~~~~~~~~~~~ +128 ], + ~~~ + + +Error: src/app/item-page/item-page.module.ts:110:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +110 imports: [ + ~ +111 CommonModule, + ~~~~~~~~~~~~~~~~~ +... +127 MiradorViewerModule, + ~~~~~~~~~~~~~~~~~~~~~~~~ +128 ], + ~~~ + + +Error: src/app/item-page/item-page.module.ts:110:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +110 imports: [ + ~ +111 CommonModule, + ~~~~~~~~~~~~~~~~~ +... +127 MiradorViewerModule, + ~~~~~~~~~~~~~~~~~~~~~~~~ +128 ], + ~~~ + + +Error: src/app/item-page/item-page.module.ts:110:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +110 imports: [ + ~ +111 CommonModule, + ~~~~~~~~~~~~~~~~~ +... +127 MiradorViewerModule, + ~~~~~~~~~~~~~~~~~~~~~~~~ +128 ], + ~~~ + + +Error: src/app/item-page/item-page.module.ts:114:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +114 EditItemPageModule, + ~~~~~~~~~~~~~~~~~~ + + +Error: src/app/item-page/item-page.module.ts:115:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +115 ItemVersionsModule, + ~~~~~~~~~~~~~~~~~~ + + +Error: src/app/item-page/item-page.module.ts:116:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +116 ItemSharedModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/app/item-page/item-page.module.ts:123:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +123 UploadModule, + ~~~~~~~~~~~~ + + +Error: src/app/item-page/item-page.module.ts:125:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +125 CrisItemPageModule, + ~~~~~~~~~~~~~~~~~~ + + +Error: src/app/item-page/item-shared.module.ts:45:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +45 SearchModule, + ~~~~~~~~~~~~ + + +Error: src/app/item-page/item-shared.module.ts:46:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +46 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/item-page/versions/item-versions.module.ts:21:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +21 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/login-page/login-page.module.ts:12:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +12 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/logout-page/logout-page.module.ts:12:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +12 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/lookup-by-id/lookup-by-id.module.ts:15:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +15 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/lucky-search/lucky-search.module.ts:12:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 12 imports: [ + ~ + 13 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 16 LuckySearchRoutingModule + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 17 ] + ~~~ + + +Error: src/app/lucky-search/lucky-search.module.ts:12:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 12 imports: [ + ~ + 13 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 16 LuckySearchRoutingModule + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 17 ] + ~~~ + + +Error: src/app/my-dspace-page/my-dspace-page.module.ts:38:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 38 imports: [ + ~ + 39 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 45 UploadModule, + ~~~~~~~~~~~~~~~~~ + 46 ], + ~~~ + + +Error: src/app/my-dspace-page/my-dspace-page.module.ts:40:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +40 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/my-dspace-page/my-dspace-page.module.ts:41:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +41 SearchModule, + ~~~~~~~~~~~~ + + +Error: src/app/my-dspace-page/my-dspace-page.module.ts:44:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +44 OpenaireModule, + ~~~~~~~~~~~~~~ + + +Error: src/app/my-dspace-page/my-dspace-page.module.ts:45:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +45 UploadModule, + ~~~~~~~~~~~~ + + +Error: src/app/my-dspace-page/my-dspace-search.module.ts:53:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 53 imports: [ + ~ + 54 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 59 JournalEntitiesModule.withEntryComponents(), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 60 ], + ~~~ + + +Error: src/app/my-dspace-page/my-dspace-search.module.ts:53:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 53 imports: [ + ~ + 54 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 59 JournalEntitiesModule.withEntryComponents(), + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 60 ], + ~~~ + + +Error: src/app/my-dspace-page/my-dspace-search.module.ts:55:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +55 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/my-dspace-page/my-dspace-search.module.ts:57:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +57 MyDSpaceActionsModule, + ~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/navbar/navbar.module.ts:31:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +31 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/openaire/openaire.module.ts:87:14 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +87 imports: [ + ~ +88 ...MODULES, + ~~~~~~~~~~~~~~~~~~~ +89 SearchModule + ~~~~~~~~~~~~~~~~~~~~ +90 ], + ~~~~~ + + +Error: src/app/openaire/openaire.module.ts:89:9 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +89 SearchModule + ~~~~~~~~~~~~ + + +Error: src/app/process-page/process-page-shared.module.ts:22:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +22 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/process-page/process-page.module.ts:10:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +10 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/process-page/process-page.module.ts:11:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +11 ProcessPageSharedModule, + ~~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/profile-page/profile-page.module.ts:22:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +22 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/profile-page/profile-page.module.ts:23:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +23 FormModule, + ~~~~~~~~~~ + + +Error: src/app/profile-page/profile-page.module.ts:25:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +25 OpenaireModule + ~~~~~~~~~~~~~~ + + +Error: src/app/register-email-form/register-email-form.module.ts:9:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +9 SharedModule + ~~~~~~~~~~~~ + + +Error: src/app/register-page/register-page.module.ts:15:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +15 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/register-page/register-page.module.ts:17:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +17 RegisterEmailFormModule, + ~~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/register-page/register-page.module.ts:18:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +18 ProfilePageModule, + ~~~~~~~~~~~~~~~~~ + + +Error: src/app/request-copy/request-copy.module.ts:16:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +16 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/root.module.ts:98:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 98 imports: [ + ~ + 99 ...IMPORTS + ~~~~~~~~~~~~~~ +100 ], + ~~~ + + +Error: src/app/root.module.ts:98:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 98 imports: [ + ~ + 99 ...IMPORTS + ~~~~~~~~~~~~~~ +100 ], + ~~~ + + +Error: src/app/root.module.ts:98:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 98 imports: [ + ~ + 99 ...IMPORTS + ~~~~~~~~~~~~~~ +100 ], + ~~~ + + +Error: src/app/root.module.ts:98:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 98 imports: [ + ~ + 99 ...IMPORTS + ~~~~~~~~~~~~~~ +100 ], + ~~~ + + +Error: src/app/root.module.ts:98:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 98 imports: [ + ~ + 99 ...IMPORTS + ~~~~~~~~~~~~~~ +100 ], + ~~~ + + +Error: src/app/search-page/search-page-routing.module.ts:13:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +13 SearchPageModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/app/search-page/search-page.module.ts:22:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 22 imports: [ + ~ + 23 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 29 ResearchEntitiesModule.withEntryComponents() + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 30 ], + ~~~ + + +Error: src/app/search-page/search-page.module.ts:22:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 22 imports: [ + ~ + 23 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 29 ResearchEntitiesModule.withEntryComponents() + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 30 ], + ~~~ + + +Error: src/app/search-page/search-page.module.ts:22:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 22 imports: [ + ~ + 23 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 29 ResearchEntitiesModule.withEntryComponents() + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 30 ], + ~~~ + + +Error: src/app/search-page/search-page.module.ts:22:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 22 imports: [ + ~ + 23 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 29 ResearchEntitiesModule.withEntryComponents() + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 30 ], + ~~~ + + +Error: src/app/search-page/search-page.module.ts:24:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +24 SearchModule, + ~~~~~~~~~~~~ + + +Error: src/app/shared/access-control-form-container/access-control-form.module.ts:19:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +19 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/shared/browse-by/shared-browse-by.module.ts:18:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +18 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/shared/browse-by/shared-browse-by.module.ts:22:5 - error NG6003: This export contains errors, which may affect components that depend on this NgModule. + +22 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/shared/comcol/comcol.module.ts:41:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +41 FormModule, + ~~~~~~~~~~ + + +Error: src/app/shared/comcol/comcol.module.ts:42:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +42 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/shared/comcol/comcol.module.ts:43:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +43 UploadModule, + ~~~~~~~~~~~~ + + +Error: src/app/shared/comcol/comcol.module.ts:47:5 - error NG6003: This export contains errors, which may affect components that depend on this NgModule. + +47 UploadModule, + ~~~~~~~~~~~~ + + +Error: src/app/shared/context-menu/context-menu.module.ts:70:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +70 imports: [ + ~ +71 MODULE, + ~~~~~~~~~~~ +72 ], + ~~~ + + +Error: src/app/shared/context-menu/context-menu.module.ts:70:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +70 imports: [ + ~ +71 MODULE, + ~~~~~~~~~~~ +72 ], + ~~~ + + +Error: src/app/shared/explore/explore.module.ts:47:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +47 SharedModule + ~~~~~~~~~~~~ + + +Error: src/app/shared/form/form.module.ts:95:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +95 SearchModule, + ~~~~~~~~~~~~ + + +Error: src/app/shared/form/form.module.ts:96:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +96 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/shared/mydspace-actions/mydspace-actions.module.ts:45:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +45 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.ts:22:14 - error NG6007: The Component 'ItemListPreviewComponent' is declared by more than one NgModule. + +22 export class ItemListPreviewComponent implements OnInit { + ~~~~~~~~~~~~~~~~~~~~~~~~ + + src/app/shared/shared.module.ts:642:14 + 642 export class SharedModule { + ~~~~~~~~~~~~ + 'ItemListPreviewComponent' is listed in the declarations of the NgModule 'SharedModule'. + src/app/my-dspace-page/my-dspace-search.module.ts:72:14 + 72 export class MyDspaceSearchModule { + ~~~~~~~~~~~~~~~~~~~~ + 'ItemListPreviewComponent' is listed in the declarations of the NgModule 'MyDspaceSearchModule'. + + +Error: src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/themed-item-list-preview.component.ts:18:14 - error NG6007: The Component 'ThemedItemListPreviewComponent' is declared by more than one NgModule. + +18 export class ThemedItemListPreviewComponent extends ThemedComponent { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + src/app/shared/shared.module.ts:642:14 + 642 export class SharedModule { + ~~~~~~~~~~~~ + 'ThemedItemListPreviewComponent' is listed in the declarations of the NgModule 'SharedModule'. + src/app/my-dspace-page/my-dspace-search.module.ts:72:14 + 72 export class MyDspaceSearchModule { + ~~~~~~~~~~~~~~~~~~~~ + 'ThemedItemListPreviewComponent' is listed in the declarations of the NgModule 'MyDspaceSearchModule'. + + +Error: src/app/shared/resource-policies/resource-policies.module.ts:37:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +37 FormModule, + ~~~~~~~~~~ + + +Error: src/app/shared/resource-policies/resource-policies.module.ts:39:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +39 SharedModule + ~~~~~~~~~~~~ + + +Error: src/app/shared/search/search.module.ts:103:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +103 imports: [ + ~ +104 CommonModule, + ~~~~~~~~~~~~~~~~~ +... +110 NouisliderModule, + ~~~~~~~~~~~~~~~~~~~~~ +111 ], + ~~~ + + +Error: src/app/shared/subscriptions/subscriptions.module.ts:27:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +27 SharedModule + ~~~~~~~~~~~~ + + +Error: src/app/shared/upload/upload.module.ts:23:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +23 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/statistics-page/cris-statistics-page/cris-statistics-page.module.ts:18:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 18 imports: [ + ~ + 19 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 24 Ng2GoogleChartsModule + ~~~~~~~~~~~~~~~~~~~~~~~~~ + 25 ], + ~~~ + + +Error: src/app/statistics-page/cris-statistics-page/cris-statistics-page.module.ts:18:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 18 imports: [ + ~ + 19 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 24 Ng2GoogleChartsModule + ~~~~~~~~~~~~~~~~~~~~~~~~~ + 25 ], + ~~~ + + +Error: src/app/statistics-page/cris-statistics-page/statistics-chart/statistics-chart.module.ts:35:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +35 imports: [ + ~ +36 ...IMPORTS + ~~~~~~~~~~~~~~ +37 ], + ~~~ + + +Error: src/app/statistics-page/statistics-page-routing.module.ts:22:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +22 StatisticsPageModule, + ~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/statistics-page/statistics-page.module.ts:42:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 42 imports: [ + ~ + 43 CommonModule, + ~~~~~~~~~~~~~~~~~ +... + 47 StatisticsModule.forRoot() + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 48 ], + ~~~ + + +Error: src/app/statistics-page/statistics-page.module.ts:44:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +44 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/statistics-page/statistics-page.module.ts:45:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +45 CrisStatisticsPageModule, + ~~~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/statistics/statistics.module.ts:19:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +19 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/submission/submission.module.ts:122:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +122 imports: [ + ~ +123 CommonModule, + ~~~~~~~~~~~~~~~~~ +... +135 UploadModule, + ~~~~~~~~~~~~~~~~~ +136 ], + ~~~ + + +Error: src/app/submission/submission.module.ts:122:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +122 imports: [ + ~ +123 CommonModule, + ~~~~~~~~~~~~~~~~~ +... +135 UploadModule, + ~~~~~~~~~~~~~~~~~ +136 ], + ~~~ + + +Error: src/app/submission/submission.module.ts:122:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +122 imports: [ + ~ +123 CommonModule, + ~~~~~~~~~~~~~~~~~ +... +135 UploadModule, + ~~~~~~~~~~~~~~~~~ +136 ], + ~~~ + + +Error: src/app/submission/submission.module.ts:125:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +125 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/submission/submission.module.ts:131:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +131 FormModule, + ~~~~~~~~~~ + + +Error: src/app/submission/submission.module.ts:135:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +135 UploadModule, + ~~~~~~~~~~~~ + + +Error: src/app/submission/submission.module.ts:140:5 - error NG6003: This export contains errors, which may affect components that depend on this NgModule. + +140 FormModule, + ~~~~~~~~~~ + + +Error: src/app/submit-page/submit-page.module.ts:12:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +12 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/submit-page/submit-page.module.ts:13:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +13 SubmissionModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/app/submit-page/submit-page.module.ts:14:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +14 FormModule, + ~~~~~~~~~~ + + +Error: src/app/subscriptions-page/subscriptions-page-routing.module.ts:9:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +9 SubscriptionsPageModule, + ~~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/subscriptions-page/subscriptions-page.module.ts:11:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +11 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/subscriptions-page/subscriptions-page.module.ts:12:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +12 SubscriptionsModule, + ~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/suggestions-page/suggestions-page.module.ts:13:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +13 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/suggestions-page/suggestions-page.module.ts:14:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +14 OpenaireModule, + ~~~~~~~~~~~~~~ + + +Error: src/app/system-wide-alert/system-wide-alert.module.ts:14:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +14 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/workflowitems-edit-page/workflowitems-edit-page.module.ts:40:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +40 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/workflowitems-edit-page/workflowitems-edit-page.module.ts:41:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +41 SubmissionModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/app/workflowitems-edit-page/workflowitems-edit-page.module.ts:42:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +42 StatisticsModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/app/workflowitems-edit-page/workflowitems-edit-page.module.ts:43:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +43 ItemPageModule, + ~~~~~~~~~~~~~~ + + +Error: src/app/workflowitems-edit-page/workflowitems-edit-page.module.ts:44:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +44 AccessControlModule, + ~~~~~~~~~~~~~~~~~~~ + + +Error: src/app/workflowitems-edit-page/workflowitems-edit-page.module.ts:45:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +45 FormModule, + ~~~~~~~~~~ + + +Error: src/app/workspaceitems-edit-page/workspaceitems-edit-page.module.ts:13:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +13 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/app/workspaceitems-edit-page/workspaceitems-edit-page.module.ts:14:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +14 SubmissionModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/modules/app/browser-app.module.ts:49:12 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + + 49 imports: [ + ~ + 50 BrowserModule.withServerTransition({ + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +... + 67 AppModule + ~~~~~~~~~~~~~ + 68 ], + ~~~ + + +Error: src/modules/app/browser-app.module.ts:67:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +67 AppModule + ~~~~~~~~~ + + +Error: src/themes/custom/eager-theme.module.ts:106:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +106 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/themes/custom/eager-theme.module.ts:107:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +107 RootModule, + ~~~~~~~~~~ + + +Error: src/themes/custom/eager-theme.module.ts:108:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +108 NavbarModule, + ~~~~~~~~~~~~ + + +Error: src/themes/custom/eager-theme.module.ts:109:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +109 SharedBrowseByModule, + ~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/eager-theme.module.ts:111:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +111 ItemPageModule, + ~~~~~~~~~~~~~~ + + +Error: src/themes/custom/eager-theme.module.ts:112:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +112 ItemSharedModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/eager-theme.module.ts:114:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +114 AttachmentRenderingModule, + ~~~~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/eager-theme.module.ts:115:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +115 ContextMenuModule, + ~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/eager-theme.module.ts:116:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +116 FooterModule, + ~~~~~~~~~~~~ + + +Error: src/themes/custom/eager-theme.module.ts:117:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +117 ExploreModule, + ~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:258:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +258 AdminRegistriesModule, + ~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:259:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +259 AdminSearchModule, + ~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:260:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +260 AdminWorkflowModuleModule, + ~~~~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:261:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +261 AppModule, + ~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:262:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +262 RootModule, + ~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:263:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +263 BitstreamFormatsModule, + ~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:264:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +264 BrowseByModule, + ~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:265:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +265 CollectionFormModule, + ~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:266:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +266 CollectionPageModule, + ~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:268:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +268 CommunityFormModule, + ~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:269:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +269 CommunityListPageModule, + ~~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:270:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +270 CommunityPageModule, + ~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:273:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +273 ItemSharedModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:274:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +274 ItemPageModule, + ~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:275:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +275 EditItemPageModule, + ~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:276:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +276 ItemVersionsModule, + ~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:278:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +278 HomePageModule, + ~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:281:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +281 InfoModule, + ~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:282:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +282 JournalEntitiesModule, + ~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:285:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +285 MyDspaceSearchModule, + ~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:286:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +286 NavbarModule, + ~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:288:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +288 ProfilePageModule, + ~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:289:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +289 RegisterEmailFormModule, + ~~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:290:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +290 ResearchEntitiesModule, + ~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:293:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +293 SearchPageModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:294:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +294 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:295:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +295 SharedBrowseByModule, + ~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:297:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +297 StatisticsModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:298:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +298 StatisticsPageModule, + ~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:302:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +302 SubmissionModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:303:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +303 MyDSpacePageModule, + ~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:304:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +304 MyDspaceSearchModule, + ~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:305:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +305 SearchModule, + ~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:307:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +307 ResourcePoliciesModule, + ~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:308:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +308 ComcolModule, + ~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:309:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +309 DsoSharedModule, + ~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:310:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +310 SystemWideAlertModule, + ~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:312:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +312 FormModule, + ~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:313:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +313 RequestCopyModule, + ~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:314:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +314 ContextMenuModule, + ~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:315:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +315 OpenaireModule, + ~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:316:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +316 CrisItemPageModule, + ~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:317:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +317 CrisStatisticsPageModule, + ~~~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:319:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +319 FooterModule, + ~~~~~~~~~~~~ + + +Error: src/themes/custom/lazy-theme.module.ts:320:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +320 ExploreModule, + ~~~~~~~~~~~~~ + + +Error: src/themes/dspace/eager-theme.module.ts:31:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +31 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/themes/dspace/eager-theme.module.ts:32:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +32 SharedBrowseByModule, + ~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/eager-theme.module.ts:34:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +34 RootModule, + ~~~~~~~~~~ + + +Error: src/themes/dspace/eager-theme.module.ts:35:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +35 NavbarModule, + ~~~~~~~~~~~~ + + +Error: src/themes/dspace/eager-theme.module.ts:36:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +36 ExploreModule + ~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:62:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +62 AdminRegistriesModule, + ~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:63:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +63 AdminSearchModule, + ~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:64:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +64 AdminWorkflowModuleModule, + ~~~~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:65:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +65 AppModule, + ~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:66:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +66 RootModule, + ~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:67:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +67 BitstreamFormatsModule, + ~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:68:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +68 BrowseByModule, + ~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:69:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +69 BrowseByPageModule, + ~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:71:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +71 CollectionFormModule, + ~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:72:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +72 CollectionPageModule, + ~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:74:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +74 CommunityFormModule, + ~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:75:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +75 CommunityListPageModule, + ~~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:76:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +76 CommunityPageModule, + ~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:79:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +79 ItemSharedModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:80:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +80 ItemPageModule, + ~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:81:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +81 EditItemPageModule, + ~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:82:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +82 ItemVersionsModule, + ~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:84:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +84 HomePageModule, + ~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:87:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +87 InfoModule, + ~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:88:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +88 JournalEntitiesModule, + ~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:90:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +90 MyDspaceSearchModule, + ~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:91:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +91 NavbarModule, + ~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:93:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +93 ProfilePageModule, + ~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:94:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +94 RegisterEmailFormModule, + ~~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:95:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +95 ResearchEntitiesModule, + ~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:98:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +98 SearchPageModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:99:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +99 SharedModule, + ~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:100:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +100 SharedBrowseByModule, + ~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:101:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +101 StatisticsModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:102:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +102 StatisticsPageModule, + ~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:106:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +106 SubmissionModule, + ~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:107:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +107 MyDSpacePageModule, + ~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:108:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +108 MyDspaceSearchModule, + ~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:109:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +109 SearchModule, + ~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:111:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +111 ResourcePoliciesModule, + ~~~~~~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:112:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +112 ComcolModule, + ~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:113:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +113 ContextMenuModule, + ~~~~~~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:115:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +115 FooterModule, + ~~~~~~~~~~~~ + + +Error: src/themes/dspace/lazy-theme.module.ts:116:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +116 ExploreModule, + ~~~~~~~~~~~~~ + + +Error: src/themes/eager-themes.module.ts:14:5 - error NG6002: This import contains errors, which may affect components that depend on this NgModule. + +14 DSpaceEagerThemeModule, + ~~~~~~~~~~~~~~~~~~~~~~ + + + + +✖ Failed to compile. diff --git a/package.json b/package.json index a80ad477a35..c97a5605194 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dspace-angular", - "version": "2023.02.00-SNAPSHOT", + "version": "2023.02.02", "scripts": { "ng": "ng", "config:watch": "nodemon", @@ -16,15 +16,15 @@ "build": "ng build --configuration development", "build:stats": "ng build --stats-json", "build:ci": "ng config cli.cache.environment ci && yarn run build:ssr", - "build:prod": "yarn run build:ssr", + "build:prod": "cross-env NODE_ENV=production yarn run build:ssr", "build:ssr": "npm run ng-high-memory -- build --configuration production && ng run dspace-angular:server:production", "ng-high-memory": "node --max_old_space_size=8192 node_modules/@angular/cli/bin/ng", - "test": "npm run ng-high-memory -- test --sourceMap=true --watch=false --configuration test", - "test:watch": "nodemon --exec \"npm run ng-high-memory -- test --sourceMap=true --watch=true --configuration test\"", - "test:headless": "npm run ng-high-memory -- test --sourceMap=true --watch=false --configuration test --browsers=ChromeHeadless --code-coverage", + "test": "npm run ng-high-memory -- test --source-map=true --watch=false --configuration test", + "test:watch": "nodemon --exec \"npm run ng-high-memory -- test --source-map=true --watch=true --configuration test\"", + "test:headless": "npm run ng-high-memory -- test --source-map=true --watch=false --configuration test --browsers=ChromeHeadless --code-coverage", "lint": "npm run ng-high-memory -- lint", "lint-fix": "npm run ng-high-memory -- lint --fix=true", - "e2e": "npm run ng-high-memory -- e2e", + "e2e": "cross-env NODE_ENV=production npm run ng-high-memory -- e2e", "clean:dev:config": "rimraf src/assets/config.json", "clean:coverage": "rimraf coverage", "clean:dist": "rimraf dist", @@ -64,147 +64,150 @@ "resolutions": { "html-to-docx": "~1.4.0", "minimist": "^1.2.5", + "roarr": "7.15.1", "webdriver-manager": "^12.1.8", "ts-node": "10.2.1" }, "dependencies": { - "@angular/animations": "~13.3.12", - "@angular/cdk": "^13.2.6", - "@angular/common": "~13.3.12", - "@angular/compiler": "~13.3.12", - "@angular/core": "~13.3.12", - "@angular/forms": "~13.3.12", - "@angular/localize": "13.3.12", - "@angular/platform-browser": "~13.3.12", - "@angular/platform-browser-dynamic": "~13.3.12", - "@angular/platform-server": "~13.3.12", - "@angular/router": "~13.3.12", - "@babel/runtime": "7.17.2", + "@angular/animations": "^15.2.8", + "@angular/cdk": "^15.2.8", + "@angular/common": "^15.2.8", + "@angular/compiler": "^15.2.8", + "@angular/core": "^15.2.8", + "@angular/forms": "^15.2.8", + "@angular/localize": "15.2.8", + "@angular/platform-browser": "^15.2.8", + "@angular/platform-browser-dynamic": "^15.2.8", + "@angular/platform-server": "^15.2.8", + "@angular/router": "^15.2.8", + "@babel/runtime": "7.21.0", "@kolkov/ngx-gallery": "^2.0.1", "@material-ui/core": "^4.11.0", - "@material-ui/icons": "^4.9.1", + "@material-ui/icons": "^4.11.3", "@ng-bootstrap/ng-bootstrap": "^11.0.0", "@ng-dynamic-forms/core": "^15.0.0", "@ng-dynamic-forms/ui-ng-bootstrap": "^15.0.0", "@ng-util/markdown": "^12.1.2", - "@ngrx/effects": "^13.0.2", - "@ngrx/router-store": "^13.0.2", - "@ngrx/store": "^13.0.2", - "@nguniversal/express-engine": "^13.0.2", - "@ngx-translate/core": "^13.0.0", - "@nicky-lenaers/ngx-scroll-to": "^13.0.0", - "@swimlane/ngx-charts": "^16.0.0", + "@ngrx/effects": "^15.4.0", + "@ngrx/router-store": "^15.4.0", + "@ngrx/store": "^15.4.0", + "@nguniversal/express-engine": "^15.2.1", + "@ngx-translate/core": "^14.0.0", + "@nicky-lenaers/ngx-scroll-to": "^14.0.0", + "@swimlane/ngx-charts": "^20.4.1", "@types/grecaptcha": "^3.0.4", "angular-idle-preload": "3.0.0", - "angulartics2": "^12.0.0", - "axios": "^0.27.2", + "angulartics2": "^12.2.0", + "axios": "^1.6.0", "bootstrap": "^4.6.1", "cerialize": "0.1.18", - "cli-progress": "^3.8.0", + "cli-progress": "^3.12.0", "colors": "^1.4.0", "compression": "^1.7.4", - "cookie-parser": "1.4.5", - "core-js": "^3.7.0", + "cookie-parser": "1.4.6", + "core-js": "^3.30.1", "date-fns": "^2.29.3", "date-fns-tz": "^1.3.7", - "deepmerge": "^4.2.2", - "ejs": "^3.1.8", - "express": "^4.17.1", + "deepmerge": "^4.3.1", + "ejs": "^3.1.9", + "express": "^4.18.2", "express-rate-limit": "^5.1.3", - "fast-json-patch": "^3.0.0-1", + "fast-json-patch": "^3.1.1", "file-saver": "^2.0.5", "filesize": "^6.1.0", "font-awesome": "4.7.0", "html-to-image": "^1.10.8", "http-proxy-middleware": "^1.0.5", - "isbot": "^3.6.5", + "http-terminator": "^3.2.0", + "isbot": "^3.6.10", "js-cookie": "2.2.1", "js-yaml": "^4.1.0", - "json5": "^2.2.2", - "jsonschema": "1.4.0", + "json5": "^2.2.3", + "jsonschema": "1.4.1", "jwt-decode": "^3.1.2", "klaro": "^0.7.18", "lodash": "^4.17.21", "lru-cache": "^7.14.1", "markdown-it": "^13.0.1", - "markdown-it-mathjax3": "^4.3.1", + "markdown-it-mathjax3": "^4.3.2", "mirador": "^3.3.0", "mirador-dl-plugin": "^0.13.0", "mirador-share-plugin": "^0.11.0", "morgan": "^1.10.0", - "ng-mocks": "^13.1.1", + "ng-mocks": "^14.10.0", "ng2-file-upload": "1.4.0", "ng2-google-charts": "^6.1.0", - "ng2-nouislider": "^1.8.3", - "ngx-infinite-scroll": "^10.0.1", - "ngx-pagination": "5.0.0", + "ng2-nouislider": "^2.0.0", + "ngx-infinite-scroll": "^15.0.0", + "ngx-pagination": "6.0.3", "ngx-sortablejs": "^11.1.0", - "ngx-ui-switch": "^13.0.2", - "nouislider": "^14.6.3", - "pem": "1.14.4", - "prop-types": "^15.7.2", - "react-copy-to-clipboard": "^5.0.1", + "ngx-ui-switch": "^14.0.3", + "nouislider": "^15.7.1", + "pem": "1.14.7", + "prop-types": "^15.8.1", + "react-copy-to-clipboard": "^5.1.0", "reflect-metadata": "^0.1.13", - "rxjs": "^7.5.5", - "sanitize-html": "^2.7.2", - "sortablejs": "1.13.0", + "rxjs": "^7.8.0", + "sanitize-html": "^2.10.0", + "sortablejs": "1.15.0", "uuid": "^8.3.2", "webfontloader": "1.6.28", "zone.js": "~0.11.5" }, "devDependencies": { - "@angular-builders/custom-webpack": "~13.1.0", - "@angular-devkit/build-angular": "~13.3.10", - "@angular-eslint/builder": "13.1.0", - "@angular-eslint/eslint-plugin": "13.1.0", - "@angular-eslint/eslint-plugin-template": "13.1.0", - "@angular-eslint/schematics": "13.1.0", - "@angular-eslint/template-parser": "13.1.0", - "@angular/cli": "~13.3.10", - "@angular/compiler-cli": "~13.3.12", - "@angular/language-service": "~13.3.12", + "@angular-builders/custom-webpack": "~15.0.0", + "@angular-devkit/build-angular": "^15.2.6", + "@angular-eslint/builder": "15.2.1", + "@angular-eslint/eslint-plugin": "15.2.1", + "@angular-eslint/eslint-plugin-template": "15.2.1", + "@angular-eslint/schematics": "15.2.1", + "@angular-eslint/template-parser": "15.2.1", + "@angular/cli": "^15.2.6", + "@angular/compiler-cli": "^15.2.8", + "@angular/language-service": "^15.2.8", "@cypress/schematic": "^1.5.0", - "@fortawesome/fontawesome-free": "^6.2.1", - "@ngrx/store-devtools": "^13.0.2", - "@ngtools/webpack": "^13.2.6", - "@nguniversal/builders": "^13.1.1", + "@fortawesome/fontawesome-free": "^6.4.0", + "@ngrx/store-devtools": "^15.4.0", + "@ngtools/webpack": "^15.2.6", + "@nguniversal/builders": "^15.2.1", "@types/deep-freeze": "0.1.2", - "@types/ejs": "^3.1.1", - "@types/express": "^4.17.9", + "@types/ejs": "^3.1.2", + "@types/express": "^4.17.17", "@types/file-saver": "^2.0.1", "@types/jasmine": "~3.6.0", "@types/js-cookie": "2.2.6", - "@types/lodash": "^4.14.165", + "@types/lodash": "^4.14.194", "@types/node": "^14.14.9", - "@types/sanitize-html": "^2.6.2", - "@typescript-eslint/eslint-plugin": "5.11.0", - "@typescript-eslint/parser": "5.11.0", - "axe-core": "^4.4.3", + "@types/sanitize-html": "^2.9.0", + "@typescript-eslint/eslint-plugin": "^5.59.1", + "@typescript-eslint/parser": "^5.59.1", + "axe-core": "^4.7.2", "compression-webpack-plugin": "^9.2.0", "copy-webpack-plugin": "^6.4.1", "cross-env": "^7.0.3", - "cypress": "9.7.0", - "cypress-axe": "^0.14.0", + "cypress": "12.17.4", + "cypress-axe": "^1.4.0", "deep-freeze": "0.0.1", - "eslint": "^8.2.0", - "eslint-plugin-deprecation": "^1.3.2", - "eslint-plugin-import": "^2.25.4", + "eslint": "^8.39.0", + "eslint-plugin-deprecation": "^1.4.1", + "eslint-plugin-import": "^2.27.5", "eslint-plugin-jsdoc": "^39.6.4", + "eslint-plugin-jsonc": "^2.6.0", "eslint-plugin-lodash": "^7.4.0", "eslint-plugin-unused-imports": "^2.0.0", - "express-static-gzip": "^2.1.5", + "express-static-gzip": "^2.1.7", "jasmine-core": "^3.8.0", "jasmine-marbles": "0.9.2", - "karma": "^6.3.14", - "karma-chrome-launcher": "~3.1.0", - "karma-coverage-istanbul-reporter": "~3.0.2", + "karma": "^6.4.2", + "karma-chrome-launcher": "~3.2.0", + "karma-coverage-istanbul-reporter": "~3.0.3", "karma-jasmine": "~4.0.0", "karma-jasmine-html-reporter": "^1.5.0", "karma-mocha-reporter": "2.2.5", - "ngx-export-as": "~1.13.0", - "ngx-mask": "^13.1.7", - "nodemon": "^2.0.20", - "postcss": "^8.1", + "ngx-export-as": "~1.15.1", + "ngx-mask": "13.1.7", + "nodemon": "^2.0.22", + "postcss": "^8.4", "postcss-apply": "0.12.0", "postcss-import": "^14.0.0", "postcss-loader": "^4.0.3", @@ -214,14 +217,14 @@ "react-dom": "^16.14.0", "rimraf": "^3.0.2", "rxjs-spy": "^8.0.2", - "sass": "~1.33.0", + "sass": "~1.62.0", "sass-loader": "^12.6.0", - "sass-resources-loader": "^2.1.1", + "sass-resources-loader": "^2.2.5", "ts-node": "^8.10.2", - "typescript": "~4.5.5", - "webpack": "^5.69.1", - "webpack-bundle-analyzer": "^4.4.0", + "typescript": "~4.8.4", + "webpack": "5.76.1", + "webpack-bundle-analyzer": "^4.8.0", "webpack-cli": "^4.2.0", - "webpack-dev-server": "^4.5.0" + "webpack-dev-server": "^4.13.3" } } diff --git a/scripts/webpack.js b/scripts/webpack.js deleted file mode 100644 index 93f17b4619f..00000000000 --- a/scripts/webpack.js +++ /dev/null @@ -1,13 +0,0 @@ -const path = require('path'); -const child_process = require('child_process'); - -const heapSize = 4096; -const webpackPath = path.join('node_modules', 'webpack', 'bin', 'webpack.js'); - -const params = [ - '--max_old_space_size=' + heapSize, - webpackPath, - ...process.argv.slice(2) -]; - -child_process.spawn('node', params, { stdio:'inherit' }); diff --git a/server.ts b/server.ts index 3e10677a8b1..06ffbbe7661 100644 --- a/server.ts +++ b/server.ts @@ -26,15 +26,15 @@ import * as ejs from 'ejs'; import * as compression from 'compression'; 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 { createHttpTerminator } from 'http-terminator'; -import { existsSync, readFileSync } from 'fs'; +import { readFileSync } from 'fs'; import { join } from 'path'; import { enableProdMode } from '@angular/core'; @@ -54,7 +54,7 @@ import { buildAppConfig } from './src/config/config.server'; import { APP_CONFIG, AppConfig } from './src/config/app-config.interface'; import { extendEnvironmentWithAppConfig } from './src/config/config.util'; import { logStartupMessage } from './startup-message'; -import { TOKENITEM } from 'src/app/core/auth/models/auth-token-info.model'; +import { TOKENITEM } from './src/app/core/auth/models/auth-token-info.model'; /* @@ -180,6 +180,15 @@ export function app() { changeOrigin: true })); + /** + * Proxy the linksets + */ + router.use('/signposting**', createProxyMiddleware({ + target: `${environment.rest.baseUrl}`, + pathRewrite: path => path.replace(environment.ui.nameSpace, '/'), + changeOrigin: true + })); + /** * Checks if the rateLimiter property is present * When it is present, the rateLimiter will be enabled. When it is undefined, the rateLimiter will be disabled. @@ -213,6 +222,11 @@ export function app() { */ server.get('/app/health', healthCheck); + /** + * Checking client status + */ + server.get('/app/client/health', clientHealthCheck); + /** * Default sending all incoming requests to ngApp() function, after first checking for a cached * copy of the page (see cacheCheck()) @@ -312,22 +326,23 @@ 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 + // When enabled, each page defaults to expiring after 1 day (defined in default-app-config.ts) 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 + ttl: environment.cache.serverSide.botCache.timeToLive, + allowStale: environment.cache.serverSide.botCache.allowStale }); } 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) + // When enabled, each page defaults to expiring after 10 seconds (defined in default-app-config.ts) + // to minimize anonymous users seeing out-of-date content anonymousCache = new LRU( { 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 + ttl: environment.cache.serverSide.anonymousCache.timeToLive, + allowStale: environment.cache.serverSide.anonymousCache.allowStale }); } } @@ -366,9 +381,19 @@ function cacheCheck(req, res, next) { } // If cached copy exists, return it to the user. - if (cachedCopy) { + if (cachedCopy && cachedCopy.page) { + if (cachedCopy.headers) { + Object.keys(cachedCopy.headers).forEach((header) => { + if (cachedCopy.headers[header]) { + if (environment.cache.serverSide.debug) { + console.log(`Restore cached ${header} header`); + } + res.setHeader(header, cachedCopy.headers[header]); + } + }); + } res.locals.ssr = true; // mark response as SSR-generated (enables text compression) - res.send(cachedCopy); + res.send(cachedCopy.page); // 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 @@ -443,22 +468,50 @@ function saveToCache(req, page: any) { const key = getCacheKey(req); // Avoid caching "/reload/[random]" paths (these are hard refreshes after logout) if (key.startsWith('/reload')) { return; } + // Avoid caching not successful responses (status code different from 2XX status) + if (hasNotSucceeded(req.res.statusCode)) { return; } + // Retrieve response headers to save, if any + const headers = retrieveHeaders(req.res); // If bot cache is enabled, save it to that cache if it doesn't exist or is expired // (NOTE: has() will return false if page is expired in cache) if (botCacheEnabled() && !botCache.has(key)) { - botCache.set(key, page); + botCache.set(key, { page, headers }); if (environment.cache.serverSide.debug) { console.log(`CACHE SAVE FOR ${key} in bot cache.`); } } // If 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); + anonymousCache.set(key, { page, headers }); if (environment.cache.serverSide.debug) { console.log(`CACHE SAVE FOR ${key} in anonymous cache.`); } } } } +/** + * Check if status code is different from 2XX + * @param statusCode + */ +function hasNotSucceeded(statusCode) { + const rgx = new RegExp(/^20+/); + return !rgx.test(statusCode); +} + +function retrieveHeaders(response) { + const headers = Object.create({}); + if (Array.isArray(environment.cache.serverSide.headers) && environment.cache.serverSide.headers.length > 0) { + environment.cache.serverSide.headers.forEach((header) => { + if (response.hasHeader(header)) { + if (environment.cache.serverSide.debug) { + console.log(`Save ${header} header to cache`); + } + headers[header] = response.getHeader(header); + } + }); + } + + return headers; +} /** * Whether a user is authenticated or not */ @@ -479,23 +532,46 @@ function serverStarted() { * @param keys SSL credentials */ function createHttpsServer(keys) { - createServer({ + const listener = createServer({ key: keys.serviceKey, cert: keys.certificate }, app).listen(environment.ui.port, environment.ui.host, () => { serverStarted(); }); + + // Graceful shutdown when signalled + const terminator = createHttpTerminator({server: listener}); + process.on('SIGINT', () => { + void (async ()=> { + console.debug('Closing HTTPS server on signal'); + await terminator.terminate().catch(e => { console.error(e); }); + console.debug('HTTPS server closed'); + })(); + }); } +/** + * Create an HTTP server with the configured port and host. + */ function run() { const port = environment.ui.port || 4000; const host = environment.ui.host || '/'; // Start up the Node server const server = app(); - server.listen(port, host, () => { + const listener = server.listen(port, host, () => { serverStarted(); }); + + // Graceful shutdown when signalled + const terminator = createHttpTerminator({server: listener}); + process.on('SIGINT', () => { + void (async () => { + console.debug('Closing HTTP server on signal'); + await terminator.terminate().catch(e => { console.error(e); }); + console.debug('HTTP server closed.');return undefined; + })(); + }); } function start() { @@ -545,6 +621,16 @@ function start() { } } +/* + * The callback function to serve client health check requests + */ +function clientHealthCheck(req, res) { + const isServerHealthy = true; + if (isServerHealthy) { + res.status(200).json({ status: 'UP' }); + } +} + /* * The callback function to serve health check requests */ diff --git a/src/app/access-control/access-control-routing-paths.ts b/src/app/access-control/access-control-routing-paths.ts index 259aa311e74..31f39f1c47d 100644 --- a/src/app/access-control/access-control-routing-paths.ts +++ b/src/app/access-control/access-control-routing-paths.ts @@ -1,12 +1,22 @@ import { URLCombiner } from '../core/url-combiner/url-combiner'; import { getAccessControlModuleRoute } from '../app-routing-paths'; -export const GROUP_EDIT_PATH = 'groups'; +export const EPERSON_PATH = 'epeople'; + +export function getEPersonsRoute(): string { + return new URLCombiner(getAccessControlModuleRoute(), EPERSON_PATH).toString(); +} + +export function getEPersonEditRoute(id: string): string { + return new URLCombiner(getEPersonsRoute(), id, 'edit').toString(); +} + +export const GROUP_PATH = 'groups'; export function getGroupsRoute() { - return new URLCombiner(getAccessControlModuleRoute(), GROUP_EDIT_PATH).toString(); + return new URLCombiner(getAccessControlModuleRoute(), GROUP_PATH).toString(); } export function getGroupEditRoute(id: string) { - return new URLCombiner(getAccessControlModuleRoute(), GROUP_EDIT_PATH, id).toString(); + return new URLCombiner(getGroupsRoute(), id, 'edit').toString(); } diff --git a/src/app/access-control/access-control-routing.module.ts b/src/app/access-control/access-control-routing.module.ts index e64b0d170a6..97d049ad836 100644 --- a/src/app/access-control/access-control-routing.module.ts +++ b/src/app/access-control/access-control-routing.module.ts @@ -3,17 +3,24 @@ import { RouterModule } from '@angular/router'; import { EPeopleRegistryComponent } from './epeople-registry/epeople-registry.component'; import { GroupFormComponent } from './group-registry/group-form/group-form.component'; import { GroupsRegistryComponent } from './group-registry/groups-registry.component'; -import { GROUP_EDIT_PATH } from './access-control-routing-paths'; +import { EPERSON_PATH, GROUP_PATH } from './access-control-routing-paths'; import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; import { GroupPageGuard } from './group-registry/group-page.guard'; -import { GroupAdministratorGuard } from '../core/data/feature-authorization/feature-authorization-guard/group-administrator.guard'; -import { SiteAdministratorGuard } from '../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard'; +import { + GroupAdministratorGuard +} from '../core/data/feature-authorization/feature-authorization-guard/group-administrator.guard'; +import { + SiteAdministratorGuard +} from '../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard'; +import { BulkAccessComponent } from './bulk-access/bulk-access.component'; +import { EPersonFormComponent } from './epeople-registry/eperson-form/eperson-form.component'; +import { EPersonResolver } from './epeople-registry/eperson-resolver.service'; @NgModule({ imports: [ RouterModule.forChild([ { - path: 'epeople', + path: EPERSON_PATH, component: EPeopleRegistryComponent, resolve: { breadcrumb: I18nBreadcrumbResolver @@ -22,7 +29,26 @@ import { SiteAdministratorGuard } from '../core/data/feature-authorization/featu canActivate: [SiteAdministratorGuard] }, { - path: GROUP_EDIT_PATH, + path: `${EPERSON_PATH}/create`, + component: EPersonFormComponent, + resolve: { + breadcrumb: I18nBreadcrumbResolver, + }, + data: { title: 'admin.access-control.epeople.add.title', breadcrumbKey: 'admin.access-control.epeople.add' }, + canActivate: [SiteAdministratorGuard], + }, + { + path: `${EPERSON_PATH}/:id/edit`, + component: EPersonFormComponent, + resolve: { + breadcrumb: I18nBreadcrumbResolver, + ePerson: EPersonResolver, + }, + data: { title: 'admin.access-control.epeople.edit.title', breadcrumbKey: 'admin.access-control.epeople.edit' }, + canActivate: [SiteAdministratorGuard], + }, + { + path: GROUP_PATH, component: GroupsRegistryComponent, resolve: { breadcrumb: I18nBreadcrumbResolver @@ -31,7 +57,7 @@ import { SiteAdministratorGuard } from '../core/data/feature-authorization/featu canActivate: [GroupAdministratorGuard] }, { - path: `${GROUP_EDIT_PATH}/newGroup`, + path: `${GROUP_PATH}/create`, component: GroupFormComponent, resolve: { breadcrumb: I18nBreadcrumbResolver @@ -40,14 +66,23 @@ import { SiteAdministratorGuard } from '../core/data/feature-authorization/featu canActivate: [GroupAdministratorGuard] }, { - path: `${GROUP_EDIT_PATH}/:groupId`, + path: `${GROUP_PATH}/:groupId/edit`, component: GroupFormComponent, resolve: { breadcrumb: I18nBreadcrumbResolver }, data: { title: 'admin.access-control.groups.title.singleGroup', breadcrumbKey: 'admin.access-control.groups.singleGroup' }, canActivate: [GroupPageGuard] - } + }, + { + path: 'bulk-access', + component: BulkAccessComponent, + resolve: { + breadcrumb: I18nBreadcrumbResolver + }, + data: { title: 'admin.access-control.bulk-access.title', breadcrumbKey: 'admin.access-control.bulk-access' }, + canActivate: [SiteAdministratorGuard] + }, ]) ] }) diff --git a/src/app/access-control/access-control.module.ts b/src/app/access-control/access-control.module.ts index 47a971a882a..3dc4b6cedc7 100644 --- a/src/app/access-control/access-control.module.ts +++ b/src/app/access-control/access-control.module.ts @@ -12,6 +12,12 @@ import { GroupsRegistryComponent } from './group-registry/groups-registry.compon import { FormModule } from '../shared/form/form.module'; import { DYNAMIC_ERROR_MESSAGES_MATCHER, DynamicErrorMessagesMatcher } from '@ng-dynamic-forms/core'; import { AbstractControl } from '@angular/forms'; +import { BulkAccessComponent } from './bulk-access/bulk-access.component'; +import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap'; +import { BulkAccessBrowseComponent } from './bulk-access/browse/bulk-access-browse.component'; +import { BulkAccessSettingsComponent } from './bulk-access/settings/bulk-access-settings.component'; +import { SearchModule } from '../shared/search/search.module'; +import { AccessControlFormModule } from '../shared/access-control-form-container/access-control-form.module'; /** * Condition for displaying error messages on email form field @@ -28,6 +34,9 @@ export const ValidateEmailErrorStateMatcher: DynamicErrorMessagesMatcher = RouterModule, AccessControlRoutingModule, FormModule, + NgbAccordionModule, + SearchModule, + AccessControlFormModule, ], exports: [ MembersListComponent, @@ -39,6 +48,9 @@ export const ValidateEmailErrorStateMatcher: DynamicErrorMessagesMatcher = GroupFormComponent, SubgroupsListComponent, MembersListComponent, + BulkAccessComponent, + BulkAccessBrowseComponent, + BulkAccessSettingsComponent, ], providers: [ { diff --git a/src/app/access-control/bulk-access/browse/bulk-access-browse.component.html b/src/app/access-control/bulk-access/browse/bulk-access-browse.component.html new file mode 100644 index 00000000000..c716aedb8b3 --- /dev/null +++ b/src/app/access-control/bulk-access/browse/bulk-access-browse.component.html @@ -0,0 +1,67 @@ + + + +
+ +
+
+ + +
+
+
+
+ + +
+
+
+
diff --git a/src/app/shared/dso-page/dso-page-subscription-button/dso-page-subscription-button.component.scss b/src/app/access-control/bulk-access/browse/bulk-access-browse.component.scss similarity index 100% rename from src/app/shared/dso-page/dso-page-subscription-button/dso-page-subscription-button.component.scss rename to src/app/access-control/bulk-access/browse/bulk-access-browse.component.scss diff --git a/src/app/access-control/bulk-access/browse/bulk-access-browse.component.spec.ts b/src/app/access-control/bulk-access/browse/bulk-access-browse.component.spec.ts new file mode 100644 index 00000000000..87b2a8d5684 --- /dev/null +++ b/src/app/access-control/bulk-access/browse/bulk-access-browse.component.spec.ts @@ -0,0 +1,82 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; + +import { of } from 'rxjs'; +import { NgbAccordionModule, NgbNavModule } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateModule } from '@ngx-translate/core'; + +import { BulkAccessBrowseComponent } from './bulk-access-browse.component'; +import { SelectableListService } from '../../../shared/object-list/selectable-list/selectable-list.service'; +import { SelectableObject } from '../../../shared/object-list/selectable-list/selectable-list.service.spec'; +import { PageInfo } from '../../../core/shared/page-info.model'; +import { buildPaginatedList } from '../../../core/data/paginated-list.model'; +import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils'; + +describe('BulkAccessBrowseComponent', () => { + let component: BulkAccessBrowseComponent; + let fixture: ComponentFixture; + + const listID1 = 'id1'; + const value1 = 'Selected object'; + const value2 = 'Another selected object'; + + const selected1 = new SelectableObject(value1); + const selected2 = new SelectableObject(value2); + + const testSelection = { id: listID1, selection: [selected1, selected2] } ; + + const selectableListService = jasmine.createSpyObj('SelectableListService', ['getSelectableList', 'deselectAll']); + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [ + NgbAccordionModule, + NgbNavModule, + TranslateModule.forRoot() + ], + declarations: [BulkAccessBrowseComponent], + providers: [ { provide: SelectableListService, useValue: selectableListService }, ], + schemas: [ + NO_ERRORS_SCHEMA + ] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(BulkAccessBrowseComponent); + component = fixture.componentInstance; + (component as any).selectableListService.getSelectableList.and.returnValue(of(testSelection)); + fixture.detectChanges(); + }); + + afterEach(() => { + fixture.destroy(); + component = null; + }); + + it('should create the component', () => { + expect(component).toBeTruthy(); + }); + + it('should have an initial active nav id of "search"', () => { + expect(component.activateId).toEqual('search'); + }); + + it('should have an initial pagination options object with default values', () => { + expect(component.paginationOptions$.getValue().id).toEqual('bas'); + expect(component.paginationOptions$.getValue().pageSize).toEqual(5); + expect(component.paginationOptions$.getValue().currentPage).toEqual(1); + }); + + it('should have an initial remote data with a paginated list as value', () => { + const list = buildPaginatedList(new PageInfo({ + 'elementsPerPage': 5, + 'totalElements': 2, + 'totalPages': 1, + 'currentPage': 1 + }), [selected1, selected2]) ; + const rd = createSuccessfulRemoteDataObject(list); + + expect(component.objectsSelected$.value).toEqual(rd); + }); + +}); diff --git a/src/app/access-control/bulk-access/browse/bulk-access-browse.component.ts b/src/app/access-control/bulk-access/browse/bulk-access-browse.component.ts new file mode 100644 index 00000000000..e806e729c8e --- /dev/null +++ b/src/app/access-control/bulk-access/browse/bulk-access-browse.component.ts @@ -0,0 +1,119 @@ +import { Component, Input, OnDestroy, OnInit } from '@angular/core'; + +import { BehaviorSubject, Subscription } from 'rxjs'; +import { distinctUntilChanged, map } from 'rxjs/operators'; + +import { SEARCH_CONFIG_SERVICE } from '../../../my-dspace-page/my-dspace-page.component'; +import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service'; +import { SelectableListService } from '../../../shared/object-list/selectable-list/selectable-list.service'; +import { SelectableListState } from '../../../shared/object-list/selectable-list/selectable-list.reducer'; +import { RemoteData } from '../../../core/data/remote-data'; +import { buildPaginatedList, PaginatedList } from '../../../core/data/paginated-list.model'; +import { ListableObject } from '../../../shared/object-collection/shared/listable-object.model'; +import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils'; +import { PageInfo } from '../../../core/shared/page-info.model'; +import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; +import { hasValue } from '../../../shared/empty.util'; + +@Component({ + selector: 'ds-bulk-access-browse', + templateUrl: 'bulk-access-browse.component.html', + styleUrls: ['./bulk-access-browse.component.scss'], + providers: [ + { + provide: SEARCH_CONFIG_SERVICE, + useClass: SearchConfigurationService + } + ] +}) +export class BulkAccessBrowseComponent implements OnInit, OnDestroy { + + /** + * The selection list id + */ + @Input() listId!: string; + + /** + * The active nav id + */ + activateId = 'search'; + + /** + * The list of the objects already selected + */ + objectsSelected$: BehaviorSubject>> = new BehaviorSubject>>(null); + + /** + * The pagination options object used for the list of selected elements + */ + paginationOptions$: BehaviorSubject = new BehaviorSubject(Object.assign(new PaginationComponentOptions(), { + id: 'bas', + pageSize: 5, + currentPage: 1 + })); + + /** + * Array to track all subscriptions and unsubscribe them onDestroy + */ + private subs: Subscription[] = []; + + constructor(private selectableListService: SelectableListService) {} + + /** + * Subscribe to selectable list updates + */ + ngOnInit(): void { + + this.subs.push( + this.selectableListService.getSelectableList(this.listId).pipe( + distinctUntilChanged(), + map((list: SelectableListState) => this.generatePaginatedListBySelectedElements(list)) + ).subscribe(this.objectsSelected$) + ); + } + + pageNext() { + this.paginationOptions$.next(Object.assign(new PaginationComponentOptions(), this.paginationOptions$.value, { + currentPage: this.paginationOptions$.value.currentPage + 1 + })); + } + + pagePrev() { + this.paginationOptions$.next(Object.assign(new PaginationComponentOptions(), this.paginationOptions$.value, { + currentPage: this.paginationOptions$.value.currentPage - 1 + })); + } + + private calculatePageCount(pageSize, totalCount = 0) { + // we suppose that if we have 0 items we want 1 empty page + return totalCount < pageSize ? 1 : Math.ceil(totalCount / pageSize); + } + + /** + * Generate The RemoteData object containing the list of the selected elements + * @param list + * @private + */ + private generatePaginatedListBySelectedElements(list: SelectableListState): RemoteData> { + const pageInfo = new PageInfo({ + elementsPerPage: this.paginationOptions$.value.pageSize, + totalElements: list?.selection.length, + totalPages: this.calculatePageCount(this.paginationOptions$.value.pageSize, list?.selection.length), + currentPage: this.paginationOptions$.value.currentPage + }); + if (pageInfo.currentPage > pageInfo.totalPages) { + pageInfo.currentPage = pageInfo.totalPages; + this.paginationOptions$.next(Object.assign(new PaginationComponentOptions(), this.paginationOptions$.value, { + currentPage: pageInfo.currentPage + })); + } + return createSuccessfulRemoteDataObject(buildPaginatedList(pageInfo, list?.selection || [])); + } + + ngOnDestroy(): void { + this.subs + .filter((sub) => hasValue(sub)) + .forEach((sub) => sub.unsubscribe()); + this.selectableListService.deselectAll(this.listId); + } +} diff --git a/src/app/access-control/bulk-access/bulk-access.component.html b/src/app/access-control/bulk-access/bulk-access.component.html new file mode 100644 index 00000000000..382caf85f46 --- /dev/null +++ b/src/app/access-control/bulk-access/bulk-access.component.html @@ -0,0 +1,19 @@ +
+ +
+ + +
+ +
+ + +
+
+ + + diff --git a/src/app/shared/object-collection/shared/mydspace-item-status/my-dspace-item-status.component.scss b/src/app/access-control/bulk-access/bulk-access.component.scss similarity index 100% rename from src/app/shared/object-collection/shared/mydspace-item-status/my-dspace-item-status.component.scss rename to src/app/access-control/bulk-access/bulk-access.component.scss diff --git a/src/app/access-control/bulk-access/bulk-access.component.spec.ts b/src/app/access-control/bulk-access/bulk-access.component.spec.ts new file mode 100644 index 00000000000..e9b253147dc --- /dev/null +++ b/src/app/access-control/bulk-access/bulk-access.component.spec.ts @@ -0,0 +1,158 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; + +import { TranslateModule } from '@ngx-translate/core'; +import { of } from 'rxjs'; + +import { BulkAccessComponent } from './bulk-access.component'; +import { BulkAccessControlService } from '../../shared/access-control-form-container/bulk-access-control.service'; +import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service'; +import { SelectableListState } from '../../shared/object-list/selectable-list/selectable-list.reducer'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { Process } from '../../process-page/processes/process.model'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; + +describe('BulkAccessComponent', () => { + let component: BulkAccessComponent; + let fixture: ComponentFixture; + let bulkAccessControlService: any; + let selectableListService: any; + + const selectableListServiceMock = jasmine.createSpyObj('SelectableListService', ['getSelectableList', 'deselectAll']); + const bulkAccessControlServiceMock = jasmine.createSpyObj('bulkAccessControlService', ['createPayloadFile', 'executeScript']); + + const mockFormState = { + 'bitstream': [], + 'item': [ + { + 'name': 'embargo', + 'startDate': { + 'year': 2026, + 'month': 5, + 'day': 31 + }, + 'endDate': null + } + ], + 'state': { + 'item': { + 'toggleStatus': true, + 'accessMode': 'replace' + }, + 'bitstream': { + 'toggleStatus': false, + 'accessMode': '', + 'changesLimit': '', + 'selectedBitstreams': [] + } + } + }; + + const mockFile = { + 'uuids': [ + '1234', '5678' + ], + 'file': { } + }; + + const mockSettings: any = jasmine.createSpyObj('AccessControlFormContainerComponent', { + getValue: jasmine.createSpy('getValue'), + reset: jasmine.createSpy('reset') + }); + const selection: any[] = [{ indexableObject: { uuid: '1234' } }, { indexableObject: { uuid: '5678' } }]; + const selectableListState: SelectableListState = { id: 'test', selection }; + const expectedIdList = ['1234', '5678']; + + const selectableListStateEmpty: SelectableListState = { id: 'test', selection: [] }; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + TranslateModule.forRoot() + ], + declarations: [ BulkAccessComponent ], + providers: [ + { provide: BulkAccessControlService, useValue: bulkAccessControlServiceMock }, + { provide: NotificationsService, useValue: NotificationsServiceStub }, + { provide: SelectableListService, useValue: selectableListServiceMock } + ], + schemas: [NO_ERRORS_SCHEMA] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(BulkAccessComponent); + component = fixture.componentInstance; + bulkAccessControlService = TestBed.inject(BulkAccessControlService); + selectableListService = TestBed.inject(SelectableListService); + + }); + + afterEach(() => { + fixture.destroy(); + }); + + describe('when there are no elements selected', () => { + + beforeEach(() => { + + (component as any).selectableListService.getSelectableList.and.returnValue(of(selectableListStateEmpty)); + fixture.detectChanges(); + component.settings = mockSettings; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should generate the id list by selected elements', () => { + expect(component.objectsSelected$.value).toEqual([]); + }); + + it('should disable the execute button when there are no objects selected', () => { + expect(component.canExport()).toBe(false); + }); + + }); + + describe('when there are elements selected', () => { + + beforeEach(() => { + + (component as any).selectableListService.getSelectableList.and.returnValue(of(selectableListState)); + fixture.detectChanges(); + component.settings = mockSettings; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should generate the id list by selected elements', () => { + expect(component.objectsSelected$.value).toEqual(expectedIdList); + }); + + it('should enable the execute button when there are objects selected', () => { + component.objectsSelected$.next(['1234']); + expect(component.canExport()).toBe(true); + }); + + it('should call the settings reset method when reset is called', () => { + component.reset(); + expect(component.settings.reset).toHaveBeenCalled(); + }); + + it('should call the bulkAccessControlService executeScript method when submit is called', () => { + (component.settings as any).getValue.and.returnValue(mockFormState); + bulkAccessControlService.createPayloadFile.and.returnValue(mockFile); + bulkAccessControlService.executeScript.and.returnValue(createSuccessfulRemoteDataObject$(new Process())); + component.objectsSelected$.next(['1234']); + component.submit(); + expect(bulkAccessControlService.executeScript).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/app/access-control/bulk-access/bulk-access.component.ts b/src/app/access-control/bulk-access/bulk-access.component.ts new file mode 100644 index 00000000000..04724614cb6 --- /dev/null +++ b/src/app/access-control/bulk-access/bulk-access.component.ts @@ -0,0 +1,94 @@ +import { Component, OnInit, ViewChild } from '@angular/core'; + +import { BehaviorSubject, Subscription } from 'rxjs'; +import { distinctUntilChanged, map } from 'rxjs/operators'; + +import { BulkAccessSettingsComponent } from './settings/bulk-access-settings.component'; +import { BulkAccessControlService } from '../../shared/access-control-form-container/bulk-access-control.service'; +import { SelectableListState } from '../../shared/object-list/selectable-list/selectable-list.reducer'; +import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service'; + +@Component({ + selector: 'ds-bulk-access', + templateUrl: './bulk-access.component.html', + styleUrls: ['./bulk-access.component.scss'] +}) +export class BulkAccessComponent implements OnInit { + + /** + * The selection list id + */ + listId = 'bulk-access-list'; + + /** + * The list of the objects already selected + */ + objectsSelected$: BehaviorSubject = new BehaviorSubject([]); + + /** + * Array to track all subscriptions and unsubscribe them onDestroy + */ + private subs: Subscription[] = []; + + /** + * The SectionsDirective reference + */ + @ViewChild('dsBulkSettings') settings: BulkAccessSettingsComponent; + + constructor( + private bulkAccessControlService: BulkAccessControlService, + private selectableListService: SelectableListService + ) { + } + + ngOnInit(): void { + this.subs.push( + this.selectableListService.getSelectableList(this.listId).pipe( + distinctUntilChanged(), + map((list: SelectableListState) => this.generateIdListBySelectedElements(list)) + ).subscribe(this.objectsSelected$) + ); + } + + canExport(): boolean { + return this.objectsSelected$.value?.length > 0; + } + + /** + * Reset the form to its initial state + * This will also reset the state of the child components (bitstream and item access) + */ + reset(): void { + this.settings.reset(); + } + + /** + * Submit the form + * This will create a payload file and execute the script + */ + submit(): void { + const settings = this.settings.getValue(); + const bitstreamAccess = settings.bitstream; + const itemAccess = settings.item; + + const { file } = this.bulkAccessControlService.createPayloadFile({ + bitstreamAccess, + itemAccess, + state: settings.state + }); + + this.bulkAccessControlService.executeScript( + this.objectsSelected$.value || [], + file + ).subscribe(); + } + + /** + * Generate The RemoteData object containing the list of the selected elements + * @param list + * @private + */ + private generateIdListBySelectedElements(list: SelectableListState): string[] { + return list?.selection?.map((entry: any) => entry.indexableObject.uuid); + } +} diff --git a/src/app/access-control/bulk-access/settings/bulk-access-settings.component.html b/src/app/access-control/bulk-access/settings/bulk-access-settings.component.html new file mode 100644 index 00000000000..01f36ef03f4 --- /dev/null +++ b/src/app/access-control/bulk-access/settings/bulk-access-settings.component.html @@ -0,0 +1,21 @@ + + + +
+ +
+
+ + +
+
+
+
+ + + +
+
diff --git a/src/themes/custom/app/shared/object-list/collection-list-element/collection-list-element.component.html b/src/app/access-control/bulk-access/settings/bulk-access-settings.component.scss similarity index 100% rename from src/themes/custom/app/shared/object-list/collection-list-element/collection-list-element.component.html rename to src/app/access-control/bulk-access/settings/bulk-access-settings.component.scss diff --git a/src/app/access-control/bulk-access/settings/bulk-access-settings.component.spec.ts b/src/app/access-control/bulk-access/settings/bulk-access-settings.component.spec.ts new file mode 100644 index 00000000000..14e0fdefb21 --- /dev/null +++ b/src/app/access-control/bulk-access/settings/bulk-access-settings.component.spec.ts @@ -0,0 +1,81 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateModule } from '@ngx-translate/core'; +import { BulkAccessSettingsComponent } from './bulk-access-settings.component'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; + +describe('BulkAccessSettingsComponent', () => { + let component: BulkAccessSettingsComponent; + let fixture: ComponentFixture; + const mockFormState = { + 'bitstream': [], + 'item': [ + { + 'name': 'embargo', + 'startDate': { + 'year': 2026, + 'month': 5, + 'day': 31 + }, + 'endDate': null + } + ], + 'state': { + 'item': { + 'toggleStatus': true, + 'accessMode': 'replace' + }, + 'bitstream': { + 'toggleStatus': false, + 'accessMode': '', + 'changesLimit': '', + 'selectedBitstreams': [] + } + } + }; + + const mockControl: any = jasmine.createSpyObj('AccessControlFormContainerComponent', { + getFormValue: jasmine.createSpy('getFormValue'), + reset: jasmine.createSpy('reset') + }); + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [NgbAccordionModule, TranslateModule.forRoot()], + declarations: [BulkAccessSettingsComponent], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(BulkAccessSettingsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + component.controlForm = mockControl; + }); + + it('should create the component', () => { + expect(component).toBeTruthy(); + }); + + it('should have a method to get the form value', () => { + expect(component.getValue).toBeDefined(); + }); + + it('should have a method to reset the form', () => { + expect(component.reset).toBeDefined(); + }); + + it('should return the correct form value', () => { + const expectedValue = mockFormState; + (component.controlForm as any).getFormValue.and.returnValue(mockFormState); + const actualValue = component.getValue(); + // @ts-ignore + expect(actualValue).toEqual(expectedValue); + }); + + it('should call reset on the control form', () => { + component.reset(); + expect(component.controlForm.reset).toHaveBeenCalled(); + }); +}); diff --git a/src/app/access-control/bulk-access/settings/bulk-access-settings.component.ts b/src/app/access-control/bulk-access/settings/bulk-access-settings.component.ts new file mode 100644 index 00000000000..eecc0162451 --- /dev/null +++ b/src/app/access-control/bulk-access/settings/bulk-access-settings.component.ts @@ -0,0 +1,34 @@ +import { Component, ViewChild } from '@angular/core'; +import { + AccessControlFormContainerComponent +} from '../../../shared/access-control-form-container/access-control-form-container.component'; + +@Component({ + selector: 'ds-bulk-access-settings', + templateUrl: 'bulk-access-settings.component.html', + styleUrls: ['./bulk-access-settings.component.scss'], + exportAs: 'dsBulkSettings' +}) +export class BulkAccessSettingsComponent { + + /** + * The SectionsDirective reference + */ + @ViewChild('dsAccessControlForm') controlForm: AccessControlFormContainerComponent; + + /** + * Will be used from a parent component to read the value of the form + */ + getValue() { + return this.controlForm.getFormValue(); + } + + /** + * Reset the form to its initial state + * This will also reset the state of the child components (bitstream and item access) + */ + reset() { + this.controlForm.reset(); + } + +} diff --git a/src/app/access-control/epeople-registry/epeople-registry.component.html b/src/app/access-control/epeople-registry/epeople-registry.component.html index 2d87f21d260..4979f858193 100644 --- a/src/app/access-control/epeople-registry/epeople-registry.component.html +++ b/src/app/access-control/epeople-registry/epeople-registry.component.html @@ -4,96 +4,91 @@
-
+
- + + +
+ +
+
+
+ + -
-
- -
- +
+
+ +
+ - - + + -
- - - - - - - - - - - - - - - - - -
{{labelPrefix + 'table.id' | translate}}{{labelPrefix + 'table.name' | translate}}{{labelPrefix + 'table.email' | translate}}{{labelPrefix + 'table.edit' | translate}}
{{epersonDto.eperson.id}}{{epersonDto.eperson.name}}{{epersonDto.eperson.email}} -
- - -
-
-
+
+ + + + + + + + + + + + + + + + + +
{{labelPrefix + 'table.id' | translate}}{{labelPrefix + 'table.name' | translate}}{{labelPrefix + 'table.email' | translate}}{{labelPrefix + 'table.edit' | translate}}
{{epersonDto.eperson.id}}{{ dsoNameService.getName(epersonDto.eperson) }}{{epersonDto.eperson.email}} +
+ + +
+
+
-
+
- + diff --git a/src/app/access-control/epeople-registry/epeople-registry.component.spec.ts b/src/app/access-control/epeople-registry/epeople-registry.component.spec.ts index c3a6fc8bbce..538179c0e9c 100644 --- a/src/app/access-control/epeople-registry/epeople-registry.component.spec.ts +++ b/src/app/access-control/epeople-registry/epeople-registry.component.spec.ts @@ -1,7 +1,7 @@ import { Router } from '@angular/router'; import { Observable, of as observableOf } from 'rxjs'; 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 { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { BrowserModule, By } from '@angular/platform-browser'; @@ -44,6 +44,7 @@ describe('EPeopleRegistryComponent', () => { let paginationService; beforeEach(waitForAsync(() => { + jasmine.getEnv().allowRespy(true); mockEPeople = [EPersonMock, EPersonMock2]; ePersonDataServiceStub = { activeEPerson: null, @@ -100,7 +101,7 @@ describe('EPeopleRegistryComponent', () => { deleteEPerson(ePerson: EPerson): Observable { this.allEpeople = this.allEpeople.filter((ePerson2: EPerson) => { return (ePerson2.uuid !== ePerson.uuid); - }); + }); return observableOf(true); }, editEPerson(ePerson: EPerson) { @@ -205,36 +206,6 @@ describe('EPeopleRegistryComponent', () => { }); }); - describe('toggleEditEPerson', () => { - describe('when you click on first edit eperson button', () => { - beforeEach(fakeAsync(() => { - const editButtons = fixture.debugElement.queryAll(By.css('.access-control-editEPersonButton')); - editButtons[0].triggerEventHandler('click', { - preventDefault: () => {/**/ - } - }); - tick(); - fixture.detectChanges(); - })); - - it('editEPerson form is toggled', () => { - const ePeopleIds = fixture.debugElement.queryAll(By.css('#epeople tr td:first-child')); - ePersonDataServiceStub.getActiveEPerson().subscribe((activeEPerson: EPerson) => { - if (ePeopleIds[0] && activeEPerson === ePeopleIds[0].nativeElement.textContent) { - expect(component.isEPersonFormShown).toEqual(false); - } else { - expect(component.isEPersonFormShown).toEqual(true); - } - - }); - }); - - it('EPerson search section is hidden', () => { - expect(fixture.debugElement.query(By.css('#search'))).toBeNull(); - }); - }); - }); - describe('deleteEPerson', () => { describe('when you click on first delete eperson button', () => { let ePeopleIdsFoundBeforeDelete; @@ -263,17 +234,16 @@ describe('EPeopleRegistryComponent', () => { describe('delete EPerson button when the isAuthorized returns false', () => { let ePeopleDeleteButton; beforeEach(() => { - authorizationService = jasmine.createSpyObj('authorizationService', { - isAuthorized: observableOf(false) - }); + spyOn(authorizationService, 'isAuthorized').and.returnValue(observableOf(false)); + component.initialisePage(); + fixture.detectChanges(); }); it('should be disabled', () => { 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); }); - }); }); }); diff --git a/src/app/access-control/epeople-registry/epeople-registry.component.ts b/src/app/access-control/epeople-registry/epeople-registry.component.ts index f600fef0b23..a132ea75c47 100644 --- a/src/app/access-control/epeople-registry/epeople-registry.component.ts +++ b/src/app/access-control/epeople-registry/epeople-registry.component.ts @@ -1,5 +1,5 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; -import { FormBuilder } from '@angular/forms'; +import { UntypedFormBuilder } from '@angular/forms'; import { Router } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; import { BehaviorSubject, combineLatest, Observable, Subscription } from 'rxjs'; @@ -21,6 +21,8 @@ import { RequestService } from '../../core/data/request.service'; import { PageInfo } from '../../core/shared/page-info.model'; import { NoContent } from '../../core/shared/NoContent.model'; import { PaginationService } from '../../core/pagination/pagination.service'; +import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; +import { getEPersonEditRoute, getEPersonsRoute } from '../access-control-routing-paths'; import { UUIDService } from '../../core/shared/uuid.service'; @Component({ @@ -64,11 +66,6 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy { currentPage: 1 }); - /** - * Whether or not to show the EPerson form - */ - isEPersonFormShown: boolean; - // The search form searchForm; @@ -90,12 +87,13 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy { private translateService: TranslateService, private notificationsService: NotificationsService, private authorizationService: AuthorizationDataService, - private formBuilder: FormBuilder, + private formBuilder: UntypedFormBuilder, private router: Router, private modalService: NgbModal, private paginationService: PaginationService, - private uuidService: UUIDService, - public requestService: RequestService) { + public requestService: RequestService, + public dsoNameService: DSONameService, + private uuidService: UUIDService) { this.currentSearchQuery = ''; this.currentSearchScope = 'metadata'; this.searchForm = this.formBuilder.group(({ @@ -113,17 +111,11 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy { */ initialisePage() { this.searching$.next(true); - this.isEPersonFormShown = false; this.search({scope: this.currentSearchScope, query: this.currentSearchQuery}); - this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => { - if (eperson != null && eperson.id) { - this.isEPersonFormShown = true; - } - })); this.subs.push(this.ePeople$.pipe( switchMap((epeople: PaginatedList) => { if (epeople.pageInfo.totalElements > 0) { - return combineLatest(...epeople.page.map((eperson) => { + return combineLatest(epeople.page.map((eperson: EPerson) => { return this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(eperson) ? eperson.self : undefined).pipe( map((authorized) => { const epersonDtoModel: EpersonDtoModel = new EpersonDtoModel(); @@ -159,14 +151,14 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy { const query: string = data.query; const scope: string = data.scope; if (query != null && this.currentSearchQuery !== query) { - this.router.navigate([this.epersonService.getEPeoplePageRouterLink()], { + void this.router.navigate([getEPersonsRoute()], { queryParamsHandling: 'merge' }); this.currentSearchQuery = query; this.paginationService.resetPage(this.config.id); } if (scope != null && this.currentSearchScope !== scope) { - this.router.navigate([this.epersonService.getEPeoplePageRouterLink()], { + void this.router.navigate([getEPersonsRoute()], { queryParamsHandling: 'merge' }); this.currentSearchScope = scope; @@ -204,23 +196,6 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy { return this.epersonService.getActiveEPerson(); } - /** - * Start editing the selected EPerson - * @param ePerson - */ - toggleEditEPerson(ePerson: EPerson) { - this.getActiveEPerson().pipe(take(1)).subscribe((activeEPerson: EPerson) => { - if (ePerson === activeEPerson) { - this.epersonService.cancelEditEPerson(); - this.isEPersonFormShown = false; - } else { - this.epersonService.editEPerson(ePerson); - this.isEPersonFormShown = true; - } - }); - this.scrollToTop(); - } - /** * Deletes EPerson, show notification on success/failure & updates EPeople list */ @@ -239,9 +214,9 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy { if (hasValue(ePerson.id)) { this.epersonService.deleteEPerson(ePerson).pipe(getFirstCompletedRemoteData()).subscribe((restResponse: RemoteData) => { if (restResponse.hasSucceeded) { - this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', {name: ePerson.name})); + this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', {name: this.dsoNameService.getName(ePerson)})); } else { - this.notificationsService.error('Error occured when trying to delete EPerson with id: ' + ePerson.id + ' with code: ' + restResponse.statusCode + ' and message: ' + restResponse.errorMessage); + this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.deleted.success', { id: ePerson.id, statusCode: restResponse.statusCode, errorMessage: restResponse.errorMessage })); } }); } @@ -263,16 +238,6 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy { this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe()); } - scrollToTop() { - (function smoothscroll() { - const currentScroll = document.documentElement.scrollTop || document.body.scrollTop; - if (currentScroll > 0) { - window.requestAnimationFrame(smoothscroll); - window.scrollTo(0, currentScroll - (currentScroll / 8)); - } - })(); - } - /** * Reset all input-fields to be empty and search all search */ @@ -283,17 +248,7 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy { this.search({query: ''}); } - /** - * This method will set everything to stale, which will cause the lists on this page to update. - */ - reset() { - this.epersonService.getBrowseEndpoint().pipe( - take(1) - ).subscribe((href: string) => { - this.requestService.setStaleByHrefSubstring(href).pipe(take(1)).subscribe(() => { - this.epersonService.cancelEditEPerson(); - this.isEPersonFormShown = false; - }); - }); + getEditEPeoplePage(id: string): string { + return getEPersonEditRoute(id); } } diff --git a/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.html b/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.html index e9cc48aee3d..6a7b8b931ff 100644 --- a/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.html +++ b/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.html @@ -1,84 +1,97 @@ -
+
+
+
- -

{{messagePrefix + '.create' | translate}}

-
+
- -

{{messagePrefix + '.edit' | translate}}

-
+ +

{{messagePrefix + '.create' | translate}}

+
- -
- -
-
- -
-
- - -
- -
+ +

{{messagePrefix + '.edit' | translate}}

+
- + +
+ +
+
+ +
+
+ + +
+ +
-
-
{{messagePrefix + '.groupsEPersonIsMemberOf' | translate}}
+ - +
+
{{messagePrefix + '.groupsEPersonIsMemberOf' | translate}}
- + -
- - - - - - - - - - - - - - - -
{{messagePrefix + '.table.id' | translate}}{{messagePrefix + '.table.name' | translate}}{{messagePrefix + '.table.collectionOrCommunity' | translate}}
{{group.id}}{{group.name}}{{(group.object | async)?.payload?.name}}
-
+ + +
+ + + + + + + + + + + + + + + +
{{messagePrefix + '.table.id' | translate}}{{messagePrefix + '.table.name' | translate}}{{messagePrefix + '.table.collectionOrCommunity' | translate}}
{{group.id}} + + {{ dsoNameService.getName(group) }} + + {{ dsoNameService.getName(undefined) }}
+
-
+
-
diff --git a/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.spec.ts b/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.spec.ts index 64217b1410f..6b0bf4dee6b 100644 --- a/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.spec.ts +++ b/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.spec.ts @@ -2,7 +2,7 @@ import { Observable, of as observableOf } from 'rxjs'; import { CommonModule } from '@angular/common'; import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; +import { UntypedFormControl, UntypedFormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; import { BrowserModule, By } from '@angular/platform-browser'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; @@ -31,6 +31,10 @@ import { PaginationServiceStub } from '../../../shared/testing/pagination-servic import { FindListOptions } from '../../../core/data/find-list-options.model'; import { ValidateEmailNotTaken } from './validators/email-taken.validator'; import { EpersonRegistrationService } from '../../../core/data/eperson-registration.service'; +import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; +import { ActivatedRoute, Router } from '@angular/router'; +import { RouterStub } from '../../../shared/testing/router.stub'; +import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub'; import { UUIDService } from '../../../core/shared/uuid.service'; import { getMockUUIDService } from '../../../shared/mocks/uuid.service.mock'; @@ -45,6 +49,8 @@ describe('EPersonFormComponent', () => { let authorizationService: AuthorizationDataService; let groupsDataService: GroupDataService; let epersonRegistrationService: EpersonRegistrationService; + let route: ActivatedRouteStub; + let router: RouterStub; let paginationService; @@ -108,6 +114,9 @@ describe('EPersonFormComponent', () => { }, getEPersonByEmail(email): Observable> { return createSuccessfulRemoteDataObject$(null); + }, + findById(_id: string, _useCachedVersionIfAvailable = true, _reRequestOnStale = true, ..._linksToFollow: FollowLinkConfig[]): Observable> { + return createSuccessfulRemoteDataObject$(null); } }; builderService = Object.assign(getMockFormBuilderService(),{ @@ -118,9 +127,9 @@ describe('EPersonFormComponent', () => { const controlModel = model; const controlState = { value: controlModel.value, disabled: controlModel.disabled }; const controlOptions = this.createAbstractControlOptions(controlModel.validators, controlModel.asyncValidators, controlModel.updateOn); - controls[model.id] = new FormControl(controlState, controlOptions); + controls[model.id] = new UntypedFormControl(controlState, controlOptions); }); - return new FormGroup(controls, options); + return new UntypedFormGroup(controls, options); }, createAbstractControlOptions(validatorsConfig = null, asyncValidatorsConfig = null, updateOn = null) { return { @@ -184,6 +193,8 @@ describe('EPersonFormComponent', () => { }); paginationService = new PaginationServiceStub(); + route = new ActivatedRouteStub(); + router = new RouterStub(); epersonRegistrationService = jasmine.createSpyObj('epersonRegistrationService', { registerEmail: createSuccessfulRemoteDataObject$(null) @@ -209,6 +220,8 @@ describe('EPersonFormComponent', () => { { provide: PaginationService, useValue: paginationService }, { provide: RequestService, useValue: jasmine.createSpyObj('requestService', ['removeByHrefSubstring'])}, { provide: EpersonRegistrationService, useValue: epersonRegistrationService }, + { provide: ActivatedRoute, useValue: route }, + { provide: Router, useValue: router }, { provide: UUIDService, useValue: getMockUUIDService() }, EPeopleRegistryComponent ], @@ -271,24 +284,18 @@ describe('EPersonFormComponent', () => { fixture.detectChanges(); }); describe('firstName, lastName and email should be required', () => { - it('form should be invalid because the firstName is required', waitForAsync(() => { - fixture.whenStable().then(() => { - expect(component.formGroup.controls.firstName.valid).toBeFalse(); - expect(component.formGroup.controls.firstName.errors.required).toBeTrue(); - }); - })); - it('form should be invalid because the lastName is required', waitForAsync(() => { - fixture.whenStable().then(() => { - expect(component.formGroup.controls.lastName.valid).toBeFalse(); - expect(component.formGroup.controls.lastName.errors.required).toBeTrue(); - }); - })); - it('form should be invalid because the email is required', waitForAsync(() => { - fixture.whenStable().then(() => { - expect(component.formGroup.controls.email.valid).toBeFalse(); - expect(component.formGroup.controls.email.errors.required).toBeTrue(); - }); - })); + it('form should be invalid because the firstName is required', () => { + expect(component.formGroup.controls.firstName.valid).toBeFalse(); + expect(component.formGroup.controls.firstName.errors.required).toBeTrue(); + }); + it('form should be invalid because the lastName is required', () => { + expect(component.formGroup.controls.lastName.valid).toBeFalse(); + expect(component.formGroup.controls.lastName.errors.required).toBeTrue(); + }); + it('form should be invalid because the email is required', () => { + expect(component.formGroup.controls.email.valid).toBeFalse(); + expect(component.formGroup.controls.email.errors.required).toBeTrue(); + }); }); describe('after inserting information firstName,lastName and email not required', () => { @@ -298,24 +305,18 @@ describe('EPersonFormComponent', () => { component.formGroup.controls.email.setValue('test@test.com'); fixture.detectChanges(); }); - it('firstName should be valid because the firstName is set', waitForAsync(() => { - fixture.whenStable().then(() => { + it('firstName should be valid because the firstName is set', () => { expect(component.formGroup.controls.firstName.valid).toBeTrue(); expect(component.formGroup.controls.firstName.errors).toBeNull(); - }); - })); - it('lastName should be valid because the lastName is set', waitForAsync(() => { - fixture.whenStable().then(() => { + }); + it('lastName should be valid because the lastName is set', () => { expect(component.formGroup.controls.lastName.valid).toBeTrue(); expect(component.formGroup.controls.lastName.errors).toBeNull(); - }); - })); - it('email should be valid because the email is set', waitForAsync(() => { - fixture.whenStable().then(() => { + }); + it('email should be valid because the email is set', () => { expect(component.formGroup.controls.email.valid).toBeTrue(); expect(component.formGroup.controls.email.errors).toBeNull(); - }); - })); + }); }); @@ -324,12 +325,10 @@ describe('EPersonFormComponent', () => { component.formGroup.controls.email.setValue('test@test'); fixture.detectChanges(); }); - it('email should not be valid because the email pattern', waitForAsync(() => { - fixture.whenStable().then(() => { + it('email should not be valid because the email pattern', () => { expect(component.formGroup.controls.email.valid).toBeFalse(); expect(component.formGroup.controls.email.errors.pattern).toBeTruthy(); - }); - })); + }); }); describe('after already utilized email', () => { @@ -344,12 +343,10 @@ describe('EPersonFormComponent', () => { fixture.detectChanges(); }); - it('email should not be valid because email is already taken', waitForAsync(() => { - fixture.whenStable().then(() => { + it('email should not be valid because email is already taken', () => { expect(component.formGroup.controls.email.valid).toBeFalse(); expect(component.formGroup.controls.email.errors.emailTaken).toBeTruthy(); - }); - })); + }); }); @@ -401,11 +398,9 @@ describe('EPersonFormComponent', () => { fixture.detectChanges(); }); - it('should emit a new eperson using the correct values', waitForAsync(() => { - fixture.whenStable().then(() => { - expect(component.submitForm.emit).toHaveBeenCalledWith(expected); - }); - })); + it('should emit a new eperson using the correct values', () => { + expect(component.submitForm.emit).toHaveBeenCalledWith(expected); + }); }); describe('with an active eperson', () => { @@ -436,11 +431,9 @@ describe('EPersonFormComponent', () => { fixture.detectChanges(); }); - it('should emit the existing eperson using the correct values', waitForAsync(() => { - fixture.whenStable().then(() => { - expect(component.submitForm.emit).toHaveBeenCalledWith(expectedWithId); - }); - })); + it('should emit the existing eperson using the correct values', () => { + expect(component.submitForm.emit).toHaveBeenCalledWith(expectedWithId); + }); }); }); @@ -499,16 +492,16 @@ describe('EPersonFormComponent', () => { }); - it('the delete button should be active if the eperson can be deleted', () => { + it('the delete button should be visible if the ePerson can be deleted', () => { const deleteButton = fixture.debugElement.query(By.css('.delete-button')); - expect(deleteButton.nativeElement.disabled).toBe(false); + expect(deleteButton).not.toBeNull(); }); - it('the delete button should be disabled if the eperson cannot be deleted', () => { + it('the delete button should be hidden if the ePerson cannot be deleted', () => { component.canDelete$ = observableOf(false); fixture.detectChanges(); const deleteButton = fixture.debugElement.query(By.css('.delete-button')); - expect(deleteButton.nativeElement.disabled).toBe(true); + expect(deleteButton).toBeNull(); }); it('should call the epersonFormComponent delete when clicked on the button', () => { diff --git a/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.ts b/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.ts index 7e24d76d82b..3d9ce42a7d7 100644 --- a/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.ts +++ b/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.ts @@ -1,5 +1,5 @@ import { ChangeDetectorRef, Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core'; -import { FormGroup } from '@angular/forms'; +import { UntypedFormGroup } from '@angular/forms'; import { DynamicCheckboxModel, DynamicFormControlModel, @@ -8,7 +8,7 @@ import { } from '@ng-dynamic-forms/core'; import { TranslateService } from '@ngx-translate/core'; import { combineLatest as observableCombineLatest, Observable, of as observableOf, Subscription } from 'rxjs'; -import { debounceTime, switchMap, take } from 'rxjs/operators'; +import { debounceTime, finalize, map, switchMap, take } from 'rxjs/operators'; import { PaginatedList } from '../../../core/data/paginated-list.model'; import { RemoteData } from '../../../core/data/remote-data'; import { EPersonDataService } from '../../../core/eperson/eperson-data.service'; @@ -37,6 +37,9 @@ import { ValidateEmailNotTaken } from './validators/email-taken.validator'; import { Registration } from '../../../core/shared/registration.model'; 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 { ActivatedRoute, Router } from '@angular/router'; +import { getEPersonsRoute } from '../../access-control-routing-paths'; import { UUIDService } from '../../../core/shared/uuid.service'; @Component({ @@ -109,7 +112,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy { /** * A FormGroup that combines all inputs */ - formGroup: FormGroup; + formGroup: UntypedFormGroup; /** * An EventEmitter that's fired whenever the form is being submitted @@ -166,6 +169,15 @@ export class EPersonFormComponent implements OnInit, OnDestroy { */ isImpersonated = false; + /** + * A boolean that indicate if to display EPersonForm's Rest password button + */ + displayResetPassword = false; + + /** + * A string that indicate the label of Submit button + */ + submitLabel = 'form.create'; /** * Subscription to email field value change */ @@ -184,12 +196,17 @@ export class EPersonFormComponent implements OnInit, OnDestroy { private paginationService: PaginationService, public requestService: RequestService, private epersonRegistrationService: EpersonRegistrationService, + public dsoNameService: DSONameService, + protected route: ActivatedRoute, + protected router: Router, private uuidService: UUIDService ) { this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => { this.epersonInitial = eperson; if (hasValue(eperson)) { this.isImpersonated = this.authService.isImpersonatingUser(eperson.id); + this.displayResetPassword = true; + this.submitLabel = 'form.submit'; } })); } @@ -202,15 +219,17 @@ export class EPersonFormComponent implements OnInit, OnDestroy { * This method will initialise the page */ initialisePage() { - - observableCombineLatest( + this.subs.push(this.epersonService.findById(this.route.snapshot.params.id).subscribe((ePersonRD: RemoteData) => { + this.epersonService.editEPerson(ePersonRD.payload); + })); + observableCombineLatest([ this.translateService.get(`${this.messagePrefix}.firstName`), this.translateService.get(`${this.messagePrefix}.lastName`), this.translateService.get(`${this.messagePrefix}.email`), this.translateService.get(`${this.messagePrefix}.canLogIn`), this.translateService.get(`${this.messagePrefix}.requireCertificate`), this.translateService.get(`${this.messagePrefix}.emailHint`), - ).subscribe(([firstName, lastName, email, canLogIn, requireCertificate, emailHint]) => { + ]).subscribe(([firstName, lastName, email, canLogIn, requireCertificate, emailHint]) => { this.firstName = new DynamicInputModel({ id: 'firstName', label: firstName, @@ -328,6 +347,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy { onCancel() { this.epersonService.cancelEditEPerson(); this.cancelForm.emit(); + void this.router.navigate([getEPersonsRoute()]); } /** @@ -377,10 +397,12 @@ export class EPersonFormComponent implements OnInit, OnDestroy { getFirstCompletedRemoteData() ).subscribe((rd: RemoteData) => { if (rd.hasSucceeded) { - this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.created.success', { name: ePersonToCreate.name })); + this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.created.success', { name: this.dsoNameService.getName(ePersonToCreate) })); this.submitForm.emit(ePersonToCreate); + this.epersonService.clearEPersonRequests(); + void this.router.navigateByUrl(getEPersonsRoute()); } else { - this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.created.failure', { name: ePersonToCreate.name })); + this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.created.failure', { name: this.dsoNameService.getName(ePersonToCreate) })); this.cancelForm.emit(); } }); @@ -416,10 +438,11 @@ export class EPersonFormComponent implements OnInit, OnDestroy { const response = this.epersonService.updateEPerson(editedEperson); response.pipe(getFirstCompletedRemoteData()).subscribe((rd: RemoteData) => { if (rd.hasSucceeded) { - this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.edited.success', { name: editedEperson.name })); + this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.edited.success', { name: this.dsoNameService.getName(editedEperson) })); this.submitForm.emit(editedEperson); + void this.router.navigateByUrl(getEPersonsRoute()); } else { - this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.edited.failure', { name: editedEperson.name })); + this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.edited.failure', { name: this.dsoNameService.getName(editedEperson) })); this.cancelForm.emit(); } }); @@ -452,31 +475,43 @@ export class EPersonFormComponent implements OnInit, OnDestroy { * Deletes the EPerson from the Repository. The EPerson will be the only that this form is showing. * It'll either show a success or error message depending on whether the delete was successful or not. */ - delete() { - this.epersonService.getActiveEPerson().pipe(take(1)).subscribe((eperson: EPerson) => { - const modalRef = this.modalService.open(ConfirmationModalComponent); - modalRef.componentInstance.dso = eperson; - modalRef.componentInstance.headerLabel = 'confirmation-modal.delete-eperson.header'; - modalRef.componentInstance.infoLabel = 'confirmation-modal.delete-eperson.info'; - modalRef.componentInstance.cancelLabel = 'confirmation-modal.delete-eperson.cancel'; - modalRef.componentInstance.confirmLabel = 'confirmation-modal.delete-eperson.confirm'; - modalRef.componentInstance.brandColor = 'danger'; - modalRef.componentInstance.confirmIcon = 'fas fa-trash'; - modalRef.componentInstance.response.pipe(take(1)).subscribe((confirm: boolean) => { - if (confirm) { - if (hasValue(eperson.id)) { - this.epersonService.deleteEPerson(eperson).pipe(getFirstCompletedRemoteData()).subscribe((restResponse: RemoteData) => { - if (restResponse.hasSucceeded) { - this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', { name: eperson.name })); - this.submitForm.emit(); - } else { - this.notificationsService.error('Error occured when trying to delete EPerson with id: ' + eperson.id + ' with code: ' + restResponse.statusCode + ' and message: ' + restResponse.errorMessage); - } - this.cancelForm.emit(); - }); - } - } - }); + delete(): void { + this.epersonService.getActiveEPerson().pipe( + take(1), + switchMap((eperson: EPerson) => { + const modalRef = this.modalService.open(ConfirmationModalComponent); + modalRef.componentInstance.dso = eperson; + modalRef.componentInstance.headerLabel = 'confirmation-modal.delete-eperson.header'; + modalRef.componentInstance.infoLabel = 'confirmation-modal.delete-eperson.info'; + modalRef.componentInstance.cancelLabel = 'confirmation-modal.delete-eperson.cancel'; + modalRef.componentInstance.confirmLabel = 'confirmation-modal.delete-eperson.confirm'; + modalRef.componentInstance.brandColor = 'danger'; + modalRef.componentInstance.confirmIcon = 'fas fa-trash'; + + return modalRef.componentInstance.response.pipe( + take(1), + switchMap((confirm: boolean) => { + if (confirm && hasValue(eperson.id)) { + this.canDelete$ = observableOf(false); + return this.epersonService.deleteEPerson(eperson).pipe( + getFirstCompletedRemoteData(), + map((restResponse: RemoteData) => ({ restResponse, eperson })) + ); + } else { + return observableOf(null); + } + }), + finalize(() => this.canDelete$ = observableOf(true)) + ); + }) + ).subscribe(({ restResponse, eperson }: { restResponse: RemoteData | null, eperson: EPerson }) => { + if (restResponse?.hasSucceeded) { + this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', { name: this.dsoNameService.getName(eperson) })); + void this.router.navigate([getEPersonsRoute()]); + } else { + this.notificationsService.error(`Error occurred when trying to delete EPerson with id: ${eperson?.id} with code: ${restResponse?.statusCode} and message: ${restResponse?.errorMessage}`); + } + this.cancelForm.emit(); }); } @@ -512,7 +547,6 @@ export class EPersonFormComponent implements OnInit, OnDestroy { * Cancel the current edit when component is destroyed & unsub all subscriptions */ ngOnDestroy(): void { - this.onCancel(); this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe()); this.paginationService.clearPagination(this.config.id); if (hasValue(this.emailValueChangeSubscribe)) { @@ -520,16 +554,6 @@ export class EPersonFormComponent implements OnInit, OnDestroy { } } - /** - * This method will ensure that the page gets reset and that the cache is cleared - */ - reset() { - this.epersonService.getActiveEPerson().pipe(take(1)).subscribe((eperson: EPerson) => { - this.requestService.removeByHrefSubstring(eperson.self); - }); - this.initialisePage(); - } - /** * Checks for the given ePerson if there is already an ePerson in the system with that email * and shows notification if this is the case @@ -545,7 +569,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy { .subscribe((list: PaginatedList) => { if (list.totalElements > 0) { this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.' + notificationSection + '.failure.emailInUse', { - name: ePerson.name, + name: this.dsoNameService.getName(ePerson), email: ePerson.email })); } diff --git a/src/app/access-control/epeople-registry/eperson-resolver.service.ts b/src/app/access-control/epeople-registry/eperson-resolver.service.ts new file mode 100644 index 00000000000..1db8e70d899 --- /dev/null +++ b/src/app/access-control/epeople-registry/eperson-resolver.service.ts @@ -0,0 +1,53 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router'; +import { Observable } from 'rxjs'; +import { EPerson } from '../../core/eperson/models/eperson.model'; +import { RemoteData } from '../../core/data/remote-data'; +import { getFirstCompletedRemoteData } from '../../core/shared/operators'; +import { ResolvedAction } from '../../core/resolving/resolver.actions'; +import { EPersonDataService } from '../../core/eperson/eperson-data.service'; +import { Store } from '@ngrx/store'; +import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; + +export const EPERSON_EDIT_FOLLOW_LINKS: FollowLinkConfig[] = [ + followLink('groups'), +]; + +/** + * This class represents a resolver that requests a specific {@link EPerson} before the route is activated + */ +@Injectable({ + providedIn: 'root', +}) +export class EPersonResolver implements Resolve> { + + constructor( + protected ePersonService: EPersonDataService, + protected store: Store, + ) { + } + + /** + * Method for resolving a {@link EPerson} based on the parameters in the current route + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @returns `Observable<>` Emits the found {@link EPerson} based on the parameters in the current + * route, or an error if something went wrong + */ + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { + const ePersonRD$: Observable> = this.ePersonService.findById(route.params.id, + true, + false, + ...EPERSON_EDIT_FOLLOW_LINKS, + ).pipe( + getFirstCompletedRemoteData(), + ); + + ePersonRD$.subscribe((ePersonRD: RemoteData) => { + this.store.dispatch(new ResolvedAction(state.url, ePersonRD.payload)); + }); + + return ePersonRD$; + } + +} diff --git a/src/app/access-control/group-registry/group-form/group-form.component.html b/src/app/access-control/group-registry/group-form/group-form.component.html index d86adc674b6..f31de0db1b5 100644 --- a/src/app/access-control/group-registry/group-form/group-form.component.html +++ b/src/app/access-control/group-registry/group-form/group-form.component.html @@ -2,13 +2,13 @@
-
+

{{messagePrefix + '.head.create' | translate}}

- +

+ [content]="(messagePrefix + '.alert.workflowGroup' | translate:{ name: dsoNameService.getName((getLinkedDSO(groupBeingEdited) | async)?.payload), comcol: (getLinkedDSO(groupBeingEdited) | async)?.payload?.type, comcolEditRolesRoute: (getLinkedEditRolesRoute(groupBeingEdited) | async) })"> [displayCancel]="false" (submitForm)="onSubmit()">
-
-
+
diff --git a/src/app/access-control/group-registry/group-form/group-form.component.spec.ts b/src/app/access-control/group-registry/group-form/group-form.component.spec.ts index a7a7cb5be46..f8c5f3cd870 100644 --- a/src/app/access-control/group-registry/group-form/group-form.component.spec.ts +++ b/src/app/access-control/group-registry/group-form/group-form.component.spec.ts @@ -2,7 +2,7 @@ import { CommonModule } from '@angular/common'; import { HttpClient } from '@angular/common/http'; import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; +import { UntypedFormControl, UntypedFormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; import { BrowserModule, By } from '@angular/platform-browser'; import { ActivatedRoute, Router } from '@angular/router'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; @@ -36,6 +36,8 @@ import { NotificationsServiceStub } from '../../../shared/testing/notifications- import { Operation } from 'fast-json-patch'; import { ValidateGroupExists } from './validators/group-exists.validator'; import { NoContent } from '../../../core/shared/NoContent.model'; +import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; +import { DSONameServiceMock } from '../../../shared/mocks/dso-name.service.mock'; describe('GroupFormComponent', () => { let component: GroupFormComponent; @@ -130,9 +132,9 @@ describe('GroupFormComponent', () => { const controlModel = model; const controlState = { value: controlModel.value, disabled: controlModel.disabled }; const controlOptions = this.createAbstractControlOptions(controlModel.validators, controlModel.asyncValidators, controlModel.updateOn); - controls[model.id] = new FormControl(controlState, controlOptions); + controls[model.id] = new UntypedFormControl(controlState, controlOptions); }); - return new FormGroup(controls, options); + return new UntypedFormGroup(controls, options); }, createAbstractControlOptions(validatorsConfig = null, asyncValidatorsConfig = null, updateOn = null) { return { @@ -188,7 +190,7 @@ describe('GroupFormComponent', () => { translateService = getMockTranslateService(); router = new RouterMock(); notificationService = new NotificationsServiceStub(); - TestBed.configureTestingModule({ + return TestBed.configureTestingModule({ imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, TranslateModule.forRoot({ loader: { @@ -198,7 +200,8 @@ describe('GroupFormComponent', () => { }), ], declarations: [GroupFormComponent], - providers: [GroupFormComponent, + providers: [ + { provide: DSONameService, useValue: new DSONameServiceMock() }, { provide: EPersonDataService, useValue: ePersonDataServiceStub }, { provide: GroupDataService, useValue: groupsDataServiceStub }, { provide: DSpaceObjectDataService, useValue: dsoDataServiceStub }, @@ -240,8 +243,8 @@ describe('GroupFormComponent', () => { fixture.detectChanges(); }); - it('should emit a new group using the correct values', waitForAsync(() => { - fixture.whenStable().then(() => { + it('should emit a new group using the correct values', (async () => { + await fixture.whenStable().then(() => { expect(component.submitForm.emit).toHaveBeenCalledWith(expected); }); })); @@ -303,8 +306,8 @@ describe('GroupFormComponent', () => { expect(groupsDataServiceStub.patch).toHaveBeenCalledWith(expected, operations); }); - it('should emit the existing group using the correct new values', waitForAsync(() => { - fixture.whenStable().then(() => { + it('should emit the existing group using the correct new values', (async () => { + await fixture.whenStable().then(() => { expect(component.submitForm.emit).toHaveBeenCalledWith(expected2); }); })); diff --git a/src/app/access-control/group-registry/group-form/group-form.component.ts b/src/app/access-control/group-registry/group-form/group-form.component.ts index 44e128acb60..b64eb4b01d3 100644 --- a/src/app/access-control/group-registry/group-form/group-form.component.ts +++ b/src/app/access-control/group-registry/group-form/group-form.component.ts @@ -1,5 +1,5 @@ import { ChangeDetectorRef, Component, EventEmitter, HostListener, OnDestroy, OnInit, Output } from '@angular/core'; -import { FormGroup } from '@angular/forms'; +import { UntypedFormGroup } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { @@ -12,7 +12,6 @@ import { TranslateService } from '@ngx-translate/core'; import { combineLatest as observableCombineLatest, Observable, - ObservedValueOf, of as observableOf, Subscription, } from 'rxjs'; @@ -37,7 +36,7 @@ import { getFirstSucceededRemoteDataPayload, getRemoteDataPayload } from '../../../core/shared/operators'; -import { AlertType } from '../../../shared/alert/aletr-type'; +import { AlertType } from '../../../shared/alert/alert-type'; import { ConfirmationModalComponent } from '../../../shared/confirmation-modal/confirmation-modal.component'; import { hasValue, hasValueOperator, isNotEmpty } from '../../../shared/empty.util'; import { FormBuilderService } from '../../../shared/form/builder/form-builder.service'; @@ -46,7 +45,9 @@ import { followLink } from '../../../shared/utils/follow-link-config.model'; import { NoContent } from '../../../core/shared/NoContent.model'; import { Operation } from 'fast-json-patch'; import { ValidateGroupExists } from './validators/group-exists.validator'; +import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; import { environment } from '../../../../environments/environment'; +import { getGroupEditRoute, getGroupsRoute } from '../../access-control-routing-paths'; @Component({ selector: 'ds-group-form', @@ -95,7 +96,7 @@ export class GroupFormComponent implements OnInit, OnDestroy { /** * A FormGroup that combines all inputs */ - formGroup: FormGroup; + formGroup: UntypedFormGroup; /** * An EventEmitter that's fired whenever the form is being submitted @@ -134,7 +135,8 @@ export class GroupFormComponent implements OnInit, OnDestroy { groupNameValueChangeSubscribe: Subscription; - constructor(public groupDataService: GroupDataService, + constructor( + public groupDataService: GroupDataService, private ePersonDataService: EPersonDataService, private dSpaceObjectDataService: DSpaceObjectDataService, private formBuilderService: FormBuilderService, @@ -145,7 +147,9 @@ export class GroupFormComponent implements OnInit, OnDestroy { private authorizationService: AuthorizationDataService, private modalService: NgbModal, public requestService: RequestService, - protected changeDetectorRef: ChangeDetectorRef) { + protected changeDetectorRef: ChangeDetectorRef, + public dsoNameService: DSONameService, + ) { } ngOnInit() { @@ -161,19 +165,19 @@ export class GroupFormComponent implements OnInit, OnDestroy { this.canEdit$ = this.groupDataService.getActiveGroup().pipe( hasValueOperator(), switchMap((group: Group) => { - return observableCombineLatest( + return observableCombineLatest([ this.authorizationService.isAuthorized(FeatureID.CanDelete, isNotEmpty(group) ? group.self : undefined), this.hasLinkedDSO(group), - (isAuthorized: ObservedValueOf>, hasLinkedDSO: ObservedValueOf>) => { - return isAuthorized && !hasLinkedDSO; - }); - }) + ]).pipe( + map(([isAuthorized, hasLinkedDSO]: [boolean, boolean]) => isAuthorized && !hasLinkedDSO), + ); + }), ); - observableCombineLatest( + observableCombineLatest([ this.translateService.get(`${this.messagePrefix}.groupName`), this.translateService.get(`${this.messagePrefix}.groupCommunity`), this.translateService.get(`${this.messagePrefix}.groupDescription`) - ).subscribe(([groupName, groupCommunity, groupDescription]) => { + ]).subscribe(([groupName, groupCommunity, groupDescription]) => { this.groupName = new DynamicInputModel({ id: 'groupName', label: groupName, @@ -211,12 +215,12 @@ export class GroupFormComponent implements OnInit, OnDestroy { } this.subs.push( - observableCombineLatest( + observableCombineLatest([ this.groupDataService.getActiveGroup(), this.canEdit$, this.groupDataService.getActiveGroup() .pipe(filter((activeGroup) => hasValue(activeGroup)),switchMap((activeGroup) => this.getLinkedDSO(activeGroup).pipe(getFirstSucceededRemoteDataPayload()))) - ).subscribe(([activeGroup, canEdit, linkedObject]) => { + ]).subscribe(([activeGroup, canEdit, linkedObject]) => { if (activeGroup != null) { @@ -226,12 +230,14 @@ export class GroupFormComponent implements OnInit, OnDestroy { this.groupBeingEdited = activeGroup; if (linkedObject?.name) { - this.formBuilderService.insertFormGroupControl(1, this.formGroup, this.formModel, this.groupCommunity); - this.formGroup.patchValue({ - groupName: activeGroup.name, - groupCommunity: linkedObject?.name ?? '', - groupDescription: activeGroup.firstMetadataValue('dc.description'), - }); + if (!this.formGroup.controls.groupCommunity) { + this.formBuilderService.insertFormGroupControl(1, this.formGroup, this.formModel, this.groupCommunity); + this.formGroup.patchValue({ + groupName: activeGroup.name, + groupCommunity: linkedObject?.name ?? '', + groupDescription: activeGroup.firstMetadataValue('dc.description'), + }); + } } else { this.formModel = [ this.groupName, @@ -259,7 +265,7 @@ export class GroupFormComponent implements OnInit, OnDestroy { onCancel() { this.groupDataService.cancelEditGroup(); this.cancelForm.emit(); - this.router.navigate([this.groupDataService.getGroupRegistryRouterLink()]); + void this.router.navigate([getGroupsRoute()]); } /** @@ -306,7 +312,7 @@ export class GroupFormComponent implements OnInit, OnDestroy { const groupSelfLink = rd.payload._links.self.href; this.setActiveGroupWithLink(groupSelfLink); this.groupDataService.clearGroupsRequests(); - this.router.navigateByUrl(this.groupDataService.getGroupEditPageRouterLinkWithID(rd.payload.uuid)); + void this.router.navigateByUrl(getGroupEditRoute(rd.payload.uuid)); } } else { this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.created.failure', { name: groupToCreate.name })); @@ -331,7 +337,7 @@ export class GroupFormComponent implements OnInit, OnDestroy { .subscribe((list: PaginatedList) => { if (list.totalElements > 0) { this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.' + notificationSection + '.failure.groupNameInUse', { - name: group.name + name: this.dsoNameService.getName(group), })); } })); @@ -364,10 +370,10 @@ export class GroupFormComponent implements OnInit, OnDestroy { getFirstCompletedRemoteData() ).subscribe((rd: RemoteData) => { if (rd.hasSucceeded) { - this.notificationsService.success(this.translateService.get(this.messagePrefix + '.notification.edited.success', { name: rd.payload.name })); + this.notificationsService.success(this.translateService.get(this.messagePrefix + '.notification.edited.success', { name: this.dsoNameService.getName(rd.payload) })); this.submitForm.emit(rd.payload); } else { - this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.edited.failure', { name: group.name })); + this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.edited.failure', { name: this.dsoNameService.getName(group) })); this.cancelForm.emit(); } }); @@ -427,11 +433,11 @@ export class GroupFormComponent implements OnInit, OnDestroy { this.groupDataService.delete(group.id).pipe(getFirstCompletedRemoteData()) .subscribe((rd: RemoteData) => { if (rd.hasSucceeded) { - this.notificationsService.success(this.translateService.get(this.messagePrefix + '.notification.deleted.success', { name: group.name })); + this.notificationsService.success(this.translateService.get(this.messagePrefix + '.notification.deleted.success', { name: this.dsoNameService.getName(group) })); this.onCancel(); } else { this.notificationsService.error( - this.translateService.get(this.messagePrefix + '.notification.deleted.failure.title', { name: group.name }), + this.translateService.get(this.messagePrefix + '.notification.deleted.failure.title', { name: this.dsoNameService.getName(group) }), this.translateService.get(this.messagePrefix + '.notification.deleted.failure.content', { cause: rd.errorMessage })); } }); diff --git a/src/app/access-control/group-registry/group-form/members-list/members-list.component.html b/src/app/access-control/group-registry/group-form/members-list/members-list.component.html index 282ee896741..c0c77f44ebc 100644 --- a/src/app/access-control/group-registry/group-form/members-list/members-list.component.html +++ b/src/app/access-control/group-registry/group-form/members-list/members-list.component.html @@ -1,51 +1,17 @@

{{messagePrefix + '.head' | translate}}

- - -
-
- -
-
-
- - - - -
-
-
- -
-
+

{{messagePrefix + '.headMembers' | translate}}

-
- +
@@ -55,31 +21,26 @@ - - - + + + @@ -89,23 +50,50 @@

{{messagePrefix + '.headMembers' | translate}}

+ - +
+
+ + + + +
+
+
+ +
+ + +
-
{{messagePrefix + '.table.id' | translate}}
{{ePerson.eperson.id}}{{ePerson.eperson.name}}
{{eperson.id}} + + {{ dsoNameService.getName(eperson) }} + + - {{messagePrefix + '.table.email' | translate}}: {{ ePerson.eperson.email ? ePerson.eperson.email : '-' }}
- {{messagePrefix + '.table.netid' | translate}}: {{ ePerson.eperson.netid ? ePerson.eperson.netid : '-' }} + {{messagePrefix + '.table.email' | translate}}: {{ eperson.email ? eperson.email : '-' }}
+ {{messagePrefix + '.table.netid' | translate}}: {{ eperson.netid ? eperson.netid : '-' }}
- - -
+
@@ -115,28 +103,24 @@

{{messagePrefix + '.headMembers' | translate}}

- - - + + + - - + + - - + + - + diff --git a/src/app/admin/admin-registries/metadata-schema/metadata-schema.component.scss b/src/app/admin/admin-registries/metadata-schema/metadata-schema.component.scss index 98d86595708..49e03846614 100644 --- a/src/app/admin/admin-registries/metadata-schema/metadata-schema.component.scss +++ b/src/app/admin/admin-registries/metadata-schema/metadata-schema.component.scss @@ -1,3 +1,8 @@ .selectable-row:hover { cursor: pointer; } + +:host ::ng-deep #metadatadatafieldgroup { + display: flex; + flex-wrap: wrap; +} diff --git a/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.spec.ts b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.spec.ts index b3ec9242b2b..e219afff90f 100644 --- a/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.spec.ts +++ b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.spec.ts @@ -21,7 +21,7 @@ import { import { getMockThemeService } from '../../../../../shared/mocks/theme-service.mock'; import { ThemeService } from '../../../../../shared/theme-support/theme.service'; import { AccessStatusDataService } from '../../../../../core/data/access-status-data.service'; -import { AccessStatusObject } from '../../../../../shared/object-list/access-status-badge/access-status.model'; +import { AccessStatusObject } from '../../../../../shared/object-collection/shared/badges/access-status-badge/access-status.model'; import { AuthService } from '../../../../../core/auth/auth.service'; import { AuthServiceStub } from '../../../../../shared/testing/auth-service.stub'; import { FileService } from '../../../../../core/shared/file.service'; diff --git a/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.ts b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.ts index 1ab8fee8c29..dab6694f368 100644 --- a/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.ts +++ b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.ts @@ -13,6 +13,7 @@ import { BitstreamDataService } from '../../../../../core/data/bitstream-data.se import { GenericConstructor } from '../../../../../core/shared/generic-constructor'; import { ListableObjectDirective } from '../../../../../shared/object-collection/shared/listable-object/listable-object.directive'; import { ThemeService } from '../../../../../shared/theme-support/theme.service'; +import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service'; @listableObjectComponent(ItemSearchResult, ViewMode.GridElement, Context.AdminSearch) @Component({ @@ -28,12 +29,14 @@ export class ItemAdminSearchResultGridElementComponent extends SearchResultGridE @ViewChild('badges', { static: true }) badges: ElementRef; @ViewChild('buttons', { static: true }) buttons: ElementRef; - constructor(protected truncatableService: TruncatableService, - protected bitstreamDataService: BitstreamDataService, - private themeService: ThemeService, - private componentFactoryResolver: ComponentFactoryResolver + constructor( + public dsoNameService: DSONameService, + protected truncatableService: TruncatableService, + protected bitstreamDataService: BitstreamDataService, + private themeService: ThemeService, + private componentFactoryResolver: ComponentFactoryResolver, ) { - super(truncatableService, bitstreamDataService); + super(dsoNameService, truncatableService, bitstreamDataService); } /** diff --git a/src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/item-search-result/item-admin-search-result-list-element.component.html b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/item-search-result/item-admin-search-result-list-element.component.html index 259512552c8..991508335fa 100644 --- a/src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/item-search-result/item-admin-search-result-list-element.component.html +++ b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/item-search-result/item-admin-search-result-list-element.component.html @@ -2,6 +2,5 @@ [viewMode]="viewModes.ListElement" [index]="index" [linkType]="linkType" - [listID]="listID" - [hideBadges]="true"> + [listID]="listID"> diff --git a/src/app/admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.ts b/src/app/admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.ts index d6cd803622b..b195526d1c0 100644 --- a/src/app/admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.ts +++ b/src/app/admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.ts @@ -12,8 +12,7 @@ import { Router } from '@angular/router'; * Represents a non-expandable section in the admin sidebar */ @Component({ - /* eslint-disable @angular-eslint/component-selector */ - selector: 'li[ds-admin-sidebar-section]', + selector: 'ds-admin-sidebar-section', templateUrl: './admin-sidebar-section.component.html', styleUrls: ['./admin-sidebar-section.component.scss'], diff --git a/src/app/admin/admin-sidebar/admin-sidebar.component.html b/src/app/admin/admin-sidebar/admin-sidebar.component.html index ef220b834ba..fe7e5595ab0 100644 --- a/src/app/admin/admin-sidebar/admin-sidebar.component.html +++ b/src/app/admin/admin-sidebar/admin-sidebar.component.html @@ -26,10 +26,10 @@

{{ 'menu.header.admin' | translate }}

- +
  • - +
  • diff --git a/src/app/admin/admin-workflow-page/admin-workflow-search-results/actions/workspace-item/supervision-order-status/supervision-order-status.component.ts b/src/app/admin/admin-workflow-page/admin-workflow-search-results/actions/workspace-item/supervision-order-status/supervision-order-status.component.ts index 11b6400ffd1..93c6441e920 100644 --- a/src/app/admin/admin-workflow-page/admin-workflow-search-results/actions/workspace-item/supervision-order-status/supervision-order-status.component.ts +++ b/src/app/admin/admin-workflow-page/admin-workflow-search-results/actions/workspace-item/supervision-order-status/supervision-order-status.component.ts @@ -8,6 +8,7 @@ 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; @@ -33,6 +34,11 @@ export class SupervisionOrderStatusComponent implements OnChanges { @Output() delete: EventEmitter = new EventEmitter(); + constructor( + public dsoNameService: DSONameService, + ) { + } + ngOnChanges(changes: SimpleChanges): void { if (changes && changes.supervisionOrderList) { this.getSupervisionOrderEntries(changes.supervisionOrderList.currentValue) diff --git a/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.ts b/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.ts index 68f10916d55..fd9d21e227d 100644 --- a/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.ts +++ b/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workflow-item/workflow-item-search-result-admin-workflow-grid-element.component.ts @@ -23,6 +23,7 @@ import { import { take } from 'rxjs/operators'; import { WorkflowItemSearchResult } from '../../../../../shared/object-collection/shared/workflow-item-search-result.model'; import { ThemeService } from '../../../../../shared/theme-support/theme.service'; +import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service'; @listableObjectComponent(WorkflowItemSearchResult, ViewMode.GridElement, Context.AdminWorkflowSearch) @Component({ @@ -55,13 +56,14 @@ export class WorkflowItemSearchResultAdminWorkflowGridElementComponent extends S public item$: Observable; constructor( + public dsoNameService: DSONameService, private componentFactoryResolver: ComponentFactoryResolver, private linkService: LinkService, protected truncatableService: TruncatableService, private themeService: ThemeService, protected bitstreamDataService: BitstreamDataService ) { - super(truncatableService, bitstreamDataService); + super(dsoNameService, truncatableService, bitstreamDataService); } /** diff --git a/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workspace-item/workspace-item-search-result-admin-workflow-grid-element.component.ts b/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workspace-item/workspace-item-search-result-admin-workflow-grid-element.component.ts index f18c18ca1c9..d6f39e79feb 100644 --- a/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workspace-item/workspace-item-search-result-admin-workflow-grid-element.component.ts +++ b/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-grid-element/workspace-item/workspace-item-search-result-admin-workflow-grid-element.component.ts @@ -1,4 +1,4 @@ -import { Component, ComponentFactoryResolver, ElementRef, ViewChild } from '@angular/core'; +import { Component, ComponentFactoryResolver, ElementRef, ViewChild, OnInit } from '@angular/core'; import { BehaviorSubject, Observable } from 'rxjs'; import { map, mergeMap, take, tap } from 'rxjs/operators'; @@ -36,6 +36,7 @@ import { DSpaceObject } from '../../../../../core/shared/dspace-object.model'; import { SupervisionOrder } from '../../../../../core/supervision-order/models/supervision-order.model'; import { 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({ @@ -46,7 +47,7 @@ import { SupervisionOrderDataService } from '../../../../../core/supervision-ord /** * The component for displaying a grid element for an workflow item on the admin workflow search page */ -export class WorkspaceItemSearchResultAdminWorkflowGridElementComponent extends SearchResultGridElementComponent { +export class WorkspaceItemSearchResultAdminWorkflowGridElementComponent extends SearchResultGridElementComponent implements OnInit { /** * The item linked to the workspace item @@ -79,6 +80,7 @@ export class WorkspaceItemSearchResultAdminWorkflowGridElementComponent extends @ViewChild('buttons', { static: true }) buttons: ElementRef; constructor( + public dsoNameService: DSONameService, private componentFactoryResolver: ComponentFactoryResolver, private linkService: LinkService, protected truncatableService: TruncatableService, @@ -86,7 +88,7 @@ export class WorkspaceItemSearchResultAdminWorkflowGridElementComponent extends protected bitstreamDataService: BitstreamDataService, protected supervisionOrderDataService: SupervisionOrderDataService, ) { - super(truncatableService, bitstreamDataService); + super(dsoNameService, truncatableService, bitstreamDataService); } /** diff --git a/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component.ts b/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component.ts index b1db3f99ce2..d0e773d696c 100644 --- a/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component.ts +++ b/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workflow-item/workflow-item-search-result-admin-workflow-list-element.component.ts @@ -39,7 +39,7 @@ export class WorkflowItemSearchResultAdminWorkflowListElementComponent extends S constructor(private linkService: LinkService, protected truncatableService: TruncatableService, - protected dsoNameService: DSONameService, + public dsoNameService: DSONameService, @Inject(APP_CONFIG) protected appConfig: AppConfig ) { super(truncatableService, dsoNameService, appConfig); diff --git a/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workspace-item/workspace-item-search-result-admin-workflow-list-element.component.ts b/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workspace-item/workspace-item-search-result-admin-workflow-list-element.component.ts index 597ed8bbe7e..3d6d1c8e445 100644 --- a/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workspace-item/workspace-item-search-result-admin-workflow-list-element.component.ts +++ b/src/app/admin/admin-workflow-page/admin-workflow-search-results/admin-workflow-search-result-list-element/workspace-item/workspace-item-search-result-admin-workflow-list-element.component.ts @@ -59,7 +59,7 @@ export class WorkspaceItemSearchResultAdminWorkflowListElementComponent extends public supervisionOrder$: BehaviorSubject = new BehaviorSubject([]); constructor(private linkService: LinkService, - protected dsoNameService: DSONameService, + public dsoNameService: DSONameService, protected supervisionOrderDataService: SupervisionOrderDataService, protected truncatableService: TruncatableService, @Inject(APP_CONFIG) protected appConfig: AppConfig diff --git a/src/app/admin/admin.module.ts b/src/app/admin/admin.module.ts index 474507964ab..0c35ebb49e2 100644 --- a/src/app/admin/admin.module.ts +++ b/src/app/admin/admin.module.ts @@ -12,6 +12,7 @@ import { ExpandableAdminSidebarSectionComponent } from './admin-sidebar/expandab import { AdminEditUserAgreementComponent } from './admin-edit-user-agreement/admin-edit-user-agreement.component'; import { EditCmsMetadataComponent } from './edit-cms-metadata/edit-cms-metadata.component'; import { BatchImportPageComponent } from './admin-import-batch-page/batch-import-page.component'; +import { UiSwitchModule } from 'ngx-ui-switch'; import { UploadModule } from '../shared/upload/upload.module'; const ENTRY_COMPONENTS = [ @@ -29,6 +30,7 @@ const ENTRY_COMPONENTS = [ AdminSearchModule.withEntryComponents(), AdminWorkflowModuleModule.withEntryComponents(), SharedModule, + UiSwitchModule, UploadModule, ], declarations: [ diff --git a/src/app/admin/edit-cms-metadata/edit-cms-metadata.component.spec.ts b/src/app/admin/edit-cms-metadata/edit-cms-metadata.component.spec.ts index e351fc8df07..83dd4cfd34f 100644 --- a/src/app/admin/edit-cms-metadata/edit-cms-metadata.component.spec.ts +++ b/src/app/admin/edit-cms-metadata/edit-cms-metadata.component.spec.ts @@ -32,7 +32,9 @@ describe('EditCmsMetadataComponent', () => { ['nl', ''], ['pt', ''], ['fr', ''], - ['lv', ''] + ['lv', ''], + ['bn', ''], + ['el', ''], ]); beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ @@ -102,8 +104,9 @@ describe('EditCmsMetadataComponent', () => { }); it('should render textareas of the languages', () => { - const languagesLength = environment.languages.length; + const languagesLength = environment.languages.filter((l) => l.active).length; const textareas = fixture.debugElement.queryAll(By.css('textarea')); + console.log(textareas.length, languagesLength); expect(textareas).toHaveSize(languagesLength); }); diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 20e03315d94..a5482ba9009 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -261,7 +261,7 @@ import { RedirectService } from './redirect/redirect.service'; { path: REQUEST_COPY_MODULE_PATH, loadChildren: () => import('./request-copy/request-copy.module').then((m) => m.RequestCopyModule), - canActivate: [AuthenticatedGuard, EndUserAgreementCurrentUserGuard] + canActivate: [EndUserAgreementCurrentUserGuard] }, { path: FORBIDDEN_PATH, @@ -271,6 +271,7 @@ import { RedirectService } from './redirect/redirect.service'; path: STATISTICS_PAGE_PATH, loadChildren: () => import('./statistics-page/statistics-page-routing.module') .then((m) => m.StatisticsPageRoutingModule), + canActivate: [EndUserAgreementCurrentUserGuard], }, { path: HEALTH_PAGE_PATH, @@ -280,7 +281,7 @@ import { RedirectService } from './redirect/redirect.service'; { path: ACCESS_CONTROL_MODULE_PATH, loadChildren: () => import('./access-control/access-control.module').then((m) => m.AccessControlModule), - canActivate: [GroupAdministratorGuard], + canActivate: [GroupAdministratorGuard, EndUserAgreementCurrentUserGuard], }, { path: 'edit-item-relationships', diff --git a/src/app/audit-page/object-audit-overview/object-audit-overview.component.html b/src/app/audit-page/object-audit-overview/object-audit-overview.component.html index 68a4a3528b8..8129e7a0868 100644 --- a/src/app/audit-page/object-audit-overview/object-audit-overview.component.html +++ b/src/app/audit-page/object-audit-overview/object-audit-overview.component.html @@ -4,7 +4,7 @@

    {{'audit.object.overview.title' | translate}}

    -

    {{ object.name }} ({{object.type}})

    +

    {{ object.name }} ({{object.type}})

    @@ -61,6 +61,7 @@

    {{ object.name }} ({{object.type}})

    +

    {{'audit.object.overview.disabled.message' | translate}}

    diff --git a/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.html b/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.html index f9f8c4aea74..91d4d91bc31 100644 --- a/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.html +++ b/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.html @@ -1,6 +1,6 @@
    -

    {{'bitstream.download.page' | translate: {bitstream: (fileName$ | async)} }}

    +

    {{'bitstream.download.page' | translate:{ bitstream: dsoNameService.getName((bitstream$ | async)) } }}

    -

    {{ 'collection.delete.text' | translate:{ dso: dso.name } }}

    +

    {{ 'collection.delete.text' | translate:{ dso: dsoNameService.getName(dso) } }}

    + + diff --git a/src/app/collection-page/edit-collection-page/collection-access-control/collection-access-control.component.scss b/src/app/collection-page/edit-collection-page/collection-access-control/collection-access-control.component.scss new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/app/collection-page/edit-collection-page/collection-access-control/collection-access-control.component.spec.ts b/src/app/collection-page/edit-collection-page/collection-access-control/collection-access-control.component.spec.ts new file mode 100644 index 00000000000..04da8bbcd92 --- /dev/null +++ b/src/app/collection-page/edit-collection-page/collection-access-control/collection-access-control.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CollectionAccessControlComponent } from './collection-access-control.component'; + +xdescribe('CollectionAccessControlComponent', () => { + let component: CollectionAccessControlComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ CollectionAccessControlComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(CollectionAccessControlComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/collection-page/edit-collection-page/collection-access-control/collection-access-control.component.ts b/src/app/collection-page/edit-collection-page/collection-access-control/collection-access-control.component.ts new file mode 100644 index 00000000000..4192fe5a9a3 --- /dev/null +++ b/src/app/collection-page/edit-collection-page/collection-access-control/collection-access-control.component.ts @@ -0,0 +1,24 @@ +import { Component, OnInit } from '@angular/core'; +import { Observable } from 'rxjs'; +import { RemoteData } from '../../../core/data/remote-data'; +import { Community } from '../../../core/shared/community.model'; +import { ActivatedRoute } from '@angular/router'; +import { map } from 'rxjs/operators'; +import { getFirstSucceededRemoteData } from '../../../core/shared/operators'; + +@Component({ + selector: 'ds-collection-access-control', + templateUrl: './collection-access-control.component.html', + styleUrls: ['./collection-access-control.component.scss'], +}) +export class CollectionAccessControlComponent implements OnInit { + itemRD$: Observable>; + + constructor(private route: ActivatedRoute) {} + + ngOnInit(): void { + this.itemRD$ = this.route.parent.parent.data.pipe( + map((data) => data.dso) + ).pipe(getFirstSucceededRemoteData()) as Observable>; + } +} diff --git a/src/app/collection-page/edit-collection-page/collection-metadata/collection-metadata.component.spec.ts b/src/app/collection-page/edit-collection-page/collection-metadata/collection-metadata.component.spec.ts index 79e7a465e12..7cc54bd994c 100644 --- a/src/app/collection-page/edit-collection-page/collection-metadata/collection-metadata.component.spec.ts +++ b/src/app/collection-page/edit-collection-page/collection-metadata/collection-metadata.component.spec.ts @@ -4,7 +4,7 @@ import { SharedModule } from '../../../shared/shared.module'; import { CommonModule } from '@angular/common'; import { RouterTestingModule } from '@angular/router/testing'; import { CollectionDataService } from '../../../core/data/collection-data.service'; -import { ActivatedRoute, Router } from '@angular/router'; +import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; import { of as observableOf } from 'rxjs'; import { NO_ERRORS_SCHEMA } from '@angular/core'; import { CollectionMetadataComponent } from './collection-metadata.component'; @@ -52,6 +52,11 @@ describe('CollectionMetadataComponent', () => { setStaleByHrefSubstring: {} }); + const routerMock = { + events: observableOf(new NavigationEnd(1, 'url', 'url')), + navigate: jasmine.createSpy('navigate'), + }; + beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule], @@ -62,6 +67,7 @@ describe('CollectionMetadataComponent', () => { { provide: ActivatedRoute, useValue: { parent: { data: observableOf({ dso: createSuccessfulRemoteDataObject(collection) }) } } }, { provide: NotificationsService, useValue: notificationsService }, { provide: RequestService, useValue: requestService }, + { provide: Router, useValue: routerMock} ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); @@ -70,8 +76,11 @@ describe('CollectionMetadataComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(CollectionMetadataComponent); comp = fixture.componentInstance; - router = (comp as any).router; itemTemplateService = (comp as any).itemTemplateService; + spyOn(comp, 'ngOnInit'); + spyOn(comp, 'initTemplateItem'); + + routerMock.events = observableOf(new NavigationEnd(1, 'url', 'url')); fixture.detectChanges(); }); @@ -83,9 +92,8 @@ describe('CollectionMetadataComponent', () => { describe('addItemTemplate', () => { it('should navigate to the collection\'s itemtemplate page', () => { - spyOn(router, 'navigate'); comp.addItemTemplate(); - expect(router.navigate).toHaveBeenCalledWith([getCollectionItemTemplateRoute(collection.uuid)]); + expect(routerMock.navigate).toHaveBeenCalledWith([getCollectionItemTemplateRoute(collection.uuid)]); }); }); diff --git a/src/app/collection-page/edit-collection-page/collection-metadata/collection-metadata.component.ts b/src/app/collection-page/edit-collection-page/collection-metadata/collection-metadata.component.ts index 8e534a0829f..634363527f7 100644 --- a/src/app/collection-page/edit-collection-page/collection-metadata/collection-metadata.component.ts +++ b/src/app/collection-page/edit-collection-page/collection-metadata/collection-metadata.component.ts @@ -1,8 +1,8 @@ -import { Component } from '@angular/core'; +import { ChangeDetectorRef, Component, OnInit } from '@angular/core'; import { ComcolMetadataComponent } from '../../../shared/comcol/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component'; import { Collection } from '../../../core/shared/collection.model'; import { CollectionDataService } from '../../../core/data/collection-data.service'; -import { ActivatedRoute, Router } from '@angular/router'; +import { ActivatedRoute, NavigationEnd, Router, Scroll } from '@angular/router'; import { ItemTemplateDataService } from '../../../core/data/item-template-data.service'; import { combineLatest as combineLatestObservable, Observable } from 'rxjs'; import { RemoteData } from '../../../core/data/remote-data'; @@ -23,7 +23,7 @@ import { hasValue } from '../../../shared/empty.util'; selector: 'ds-collection-metadata', templateUrl: './collection-metadata.component.html', }) -export class CollectionMetadataComponent extends ComcolMetadataComponent { +export class CollectionMetadataComponent extends ComcolMetadataComponent implements OnInit { protected frontendURL = '/collections/'; protected type = Collection.type; @@ -40,13 +40,27 @@ export class CollectionMetadataComponent extends ComcolMetadataComponent { + if ( + event instanceof NavigationEnd || + (event instanceof Scroll && event.routerEvent instanceof NavigationEnd) + ) { + super.ngOnInit(); + this.initTemplateItem(); + this.chd.detectChanges(); + } + }); } /** diff --git a/src/app/collection-page/edit-collection-page/collection-roles/collection-roles.component.spec.ts b/src/app/collection-page/edit-collection-page/collection-roles/collection-roles.component.spec.ts index 5a8ca5b7abc..c375a23ddf9 100644 --- a/src/app/collection-page/edit-collection-page/collection-roles/collection-roles.component.spec.ts +++ b/src/app/collection-page/edit-collection-page/collection-roles/collection-roles.component.spec.ts @@ -15,6 +15,8 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { ComcolModule } from '../../../shared/comcol/comcol.module'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; +import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; +import { DSONameServiceMock } from '../../../shared/mocks/dso-name.service.mock'; describe('CollectionRolesComponent', () => { @@ -79,6 +81,7 @@ describe('CollectionRolesComponent', () => { ], providers: [ { provide: ActivatedRoute, useValue: route }, + { provide: DSONameService, useValue: new DSONameServiceMock() }, { provide: RequestService, useValue: requestService }, { provide: GroupDataService, useValue: groupDataService }, { provide: NotificationsService, useClass: NotificationsServiceStub } diff --git a/src/app/collection-page/edit-collection-page/collection-source/collection-source.component.spec.ts b/src/app/collection-page/edit-collection-page/collection-source/collection-source.component.spec.ts index 1b3d603d31a..36a91d8c1ae 100644 --- a/src/app/collection-page/edit-collection-page/collection-source/collection-source.component.spec.ts +++ b/src/app/collection-page/edit-collection-page/collection-source/collection-source.component.spec.ts @@ -12,7 +12,7 @@ import { NotificationType } from '../../../shared/notifications/models/notificat import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { DynamicFormControlModel, DynamicFormService } from '@ng-dynamic-forms/core'; import { hasValue } from '../../../shared/empty.util'; -import { FormControl, FormGroup } from '@angular/forms'; +import { UntypedFormControl, UntypedFormGroup } from '@angular/forms'; import { RouterStub } from '../../../shared/testing/router.stub'; import { By } from '@angular/platform-browser'; import { Collection } from '../../../core/shared/collection.model'; @@ -98,9 +98,9 @@ describe('CollectionSourceComponent', () => { const controls = {}; if (hasValue(fModel)) { fModel.forEach((controlModel) => { - controls[controlModel.id] = new FormControl((controlModel as any).value); + controls[controlModel.id] = new UntypedFormControl((controlModel as any).value); }); - return new FormGroup(controls); + return new UntypedFormGroup(controls); } return undefined; } diff --git a/src/app/collection-page/edit-collection-page/collection-source/collection-source.component.ts b/src/app/collection-page/edit-collection-page/collection-source/collection-source.component.ts index 2677431f08f..4b6e43e6753 100644 --- a/src/app/collection-page/edit-collection-page/collection-source/collection-source.component.ts +++ b/src/app/collection-page/edit-collection-page/collection-source/collection-source.component.ts @@ -15,7 +15,7 @@ import { Location } from '@angular/common'; import { TranslateService } from '@ngx-translate/core'; import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; -import { FormGroup } from '@angular/forms'; +import { UntypedFormGroup } from '@angular/forms'; import { hasNoValue, hasValue, isNotEmpty } from '../../../shared/empty.util'; import { ContentSource, ContentSourceHarvestType } from '../../../core/shared/content-source.model'; import { Observable, Subscription, throwError } from 'rxjs'; @@ -349,7 +349,7 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem /** * The form group of this form */ - formGroup: FormGroup; + formGroup: UntypedFormGroup; /** * Subscription to update the current form diff --git a/src/app/collection-page/edit-collection-page/edit-collection-page.module.ts b/src/app/collection-page/edit-collection-page/edit-collection-page.module.ts index 18f7feb6998..8d0cb179f1f 100644 --- a/src/app/collection-page/edit-collection-page/edit-collection-page.module.ts +++ b/src/app/collection-page/edit-collection-page/edit-collection-page.module.ts @@ -9,10 +9,14 @@ import { CollectionCurateComponent } from './collection-curate/collection-curate import { CollectionSourceComponent } from './collection-source/collection-source.component'; import { CollectionAuthorizationsComponent } from './collection-authorizations/collection-authorizations.component'; import { CollectionFormModule } from '../collection-form/collection-form.module'; -import { CollectionSourceControlsComponent } from './collection-source/collection-source-controls/collection-source-controls.component'; +import { + CollectionSourceControlsComponent +} from './collection-source/collection-source-controls/collection-source-controls.component'; import { ResourcePoliciesModule } from '../../shared/resource-policies/resource-policies.module'; import { FormModule } from '../../shared/form/form.module'; import { ComcolModule } from '../../shared/comcol/comcol.module'; +import { CollectionAccessControlComponent } from './collection-access-control/collection-access-control.component'; +import { AccessControlFormModule } from '../../shared/access-control-form-container/access-control-form.module'; /** * Module that contains all components related to the Edit Collection page administrator functionality @@ -26,6 +30,7 @@ import { ComcolModule } from '../../shared/comcol/comcol.module'; ResourcePoliciesModule, FormModule, ComcolModule, + AccessControlFormModule, ], declarations: [ EditCollectionPageComponent, @@ -33,7 +38,7 @@ import { ComcolModule } from '../../shared/comcol/comcol.module'; CollectionRolesComponent, CollectionCurateComponent, CollectionSourceComponent, - + CollectionAccessControlComponent, CollectionSourceControlsComponent, CollectionAuthorizationsComponent ] diff --git a/src/app/collection-page/edit-collection-page/edit-collection-page.routing.module.ts b/src/app/collection-page/edit-collection-page/edit-collection-page.routing.module.ts index 92fc6efeff3..c4481985c0a 100644 --- a/src/app/collection-page/edit-collection-page/edit-collection-page.routing.module.ts +++ b/src/app/collection-page/edit-collection-page/edit-collection-page.routing.module.ts @@ -13,6 +13,7 @@ import { ResourcePolicyCreateComponent } from '../../shared/resource-policies/cr import { ResourcePolicyResolver } from '../../shared/resource-policies/resolvers/resource-policy.resolver'; import { ResourcePolicyEditComponent } from '../../shared/resource-policies/edit/resource-policy-edit.component'; import { CollectionAdministratorGuard } from '../../core/data/feature-authorization/feature-authorization-guard/collection-administrator.guard'; +import { CollectionAccessControlComponent } from './collection-access-control/collection-access-control.component'; /** * Routing module that handles the routing for the Edit Collection page administrator functionality @@ -58,6 +59,11 @@ import { CollectionAdministratorGuard } from '../../core/data/feature-authorizat component: CollectionCurateComponent, data: { title: 'collection.edit.tabs.curate.title', showBreadcrumbs: true } }, + { + path: 'access-control', + component: CollectionAccessControlComponent, + data: { title: 'collection.edit.tabs.access-control.title', showBreadcrumbs: true } + }, /* { path: 'authorizations', component: CollectionAuthorizationsComponent, diff --git a/src/app/collection-page/edit-item-template-page/edit-item-template-page.component.html b/src/app/collection-page/edit-item-template-page/edit-item-template-page.component.html index 4d630659e8c..20afd701ffc 100644 --- a/src/app/collection-page/edit-item-template-page/edit-item-template-page.component.html +++ b/src/app/collection-page/edit-item-template-page/edit-item-template-page.component.html @@ -2,7 +2,7 @@
    -

    {{ 'collection.edit.template.head' | translate:{ collection: collection?.name } }}

    +

    {{ 'collection.edit.template.head' | translate:{ collection: dsoNameService.getName(collection) } }}

    diff --git a/src/app/collection-page/edit-item-template-page/edit-item-template-page.component.ts b/src/app/collection-page/edit-item-template-page/edit-item-template-page.component.ts index 5d29eb7f73b..238ec5e37a2 100644 --- a/src/app/collection-page/edit-item-template-page/edit-item-template-page.component.ts +++ b/src/app/collection-page/edit-item-template-page/edit-item-template-page.component.ts @@ -8,7 +8,8 @@ import { ItemTemplateDataService } from '../../core/data/item-template-data.serv import { getCollectionEditRoute } from '../collection-page-routing-paths'; import { Item } from '../../core/shared/item.model'; import { getFirstSucceededRemoteDataPayload } from '../../core/shared/operators'; -import { AlertType } from '../../shared/alert/aletr-type'; +import { AlertType } from '../../shared/alert/alert-type'; +import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; @Component({ selector: 'ds-edit-item-template-page', @@ -35,8 +36,11 @@ export class EditItemTemplatePageComponent implements OnInit { */ AlertTypeEnum = AlertType; - constructor(protected route: ActivatedRoute, - public itemTemplateService: ItemTemplateDataService) { + constructor( + protected route: ActivatedRoute, + public itemTemplateService: ItemTemplateDataService, + public dsoNameService: DSONameService, + ) { } ngOnInit(): void { diff --git a/src/app/collection-page/edit-item-template-page/item-template-page.resolver.spec.ts b/src/app/collection-page/edit-item-template-page/item-template-page.resolver.spec.ts index 2712a194c06..95f0d888e47 100644 --- a/src/app/collection-page/edit-item-template-page/item-template-page.resolver.spec.ts +++ b/src/app/collection-page/edit-item-template-page/item-template-page.resolver.spec.ts @@ -2,18 +2,22 @@ import { first } from 'rxjs/operators'; import { ItemTemplatePageResolver } from './item-template-page.resolver'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; +import { DSONameServiceMock } from '../../shared/mocks/dso-name.service.mock'; describe('ItemTemplatePageResolver', () => { describe('resolve', () => { let resolver: ItemTemplatePageResolver; let itemTemplateService: any; + let dsoNameService: DSONameServiceMock; const uuid = '1234-65487-12354-1235'; beforeEach(() => { itemTemplateService = { findByCollectionID: (id: string) => createSuccessfulRemoteDataObject$({ id }) }; - resolver = new ItemTemplatePageResolver(itemTemplateService); + dsoNameService = new DSONameServiceMock(); + resolver = new ItemTemplatePageResolver(dsoNameService as DSONameService, itemTemplateService); }); it('should resolve an item template with the correct id', (done) => { diff --git a/src/app/collection-page/edit-item-template-page/item-template-page.resolver.ts b/src/app/collection-page/edit-item-template-page/item-template-page.resolver.ts index 719a04196f3..586617c44c1 100644 --- a/src/app/collection-page/edit-item-template-page/item-template-page.resolver.ts +++ b/src/app/collection-page/edit-item-template-page/item-template-page.resolver.ts @@ -6,13 +6,17 @@ import { ItemTemplateDataService } from '../../core/data/item-template-data.serv import { Observable } from 'rxjs'; import { followLink } from '../../shared/utils/follow-link-config.model'; import { getFirstCompletedRemoteData } from '../../core/shared/operators'; +import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; /** * This class represents a resolver that requests a specific collection's item template before the route is activated */ @Injectable() export class ItemTemplatePageResolver implements Resolve> { - constructor(private itemTemplateService: ItemTemplateDataService) { + constructor( + public dsoNameService: DSONameService, + private itemTemplateService: ItemTemplateDataService, + ) { } /** diff --git a/src/app/community-list-page/community-list-page.component.html b/src/app/community-list-page/community-list-page.component.html index 9759f4405da..4392fb87d03 100644 --- a/src/app/community-list-page/community-list-page.component.html +++ b/src/app/community-list-page/community-list-page.component.html @@ -1,4 +1,4 @@
    -

    {{ 'communityList.title' | translate }}

    +

    {{ 'communityList.title' | translate }}

    diff --git a/src/app/community-list-page/community-list-service.ts b/src/app/community-list-page/community-list-service.ts index 99e9dbeb0de..bbf1c7cdb5d 100644 --- a/src/app/community-list-page/community-list-service.ts +++ b/src/app/community-list-page/community-list-service.ts @@ -24,8 +24,9 @@ import { FlatNode } from './flat-node.model'; import { ShowMoreFlatNode } from './show-more-flat-node.model'; import { FindListOptions } from '../core/data/find-list-options.model'; import { AppConfig, APP_CONFIG } from 'src/config/app-config.interface'; +import { v4 as uuidv4 } from 'uuid'; -// Helper method to combine an flatten an array of observables of flatNode arrays +// Helper method to combine and flatten an array of observables of flatNode arrays export const combineAndFlatten = (obsList: Observable[]): Observable => observableCombineLatest([...obsList]).pipe( map((matrix: any[][]) => [].concat(...matrix)), @@ -186,7 +187,7 @@ export class CommunityListService { return this.transformCommunity(community, level, parent, expandedNodes); }); if (currentPage < listOfPaginatedCommunities.totalPages && currentPage === listOfPaginatedCommunities.currentPage) { - obsList = [...obsList, observableOf([showMoreFlatNode('community', level, parent)])]; + obsList = [...obsList, observableOf([showMoreFlatNode(`community-${uuidv4()}`, level, parent)])]; } return combineAndFlatten(obsList); @@ -199,7 +200,7 @@ export class CommunityListService { * Transforms a community in a list of FlatNodes containing firstly a flatnode of the community itself, * followed by flatNodes of its possible subcommunities and collection * It gets called recursively for each subcommunity to add its subcommunities and collections to the list - * Number of subcommunities and collections added, is dependant on the current page the parent is at for respectively subcommunities and collections. + * Number of subcommunities and collections added, is dependent on the current page the parent is at for respectively subcommunities and collections. * @param community Community being transformed * @param level Depth of the community in the list, subcommunities and collections go one level deeper * @param parent Flatnode of the parent community @@ -257,7 +258,7 @@ export class CommunityListService { let nodes = rd.payload.page .map((collection: Collection) => toFlatNode(collection, observableOf(false), level + 1, false, communityFlatNode)); if (currentCollectionPage < rd.payload.totalPages && currentCollectionPage === rd.payload.currentPage) { - nodes = [...nodes, showMoreFlatNode('collection', level + 1, communityFlatNode)]; + nodes = [...nodes, showMoreFlatNode(`collection-${uuidv4()}`, level + 1, communityFlatNode)]; } return nodes; } else { @@ -275,7 +276,7 @@ export class CommunityListService { /** * Checks if a community has subcommunities or collections by querying the respective services with a pageSize = 0 - * Returns an observable that combines the result.payload.totalElements fo the observables that the + * Returns an observable that combines the result.payload.totalElements of the observables that the * respective services return when queried * @param community Community being checked whether it is expandable (if it has subcommunities or collections) */ diff --git a/src/app/community-list-page/community-list/community-list.component.html b/src/app/community-list-page/community-list/community-list.component.html index 821cb58473b..de67607bb4b 100644 --- a/src/app/community-list-page/community-list/community-list.component.html +++ b/src/app/community-list-page/community-list/community-list.component.html @@ -1,5 +1,5 @@ - + @@ -8,10 +8,10 @@
    @@ -25,18 +25,23 @@ class="example-tree-node expandable-node">
    -
    - - {{node.name}} - -
    +
    + + + {{ dsoNameService.getName(node.payload) }} + +   + {{node.payload.archivedItemsCount}} + +
    @@ -65,12 +70,11 @@
    class="example-tree-node childless-node"> diff --git a/src/app/community-list-page/community-list/community-list.component.spec.ts b/src/app/community-list-page/community-list/community-list.component.spec.ts index 575edf14e87..fb47f4994d2 100644 --- a/src/app/community-list-page/community-list/community-list.component.spec.ts +++ b/src/app/community-list-page/community-list/community-list.component.spec.ts @@ -16,6 +16,8 @@ import { of as observableOf } from 'rxjs'; import { By } from '@angular/platform-browser'; import { isEmpty, isNotEmpty } from '../../shared/empty.util'; import { FlatNode } from '../flat-node.model'; +import { RouterLinkWithHref } from '@angular/router'; +import { v4 as uuidv4 } from 'uuid'; describe('CommunityListComponent', () => { let component: CommunityListComponent; @@ -137,7 +139,7 @@ describe('CommunityListComponent', () => { } if (expandedNodes === null || isEmpty(expandedNodes)) { if (showMoreTopComNode) { - return observableOf([...mockTopFlatnodesUnexpanded.slice(0, endPageIndex), showMoreFlatNode('community', 0, null)]); + return observableOf([...mockTopFlatnodesUnexpanded.slice(0, endPageIndex), showMoreFlatNode(`community-${uuidv4()}`, 0, null)]); } else { return observableOf(mockTopFlatnodesUnexpanded.slice(0, endPageIndex)); } @@ -164,21 +166,21 @@ describe('CommunityListComponent', () => { const endSubComIndex = this.pageSize * expandedParent.currentCommunityPage; flatnodes = [...flatnodes, ...subComFlatnodes.slice(0, endSubComIndex)]; if (subComFlatnodes.length > endSubComIndex) { - flatnodes = [...flatnodes, showMoreFlatNode('community', topNode.level + 1, expandedParent)]; + flatnodes = [...flatnodes, showMoreFlatNode(`community-${uuidv4()}`, topNode.level + 1, expandedParent)]; } } if (isNotEmpty(collFlatnodes)) { const endColIndex = this.pageSize * expandedParent.currentCollectionPage; flatnodes = [...flatnodes, ...collFlatnodes.slice(0, endColIndex)]; if (collFlatnodes.length > endColIndex) { - flatnodes = [...flatnodes, showMoreFlatNode('collection', topNode.level + 1, expandedParent)]; + flatnodes = [...flatnodes, showMoreFlatNode(`collection-${uuidv4()}`, topNode.level + 1, expandedParent)]; } } } } }); if (showMoreTopComNode) { - flatnodes = [...flatnodes, showMoreFlatNode('community', 0, null)]; + flatnodes = [...flatnodes, showMoreFlatNode(`community-${uuidv4()}`, 0, null)]; } return observableOf(flatnodes); } @@ -193,7 +195,8 @@ describe('CommunityListComponent', () => { }, }), CdkTreeModule, - RouterTestingModule], + RouterTestingModule, + RouterLinkWithHref], declarations: [CommunityListComponent], providers: [CommunityListComponent, { provide: CommunityListService, useValue: communityListServiceStub },], @@ -230,9 +233,14 @@ describe('CommunityListComponent', () => { expect(showMoreEl).toBeTruthy(); }); + it('should not render the show more button as an empty link', () => { + const debugElements = fixture.debugElement.queryAll(By.directive(RouterLinkWithHref)); + expect(debugElements).toBeTruthy(); + }); + describe('when show more of top communities is clicked', () => { beforeEach(fakeAsync(() => { - const showMoreLink = fixture.debugElement.query(By.css('.show-more-node a')); + const showMoreLink = fixture.debugElement.query(By.css('.show-more-node .btn-outline-primary')); showMoreLink.triggerEventHandler('click', { preventDefault: () => {/**/ } @@ -240,6 +248,7 @@ describe('CommunityListComponent', () => { tick(); fixture.detectChanges(); })); + it('tree contains maximum of currentPage (2) * (2) elementsPerPage of first top communities, or less if there are less communities (3)', () => { const expandableNodesFound = fixture.debugElement.queryAll(By.css('.expandable-node a')); const childlessNodesFound = fixture.debugElement.queryAll(By.css('.childless-node a')); diff --git a/src/app/community-list-page/community-list/community-list.component.ts b/src/app/community-list-page/community-list/community-list.component.ts index 556387da251..6b5c6578e1f 100644 --- a/src/app/community-list-page/community-list/community-list.component.ts +++ b/src/app/community-list-page/community-list/community-list.component.ts @@ -7,6 +7,7 @@ import { FlatTreeControl } from '@angular/cdk/tree'; import { isEmpty } from '../../shared/empty.util'; import { FlatNode } from '../flat-node.model'; import { FindListOptions } from '../../core/data/find-list-options.model'; +import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; /** * A tree-structured list of nodes representing the communities, their subCommunities and collections. @@ -27,12 +28,14 @@ export class CommunityListComponent implements OnInit, OnDestroy { treeControl = new FlatTreeControl( (node: FlatNode) => node.level, (node: FlatNode) => true ); - dataSource: CommunityListDatasource; - paginationConfig: FindListOptions; + trackBy = (index, node: FlatNode) => node.id; - constructor(private communityListService: CommunityListService) { + constructor( + protected communityListService: CommunityListService, + public dsoNameService: DSONameService, + ) { this.paginationConfig = new FindListOptions(); this.paginationConfig.elementsPerPage = 2; this.paginationConfig.currentPage = 1; @@ -54,24 +57,34 @@ export class CommunityListComponent implements OnInit, OnDestroy { this.communityListService.saveCommunityListStateToStore(this.expandedNodes, this.loadingNode); } - // whether or not this node has children (subcommunities or collections) + /** + * Whether this node has children (subcommunities or collections) + * @param _ + * @param node + */ hasChild(_: number, node: FlatNode) { return node.isExpandable$; } - // whether or not it is a show more node (contains no data, but is indication that there are more topcoms, subcoms or collections + /** + * Whether this is a show more node that contains no data, but indicates that there is + * one or more community or collection. + * @param _ + * @param node + */ isShowMore(_: number, node: FlatNode) { return node.isShowMoreNode; } /** - * Toggles the expanded variable of a node, adds it to the expanded nodes list and reloads the tree so this node is expanded + * Toggles the expanded variable of a node, adds it to the expanded nodes list and reloads the tree + * so this node is expanded * @param node Node we want to expand */ toggleExpanded(node: FlatNode) { this.loadingNode = node; if (node.isExpanded) { - this.expandedNodes = this.expandedNodes.filter((node2) => node2.name !== node.name); + this.expandedNodes = this.expandedNodes.filter((node2) => node2.id !== node.id); node.isExpanded = false; } else { this.expandedNodes.push(node); @@ -88,26 +101,28 @@ export class CommunityListComponent implements OnInit, OnDestroy { /** * Makes sure the next page of a node is added to the tree (top community, sub community of collection) - * > Finds its parent (if not top community) and increases its corresponding collection/subcommunity currentPage - * > Reloads tree with new page added to corresponding top community lis, sub community list or collection list - * @param node The show more node indicating whether it's an increase in top communities, sub communities or collections + * > Finds its parent (if not top community) and increases its corresponding collection/subcommunity + * currentPage + * > Reloads tree with new page added to corresponding top community lis, sub community list or + * collection list + * @param node The show more node indicating whether it's an increase in top communities, sub communities + * or collections */ getNextPage(node: FlatNode): void { this.loadingNode = node; if (node.parent != null) { - if (node.id === 'collection') { + if (node.id.startsWith('collection')) { const parentNodeInExpandedNodes = this.expandedNodes.find((node2: FlatNode) => node.parent.id === node2.id); parentNodeInExpandedNodes.currentCollectionPage++; } - if (node.id === 'community') { + if (node.id.startsWith('community')) { const parentNodeInExpandedNodes = this.expandedNodes.find((node2: FlatNode) => node.parent.id === node2.id); parentNodeInExpandedNodes.currentCommunityPage++; } - this.dataSource.loadCommunities(this.paginationConfig, this.expandedNodes); } else { this.paginationConfig.currentPage++; - this.dataSource.loadCommunities(this.paginationConfig, this.expandedNodes); } + this.dataSource.loadCommunities(this.paginationConfig, this.expandedNodes); } } diff --git a/src/app/community-list-page/show-more-flat-node.model.ts b/src/app/community-list-page/show-more-flat-node.model.ts index 801c9e7388a..c7b7162d213 100644 --- a/src/app/community-list-page/show-more-flat-node.model.ts +++ b/src/app/community-list-page/show-more-flat-node.model.ts @@ -1,6 +1,6 @@ /** * The show more links in the community tree are also represented by a flatNode so we know where in - * the tree it should be rendered an who its parent is (needed for the action resulting in clicking this link) + * the tree it should be rendered and who its parent is (needed for the action resulting in clicking this link) */ export class ShowMoreFlatNode { } diff --git a/src/app/community-page/community-form/community-form.component.ts b/src/app/community-page/community-form/community-form.component.ts index c6dd1147c34..fa4809738d9 100644 --- a/src/app/community-page/community-form/community-form.component.ts +++ b/src/app/community-page/community-form/community-form.component.ts @@ -1,4 +1,4 @@ -import { Component, Input } from '@angular/core'; +import { Component, Input, OnChanges, SimpleChange, SimpleChanges } from '@angular/core'; import { DynamicFormControlModel, DynamicFormService, @@ -23,7 +23,7 @@ import { environment } from '../../../environments/environment'; styleUrls: ['../../shared/comcol/comcol-forms/comcol-form/comcol-form.component.scss'], templateUrl: '../../shared/comcol/comcol-forms/comcol-form/comcol-form.component.html' }) -export class CommunityFormComponent extends ComColFormComponent { +export class CommunityFormComponent extends ComColFormComponent implements OnChanges { /** * @type {Community} A new community when a community is being created, an existing Input community when a community is being edited */ @@ -81,4 +81,11 @@ export class CommunityFormComponent extends ComColFormComponent { protected objectCache: ObjectCacheService) { super(formService, translate, notificationsService, authService, requestService, objectCache); } + + ngOnChanges(changes: SimpleChanges) { + const dsoChange: SimpleChange = changes.dso; + if (this.dso && dsoChange && !dsoChange.isFirstChange()) { + super.ngOnInit(); + } + } } diff --git a/src/app/community-page/community-page.component.html b/src/app/community-page/community-page.component.html index c56e304b21b..e0b8bfd4774 100644 --- a/src/app/community-page/community-page.component.html +++ b/src/app/community-page/community-page.component.html @@ -5,7 +5,7 @@
    - + diff --git a/src/app/community-page/community-page.component.spec.ts b/src/app/community-page/community-page.component.spec.ts new file mode 100644 index 00000000000..d5f457fe24f --- /dev/null +++ b/src/app/community-page/community-page.component.spec.ts @@ -0,0 +1,123 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ActivatedRoute, Router } from '@angular/router'; +import { CommunityPageComponent } from './community-page.component'; +import { AuthService } from '../core/auth/auth.service'; +import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service'; +import { DSONameService } from '../core/breadcrumbs/dso-name.service'; +import { ActivatedRouteStub } from '../shared/testing/active-router.stub'; +import { RouterStub } from '../shared/testing/router.stub'; +import { RouterTestingModule } from '@angular/router/testing'; +import { FormsModule } from '@angular/forms'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { TranslateModule } from '@ngx-translate/core'; +import { VarDirective } from '../shared/utils/var.directive'; +import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; +import { Community } from '../core/shared/community.model'; +import { of } from 'rxjs'; +import { CommunityDataService } from '../core/data/community-data.service'; +import { MetadataService } from '../core/metadata/metadata.service'; +import { Bitstream } from '../core/shared/bitstream.model'; +import { By } from '@angular/platform-browser'; + +describe('CommunityPageComponent', () => { + let component: CommunityPageComponent; + let fixture: ComponentFixture; + + let authServiceSpy: jasmine.SpyObj; + let authorizationDataServiceSpy: jasmine.SpyObj; + let dsoNameServiceSpy: jasmine.SpyObj; + let aroute = new ActivatedRouteStub(); + let router = new RouterStub(); + + const community = Object.assign(new Community(), { + id: 'test-community', + uuid: 'test-community', + metadata: [ + { + key: 'dc.title', + language: 'en_US', + value: 'test community' + } + ], + logo: createSuccessfulRemoteDataObject$(new Bitstream()), + }); + + beforeEach(async () => { + authServiceSpy = jasmine.createSpyObj('AuthService', ['isAuthenticated']); + authorizationDataServiceSpy = jasmine.createSpyObj('AuthorizationDataService', ['isAuthorized']); + dsoNameServiceSpy = jasmine.createSpyObj('DSONameService', ['getName']); + await TestBed.configureTestingModule({ + imports: [RouterTestingModule, FormsModule, TranslateModule.forRoot(), BrowserAnimationsModule], + declarations: [CommunityPageComponent, VarDirective], + providers: [ + { provide: ActivatedRoute, useValue: aroute }, + { provide: Router, useValue: router }, + { provide: AuthService, useValue: authServiceSpy }, + { provide: AuthorizationDataService, useValue: authorizationDataServiceSpy }, + { provide: DSONameService, useValue: dsoNameServiceSpy }, + { provide: CommunityDataService, useValue: {} }, + { provide: MetadataService, useValue: {} } + ] + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(CommunityPageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should initialize the component', () => { + const routeData = { + data: of({ dso: createSuccessfulRemoteDataObject$(community) }), + }; + authorizationDataServiceSpy.isAuthorized.and.returnValue(of(true)); + + Object.defineProperty(TestBed.inject(ActivatedRoute), 'data', { + get: () => of(routeData), + }); + + component.ngOnInit(); + expect(component.communityRD$).toBeDefined(); + expect(component.logoRD$).toBeDefined(); + expect(component.communityPageRoute$).toBeDefined(); + expect(component.isCommunityAdmin$).toBeDefined(); + }); + + it('should display community logo if available', () => { + component.communityRD$ = createSuccessfulRemoteDataObject$(community); + fixture.detectChanges(); + + fixture.whenStable().then(() => { + const logoElement = fixture.debugElement.query(By.css('ds-comcol-page-logo')).nativeElement; + expect(logoElement).toBeTruthy(); + }); + }); + + + it('should not display community logo if not available', () => { + component.communityRD$ = createSuccessfulRemoteDataObject$(Object.assign(new Community(), { + name: 'Test', + logo: createSuccessfulRemoteDataObject$(null), + })); + fixture.detectChanges(); + + fixture.whenStable().then(() => { + const logoElement = fixture.debugElement.query(By.css('ds-comcol-page-logo')); + expect(logoElement).toBeNull(); + }); + }); + + it('should display collection name', () => { + component.communityRD$ = createSuccessfulRemoteDataObject$(Object.assign(community)); + fixture.detectChanges(); + fixture.whenStable().then(() => { + const collectionNameElement = fixture.debugElement.query(By.css('ds-comcol-page-header')).nativeElement; + expect(collectionNameElement.textContent.trim()).toBe('Test Collection'); + }); + }); +}); diff --git a/src/app/community-page/community-page.component.ts b/src/app/community-page/community-page.component.ts index b1a0cfc9466..a5bbff3cee7 100644 --- a/src/app/community-page/community-page.component.ts +++ b/src/app/community-page/community-page.component.ts @@ -19,6 +19,7 @@ import { AuthorizationDataService } from '../core/data/feature-authorization/aut import { FeatureID } from '../core/data/feature-authorization/feature-id'; import { getCommunityPageRoute } from './community-page-routing-paths'; import { redirectOn4xx } from '../core/shared/authorized.operators'; +import { DSONameService } from '../core/breadcrumbs/dso-name.service'; @Component({ selector: 'ds-community-page', @@ -57,7 +58,8 @@ export class CommunityPageComponent implements OnInit { private route: ActivatedRoute, private router: Router, private authService: AuthService, - private authorizationDataService: AuthorizationDataService + private authorizationDataService: AuthorizationDataService, + public dsoNameService: DSONameService, ) { } diff --git a/src/app/community-page/create-community-page/create-community-page.component.html b/src/app/community-page/create-community-page/create-community-page.component.html index 71a580b0aaa..57039040c2a 100644 --- a/src/app/community-page/create-community-page/create-community-page.component.html +++ b/src/app/community-page/create-community-page/create-community-page.component.html @@ -3,7 +3,7 @@
    -

    {{ 'community.create.sub-head' | translate:{ parent: parent.name } }}

    +

    {{ 'community.create.sub-head' | translate:{ parent: dsoNameService.getName(parent) } }}

    diff --git a/src/app/community-page/create-community-page/create-community-page.component.ts b/src/app/community-page/create-community-page/create-community-page.component.ts index b332fad1000..eea09083887 100644 --- a/src/app/community-page/create-community-page/create-community-page.component.ts +++ b/src/app/community-page/create-community-page/create-community-page.component.ts @@ -7,6 +7,7 @@ import { CreateComColPageComponent } from '../../shared/comcol/comcol-forms/crea import { NotificationsService } from '../../shared/notifications/notifications.service'; import { TranslateService } from '@ngx-translate/core'; import { RequestService } from '../../core/data/request.service'; +import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; /** * Component that represents the page where a user can create a new Community @@ -22,12 +23,13 @@ export class CreateCommunityPageComponent extends CreateComColPageComponent
    -

    {{ 'community.delete.text' | translate:{ dso: dso.name } }}

    +

    {{ 'community.delete.text' | translate:{ dso: dsoNameService.getName(dso) } }}

    - + + + + + + + + diff --git a/src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metadata/rendering-types/advanced-attachment/bitstream-attachment/bitstream-attachment.component.html b/src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metadata/rendering-types/advanced-attachment/bitstream-attachment/bitstream-attachment.component.html index aaa4d4d43ca..01dbf22b091 100644 --- a/src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metadata/rendering-types/advanced-attachment/bitstream-attachment/bitstream-attachment.component.html +++ b/src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metadata/rendering-types/advanced-attachment/bitstream-attachment/bitstream-attachment.component.html @@ -42,12 +42,22 @@ +

    + {{'cris-layout.advanced-attachment.label.not-present' | translate}} +

    {{getFormat(attachment) | async}}

    {{getSize(attachment) | dsFileSize}}

    + + +

    + {{'cris-layout.advanced-attachment.label.not-present' | translate}} +

    +

    ({{getChecksum(attachment).checkSumAlgorithm}}):{{ getChecksum(attachment).value }}

    +
    diff --git a/src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metadata/rendering-types/thumbnail/thumbnail.component.spec.ts b/src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metadata/rendering-types/thumbnail/thumbnail.component.spec.ts index 9ca63334666..5aab0e0dbc5 100644 --- a/src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metadata/rendering-types/thumbnail/thumbnail.component.spec.ts +++ b/src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metadata/rendering-types/thumbnail/thumbnail.component.spec.ts @@ -152,7 +152,7 @@ describe('ThumbnailComponent', () => { })); it('should show default thumbnail', () => { - expect(component.default).toBe('assets/images/person-placeholder.svg'); + expect(component.default).toBe('assets/images/file-placeholder.svg'); }); }); @@ -172,7 +172,7 @@ describe('ThumbnailComponent', () => { })); it('should show default thumbnail', () => { - expect(component.default).toBe('assets/images/person-placeholder.svg'); + expect(component.default).toBe('assets/images/file-placeholder.svg'); }); }); @@ -239,7 +239,7 @@ describe('ThumbnailComponent', () => { }); it('should show default thumbnail', () => { - expect(component.default).toBe('assets/images/person-placeholder.svg'); + expect(component.default).toBe('assets/images/file-placeholder.svg'); }); }); @@ -254,7 +254,7 @@ describe('ThumbnailComponent', () => { }); it('should not show bitstream content image src but the default image', () => { - expect(component.default).toBe('assets/images/person-placeholder.svg'); + expect(component.default).toBe('assets/images/file-placeholder.svg'); }); }); @@ -269,7 +269,7 @@ describe('ThumbnailComponent', () => { }); it('should not show thumbnail content image src but the default image', () => { - expect(component.default).toBe('assets/images/person-placeholder.svg'); + expect(component.default).toBe('assets/images/file-placeholder.svg'); }); }); @@ -284,7 +284,7 @@ describe('ThumbnailComponent', () => { }); it('should not show thumbnail content image src but the default image', () => { - expect(component.default).toBe('assets/images/person-placeholder.svg'); + expect(component.default).toBe('assets/images/file-placeholder.svg'); }); }); diff --git a/src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metadata/rendering-types/thumbnail/thumbnail.component.ts b/src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metadata/rendering-types/thumbnail/thumbnail.component.ts index a0dd0a3fe29..cc034068556 100644 --- a/src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metadata/rendering-types/thumbnail/thumbnail.component.ts +++ b/src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metadata/rendering-types/thumbnail/thumbnail.component.ts @@ -96,11 +96,13 @@ export class ThumbnailComponent extends BitstreamRenderingModelComponent impleme */ setDefaultImage(): void { const eType = this.item.firstMetadataValue('dspace.entity.type'); - this.default = 'assets/images/person-placeholder.svg'; + this.default = 'assets/images/file-placeholder.svg'; if (hasValue(eType) && eType.toUpperCase() === 'PROJECT') { this.default = 'assets/images/project-placeholder.svg'; } else if (hasValue(eType) && eType.toUpperCase() === 'ORGUNIT') { this.default = 'assets/images/orgunit-placeholder.svg'; + } else if (hasValue(eType) && eType.toUpperCase() === 'PERSON') { + this.default = 'assets/images/person-placeholder.svg'; } } } diff --git a/src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/relation/cris-layout-relation-box.component.html b/src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/relation/cris-layout-relation-box.component.html index 94b6abf8cf3..e2ab0222037 100644 --- a/src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/relation/cris-layout-relation-box.component.html +++ b/src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/relation/cris-layout-relation-box.component.html @@ -1,7 +1,10 @@ + [showScopeSelector]="false" + [showSearchResultNotice]="showSearchResultNotice$ | async" + [searchResultNotice]="searchResultNotice"> diff --git a/src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/relation/cris-layout-relation-box.component.spec.ts b/src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/relation/cris-layout-relation-box.component.spec.ts index 4ac24f6af1e..c51566fdd7d 100644 --- a/src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/relation/cris-layout-relation-box.component.spec.ts +++ b/src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/relation/cris-layout-relation-box.component.spec.ts @@ -1,4 +1,4 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, fakeAsync, TestBed, waitForAsync } from '@angular/core/testing'; import { CrisLayoutRelationBoxComponent } from './cris-layout-relation-box.component'; import { NO_ERRORS_SCHEMA } from '@angular/core'; @@ -6,9 +6,14 @@ import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { CommonModule } from '@angular/common'; import { SharedModule } from '../../../../../shared/shared.module'; import { Item } from '../../../../../core/shared/item.model'; -import { of } from 'rxjs'; +import { of as observableOf, of } from 'rxjs'; import { CrisLayoutBox } from '../../../../../core/layout/models/box.model'; import { TranslateLoaderMock } from '../../../../../shared/mocks/translate-loader.mock'; +import { MetadataValue } from '../../../../../core/shared/metadata.models'; +import { AuthService } from '../../../../../core/auth/auth.service'; +import { cold } from 'jasmine-marbles'; +import { EPersonMock, EPersonMock2 } from '../../../../../shared/testing/eperson.mock'; +import { EPerson } from '../../../../../core/eperson/models/eperson.model'; describe('CrisLayoutRelationBoxComponent', () => { let component: CrisLayoutRelationBoxComponent; @@ -25,7 +30,37 @@ describe('CrisLayoutRelationBoxComponent', () => { collapsed: false, header: 'CrisLayoutBox Header', shortname: 'test-box', - configuration: of({ configuration: 'box-configuration-id' }) + configuration: { 'discovery-configuration': 'box-configuration-id' } + }); + + const relationPublicationsBox = Object.assign(new CrisLayoutBox(), { + id: '2', + collapsed: false, + header: 'Publications', + shortname: 'publications', + configuration: { 'discovery-configuration': 'RELATION.Person.researchoutputs' } + }); + + const personItem = Object.assign(new Item(), { + id: '1234-65487-12354-1235', + bundles: of({}), + metadata: { + 'dspace.entity.type': [{ value: 'Person' }] as MetadataValue[], + 'dspace.object.owner': [{ value: 'not Owner', authority: EPersonMock2.id }] as MetadataValue[], + } + }); + + const ownerItem = Object.assign(new Item(), { + id: '1234-65487-12354-1235', + bundles: of({}), + metadata: { + 'dspace.entity.type': [{ value: 'Person' }] as MetadataValue[], + 'dspace.object.owner': [{ value: 'Owner', authority: EPersonMock.id }] as MetadataValue[], + } + }); + + const authService = jasmine.createSpyObj('authService', { + getAuthenticatedUserFromStore: observableOf(null) }); beforeEach(waitForAsync(() => { @@ -40,25 +75,96 @@ describe('CrisLayoutRelationBoxComponent', () => { CommonModule, SharedModule ], - declarations: [ CrisLayoutRelationBoxComponent ], + declarations: [CrisLayoutRelationBoxComponent], schemas: [NO_ERRORS_SCHEMA], providers: [ - { provide: 'boxProvider', useClass: testBox }, - { provide: 'itemProvider', useClass: testItem }, + { provide: 'boxProvider', useValue: testBox }, + { provide: 'itemProvider', useValue: testItem }, + { provide: AuthService, useValue: authService }, ] }) - .compileComponents(); + .compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(CrisLayoutRelationBoxComponent); component = fixture.componentInstance; - component.box = testBox; - component.item = testItem; - fixture.detectChanges(); }); - xit('should have set scope in searchFilter', () => { - expect(component.searchFilter).toContain('scope=' + testItem.id); + describe('When item is not a Person', () => { + beforeEach(() => { + component.box = testBox; + component.item = testItem; + fixture.detectChanges(); + }); + + it('should create CrisLayoutRelationBoxComponent', () => { + expect(component).toBeDefined(); + }); + + it('should have set scope in searchFilter', () => { + expect(component.searchFilter).toContain('scope=' + testItem.id); + }); + + it('info message cannot be shown', fakeAsync(() => { + expect(component.showSearchResultNotice$).toBeObservable(cold('a', { a: false })); + })); + + it('info message has no value', fakeAsync(() => { + expect(component.searchResultNotice).toBeUndefined(); + })); + }); + + describe('When item is a Person', () => { + + describe('When relation-box of researchoutputs is shown', () => { + beforeEach(() => { + component.box = relationPublicationsBox; + }); + + describe('Whenever the personItem is the researcher profile of the logged user', () => { + beforeEach(() => { + (authService.getAuthenticatedUserFromStore as jasmine.Spy).and.returnValue(observableOf({ id: EPersonMock.id } as EPerson)); + component.item = ownerItem; + fixture.detectChanges(); + }); + it('info message can be shown', fakeAsync(() => { + expect(component.showSearchResultNotice$).toBeObservable(cold('a', { a: true })); + })); + it('info message has value', fakeAsync(() => { + expect(component.searchResultNotice).not.toBeUndefined(); + })); + }); + + describe('Whenever the personItem is not the researcher profile of the logged user', () => { + beforeEach(() => { + (authService.getAuthenticatedUserFromStore as jasmine.Spy).and.returnValue(observableOf({ id: 'fake-uuid' } as EPerson)); + component.item = personItem; + fixture.detectChanges(); + }); + it('info message cannot be shown', fakeAsync(() => { + expect(component.showSearchResultNotice$).toBeObservable(cold('a', { a: false })); + })); + it('info message has no value', fakeAsync(() => { + expect(component.searchResultNotice).not.toBeUndefined(); + })); + }); + + describe('no one is logged', () => { + beforeEach(() => { + (authService.getAuthenticatedUserFromStore as jasmine.Spy).and.returnValue(observableOf(null as EPerson)); + fixture.detectChanges(); + }); + it('info message has no value', fakeAsync(() => { + // component.showSearchResultNotice$.subscribe(value => expect(value).toBeUndefined()); + expect(component.showSearchResultNotice$).toBeObservable(cold('a', { a: false })); + })); + it('info message has no value', fakeAsync(() => { + expect(component.searchResultNotice).toBeUndefined(); + })); + }); + + }); }); + }); diff --git a/src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/relation/cris-layout-relation-box.component.ts b/src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/relation/cris-layout-relation-box.component.ts index b80f8dd9df2..28a99756a50 100644 --- a/src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/relation/cris-layout-relation-box.component.ts +++ b/src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/relation/cris-layout-relation-box.component.ts @@ -1,10 +1,17 @@ -import { ChangeDetectorRef, Component, Inject, OnInit } from '@angular/core'; +import { Component, Inject, OnInit } from '@angular/core'; + +import { BehaviorSubject, Observable, shareReplay } from 'rxjs'; +import { filter, map, take } from 'rxjs/operators'; +import { TranslateService } from '@ngx-translate/core'; + import { RenderCrisLayoutBoxFor } from '../../../../decorators/cris-layout-box.decorator'; import { LayoutBox } from '../../../../enums/layout-box.enum'; import { CrisLayoutBoxModelComponent } from '../../../../models/cris-layout-box-component.model'; -import { TranslateService } from '@ngx-translate/core'; import { CrisLayoutBox, RelationBoxConfiguration } from '../../../../../core/layout/models/box.model'; import { Item } from '../../../../../core/shared/item.model'; +import { AuthService } from '../../../../../core/auth/auth.service'; +import { isNotEmpty } from '../../../../../shared/empty.util'; +import { EPerson } from '../../../../../core/eperson/models/eperson.model'; @Component({ selector: 'ds-cris-layout-search-box', @@ -26,15 +33,23 @@ export class CrisLayoutRelationBoxComponent extends CrisLayoutBoxModelComponent * flag for enable/disable search bar */ searchEnabled = false; + /** - * The width of the sidebar (bootstrap columns) + * A boolean representing if to show or not the search notice */ - // sideBarWidth = 3; + showSearchResultNotice$: BehaviorSubject = new BehaviorSubject(false); - constructor(public cd: ChangeDetectorRef, - protected translateService: TranslateService, - @Inject('boxProvider') public boxProvider: CrisLayoutBox, - @Inject('itemProvider') public itemProvider: Item) { + /** + * The search notice message + */ + searchResultNotice: string; + + constructor( + protected authService: AuthService, + protected translateService: TranslateService, + @Inject('boxProvider') public boxProvider: CrisLayoutBox, + @Inject('itemProvider') public itemProvider: Item + ) { super(translateService, boxProvider, itemProvider); } @@ -43,6 +58,31 @@ export class CrisLayoutRelationBoxComponent extends CrisLayoutBoxModelComponent this.searchFilter = `scope=${this.item.id}`; this.configuration = (this.box.configuration as RelationBoxConfiguration)['discovery-configuration']; + const isResearchOutputsConfiguration = + this.configuration?.endsWith('researchoutputs') || this.configuration?.endsWith('Publication'); + if (!this.isPersonEntity() || !isResearchOutputsConfiguration) { + this.showSearchResultNotice$.next(false); + } else { + this.isProfileOwner().pipe(take(1)).subscribe((result) => { + this.searchResultNotice = this.translateService.instant('manage.relationships.hidden-related-items-alert'); + this.showSearchResultNotice$.next(result); + }); + } + } + + protected getOwner(user: EPerson) { + return this.item.firstMetadataValue('dspace.object.owner', { authority: user.id }); } + protected isProfileOwner(): Observable { + return this.authService.getAuthenticatedUserFromStore().pipe( + filter(isNotEmpty), + map((user) => isNotEmpty(this.getOwner(user))), + shareReplay(1) + ); + } + + protected isPersonEntity(): boolean { + return isNotEmpty(this.item.firstMetadataValue('dspace.entity.type', { value: 'Person' })); + } } diff --git a/src/app/curation-form/curation-form.component.spec.ts b/src/app/curation-form/curation-form.component.spec.ts index dc70b925e83..a0bdee21f49 100644 --- a/src/app/curation-form/curation-form.component.spec.ts +++ b/src/app/curation-form/curation-form.component.spec.ts @@ -1,4 +1,4 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ComponentFixture, TestBed, waitForAsync, fakeAsync, flush } from '@angular/core/testing'; import { TranslateModule } from '@ngx-translate/core'; import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { CurationFormComponent } from './curation-form.component'; @@ -16,6 +16,7 @@ import { ConfigurationDataService } from '../core/data/configuration-data.servic import { ConfigurationProperty } from '../core/shared/configuration-property.model'; import { getProcessDetailRoute } from '../process-page/process-page-routing.paths'; import { HandleService } from '../shared/handle.service'; +import { of as observableOf } from 'rxjs'; describe('CurationFormComponent', () => { let comp: CurationFormComponent; @@ -54,7 +55,7 @@ describe('CurationFormComponent', () => { }); handleService = { - normalizeHandle: (a) => a + normalizeHandle: (a: string) => observableOf(a), } as any; notificationsService = new NotificationsServiceStub(); @@ -151,12 +152,13 @@ describe('CurationFormComponent', () => { ], []); }); - it(`should show an error notification and return when an invalid dsoHandle is provided`, () => { + it(`should show an error notification and return when an invalid dsoHandle is provided`, fakeAsync(() => { comp.dsoHandle = 'test-handle'; - spyOn(handleService, 'normalizeHandle').and.returnValue(null); + spyOn(handleService, 'normalizeHandle').and.returnValue(observableOf(null)); comp.submit(); + flush(); expect(notificationsService.error).toHaveBeenCalled(); expect(scriptDataService.invoke).not.toHaveBeenCalled(); - }); + })); }); diff --git a/src/app/curation-form/curation-form.component.ts b/src/app/curation-form/curation-form.component.ts index 4b67580e770..cc2c14f89fb 100644 --- a/src/app/curation-form/curation-form.component.ts +++ b/src/app/curation-form/curation-form.component.ts @@ -1,22 +1,22 @@ -import { Component, Input, OnInit } from '@angular/core'; +import { ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core'; import { ScriptDataService } from '../core/data/processes/script-data.service'; -import { FormControl, FormGroup } from '@angular/forms'; -import { getFirstCompletedRemoteData } from '../core/shared/operators'; -import { find, map } from 'rxjs/operators'; +import { UntypedFormControl, UntypedFormGroup } from '@angular/forms'; +import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../core/shared/operators'; +import { map } from 'rxjs/operators'; import { NotificationsService } from '../shared/notifications/notifications.service'; import { TranslateService } from '@ngx-translate/core'; import { hasValue, isEmpty, isNotEmpty } from '../shared/empty.util'; import { RemoteData } from '../core/data/remote-data'; import { Router } from '@angular/router'; -import { ProcessDataService } from '../core/data/processes/process-data.service'; import { Process } from '../process-page/processes/process.model'; import { ConfigurationDataService } from '../core/data/configuration-data.service'; import { ConfigurationProperty } from '../core/shared/configuration-property.model'; -import { Observable } from 'rxjs'; +import { Observable, Subscription } from 'rxjs'; import { getProcessDetailRoute } from '../process-page/process-page-routing.paths'; import { HandleService } from '../shared/handle.service'; export const CURATION_CFG = 'plugin.named.org.dspace.curate.CurationTask'; + /** * Component responsible for rendering the Curation Task form */ @@ -24,42 +24,48 @@ export const CURATION_CFG = 'plugin.named.org.dspace.curate.CurationTask'; selector: 'ds-curation-form', templateUrl: './curation-form.component.html' }) -export class CurationFormComponent implements OnInit { +export class CurationFormComponent implements OnDestroy, OnInit { config: Observable>; tasks: string[]; - form: FormGroup; + form: UntypedFormGroup; @Input() dsoHandle: string; + subs: Subscription[] = []; + constructor( private scriptDataService: ScriptDataService, private configurationDataService: ConfigurationDataService, - private processDataService: ProcessDataService, private notificationsService: NotificationsService, private translateService: TranslateService, private handleService: HandleService, - private router: Router + private router: Router, + private cdr: ChangeDetectorRef ) { } + ngOnDestroy(): void { + this.subs.forEach((sub: Subscription) => sub.unsubscribe()); + } + ngOnInit(): void { - this.form = new FormGroup({ - task: new FormControl(''), - handle: new FormControl('') + this.form = new UntypedFormGroup({ + task: new UntypedFormControl(''), + handle: new UntypedFormControl('') }); this.config = this.configurationDataService.findByPropertyName(CURATION_CFG); - this.config.pipe( - find((rd: RemoteData) => rd.hasSucceeded), - map((rd: RemoteData) => rd.payload) - ).subscribe((configProperties) => { + this.subs.push(this.config.pipe( + getFirstSucceededRemoteDataPayload(), + ).subscribe((configProperties: ConfigurationProperty) => { this.tasks = configProperties.values .filter((value) => isNotEmpty(value) && value.includes('=')) .map((value) => value.split('=')[1].trim()); this.form.get('task').patchValue(this.tasks[0]); - }); + this.cdr.detectChanges(); + })); } /** @@ -75,33 +81,41 @@ export class CurationFormComponent implements OnInit { */ submit() { const taskName = this.form.get('task').value; - let handle; + let handle$: Observable; if (this.hasHandleValue()) { - handle = this.handleService.normalizeHandle(this.dsoHandle); - if (isEmpty(handle)) { - this.notificationsService.error(this.translateService.get('curation.form.submit.error.head'), - this.translateService.get('curation.form.submit.error.invalid-handle')); - return; - } + handle$ = this.handleService.normalizeHandle(this.dsoHandle).pipe( + map((handle: string | null) => { + if (isEmpty(handle)) { + this.notificationsService.error(this.translateService.get('curation.form.submit.error.head'), + this.translateService.get('curation.form.submit.error.invalid-handle')); + } + return handle; + }), + ); } else { - handle = this.handleService.normalizeHandle(this.form.get('handle').value); - if (isEmpty(handle)) { - handle = 'all'; - } + handle$ = this.handleService.normalizeHandle(this.form.get('handle').value).pipe( + map((handle: string | null) => isEmpty(handle) ? 'all' : handle), + ); } - this.scriptDataService.invoke('curate', [ - { name: '-t', value: taskName }, - { name: '-i', value: handle }, - ], []).pipe(getFirstCompletedRemoteData()).subscribe((rd: RemoteData) => { - if (rd.hasSucceeded) { - this.notificationsService.success(this.translateService.get('curation.form.submit.success.head'), - this.translateService.get('curation.form.submit.success.content')); - this.router.navigateByUrl(getProcessDetailRoute(rd.payload.processId)); - } else { - this.notificationsService.error(this.translateService.get('curation.form.submit.error.head'), - this.translateService.get('curation.form.submit.error.content')); + this.subs.push(handle$.subscribe((handle: string) => { + if (hasValue(handle)) { + this.subs.push(this.scriptDataService.invoke('curate', [ + { name: '-t', value: taskName }, + { name: '-i', value: handle }, + ], []).pipe( + getFirstCompletedRemoteData(), + ).subscribe((rd: RemoteData) => { + if (rd.hasSucceeded) { + this.notificationsService.success(this.translateService.get('curation.form.submit.success.head'), + this.translateService.get('curation.form.submit.success.content')); + void this.router.navigateByUrl(getProcessDetailRoute(rd.payload.processId)); + } else { + this.notificationsService.error(this.translateService.get('curation.form.submit.error.head'), + this.translateService.get('curation.form.submit.error.content')); + } + })); } - }); + })); } } diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-field-values/dso-edit-metadata-field-values.component.ts b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-field-values/dso-edit-metadata-field-values.component.ts index c489397a2da..447b9e829c4 100644 --- a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-field-values/dso-edit-metadata-field-values.component.ts +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-field-values/dso-edit-metadata-field-values.component.ts @@ -50,7 +50,7 @@ export class DsoEditMetadataFieldValuesComponent { /** * Security Settings configuration for the current entity */ - @Input() metadataSecurityConfiguration: Observable; + @Input() metadataSecurityConfiguration: MetadataSecurityConfiguration; /** * Emit when the value has been saved within the form */ diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-headers/dso-edit-metadata-headers.component.html b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-headers/dso-edit-metadata-headers.component.html index 232a1a6fcac..16f8039f6cb 100644 --- a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-headers/dso-edit-metadata-headers.component.html +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-headers/dso-edit-metadata-headers.component.html @@ -4,7 +4,7 @@
    {{ dsoType + '.edit.metadata.headers.value' | translate }}
    {{ dsoType + '.edit.metadata.headers.language' | translate }}
    -
    {{'item.edit.metadata.headers.security'| translate}}
    +
    {{'item.edit.metadata.headers.security'| translate}}
    {{ dsoType + '.edit.metadata.headers.edit' | translate }}
    diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-shared/dso-edit-metadata-cells.scss b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-shared/dso-edit-metadata-cells.scss index 20d479ac880..c67fd5f1ea7 100644 --- a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-shared/dso-edit-metadata-cells.scss +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-shared/dso-edit-metadata-cells.scss @@ -12,6 +12,11 @@ max-width: var(--ds-dso-edit-lang-width); } +.ds-security-cell { + min-width: var(--ds-dso-edit-security-width); + max-width: var(--ds-dso-edit-security-width); +} + .ds-edit-cell { min-width: var(--ds-dso-edit-actions-width); } diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.html b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.html index f41a13c10be..64f8115ea34 100644 --- a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.html +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.html @@ -7,7 +7,7 @@ [dsDebounce]="300" (onDebounce)="confirm.emit(false)">
    @@ -15,14 +15,14 @@
    -
    +
    - +
    diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.spec.ts b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.spec.ts index 10b3016a52d..1d4a29a4f14 100644 --- a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.spec.ts +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.spec.ts @@ -7,10 +7,13 @@ import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core'; import { RelationshipDataService } from '../../../core/data/relationship-data.service'; import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; import { of } from 'rxjs/internal/observable/of'; -import { ItemMetadataRepresentation } from '../../../core/shared/metadata-representation/item/item-metadata-representation.model'; +import { + ItemMetadataRepresentation +} from '../../../core/shared/metadata-representation/item/item-metadata-representation.model'; import { MetadataValue, VIRTUAL_METADATA_PREFIX } from '../../../core/shared/metadata.models'; import { DsoEditMetadataChangeType, DsoEditMetadataValue } from '../dso-edit-metadata-form'; import { By } from '@angular/platform-browser'; +import { mockSecurityConfig } from '../../../shared/mocks/submission.mock'; const EDIT_BTN = 'edit'; const CONFIRM_BTN = 'confirm'; @@ -63,12 +66,27 @@ describe('DsoEditMetadataValueComponent', () => { fixture = TestBed.createComponent(DsoEditMetadataValueComponent); component = fixture.componentInstance; component.mdValue = editMetadataValue; + component.metadataSecurityConfiguration = mockSecurityConfig; + component.mdField = 'person.birthDate'; component.saving$ = of(false); + spyOn(component, 'initSecurityLevel').and.callThrough(); fixture.detectChanges(); }); it('should not show a badge', () => { + expect(fixture.debugElement.query(By.css('ds-themed-type-badge'))).toBeNull(); + }); + + it('should call initSecurityLevel on init', () => { expect(fixture.debugElement.query(By.css('ds-type-badge'))).toBeNull(); + expect(component.initSecurityLevel).toHaveBeenCalled(); + expect(component.mdSecurityConfigLevel$.value).toEqual([0, 1]); + }); + + it('should call initSecurityLevel when field changes', () => { + component.mdField = 'test'; + expect(component.initSecurityLevel).toHaveBeenCalled(); + expect(component.mdSecurityConfigLevel$.value).toEqual([0, 1, 2]); }); describe('when no changes have been made', () => { @@ -134,7 +152,7 @@ describe('DsoEditMetadataValueComponent', () => { }); it('should show a badge', () => { - expect(fixture.debugElement.query(By.css('ds-type-badge'))).toBeTruthy(); + expect(fixture.debugElement.query(By.css('ds-themed-type-badge'))).toBeTruthy(); }); assertButton(EDIT_BTN, true, true); diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.ts b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.ts index e00edf846bd..baa958e396d 100644 --- a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.ts +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.ts @@ -1,18 +1,28 @@ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; import { DsoEditMetadataChangeType, DsoEditMetadataValue } from '../dso-edit-metadata-form'; -import { Observable } from 'rxjs/internal/Observable'; +import { + BehaviorSubject, + combineLatest, + distinctUntilChanged, + EMPTY, + Observable, + shareReplay, + Subscription +} from 'rxjs'; import { MetadataRepresentation, MetadataRepresentationType } from '../../../core/shared/metadata-representation/metadata-representation.model'; import { RelationshipDataService } from '../../../core/data/relationship-data.service'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; -import { ItemMetadataRepresentation } from '../../../core/shared/metadata-representation/item/item-metadata-representation.model'; +import { + ItemMetadataRepresentation +} from '../../../core/shared/metadata-representation/item/item-metadata-representation.model'; import { map } from 'rxjs/operators'; import { getItemPageRoute } from '../../../item-page/item-page-routing-paths'; import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; -import { EMPTY } from 'rxjs/internal/observable/empty'; import { MetadataSecurityConfiguration } from '../../../core/submission/models/metadata-security-configuration'; +import { hasValue } from '../../../shared/empty.util'; @Component({ selector: 'ds-dso-edit-metadata-value', @@ -22,7 +32,7 @@ import { MetadataSecurityConfiguration } from '../../../core/submission/models/m /** * Component displaying a single editable row for a metadata value */ -export class DsoEditMetadataValueComponent implements OnInit { +export class DsoEditMetadataValueComponent implements OnInit, OnDestroy { /** * The parent {@link DSpaceObject} to display a metadata form for * Also used to determine metadata-representations in case of virtual metadata @@ -37,12 +47,31 @@ export class DsoEditMetadataValueComponent implements OnInit { /** * The metadata security configuration for the entity. */ - @Input() metadataSecurityConfiguration: Observable; + @Input() + set metadataSecurityConfiguration(metadataSecurityConfiguration: MetadataSecurityConfiguration) { + this._metadataSecurityConfiguration$.next(metadataSecurityConfiguration); + } + + get metadataSecurityConfiguration() { + return this._metadataSecurityConfiguration$.value; + } + + protected readonly _metadataSecurityConfiguration$ = + new BehaviorSubject(null); /** * The metadata field to display a value for */ - @Input() mdField: string; + @Input() + set mdField(mdField: string) { + this._mdField$.next(mdField); + } + + get mdField() { + return this._mdField$.value; + } + + protected readonly _mdField$ = new BehaviorSubject(null); /** * Flag whether this is a new metadata field or exists already @@ -122,6 +151,11 @@ export class DsoEditMetadataValueComponent implements OnInit { * The name of the item represented by this virtual metadata value (otherwise null) */ mdRepresentationName$: Observable; + readonly mdSecurityConfigLevel$: BehaviorSubject = new BehaviorSubject([]); + + canShowMetadataSecurity$: Observable; + + private sub: Subscription; constructor(protected relationshipService: RelationshipDataService, protected dsoNameService: DSONameService) { @@ -129,6 +163,42 @@ export class DsoEditMetadataValueComponent implements OnInit { ngOnInit(): void { this.initVirtualProperties(); + + this.sub = combineLatest([ + this._mdField$, + this._metadataSecurityConfiguration$ + ]).subscribe(([mdField, metadataSecurityConfig]) => this.initSecurityLevel(mdField, metadataSecurityConfig)); + + this.canShowMetadataSecurity$ = + combineLatest([ + this._mdField$.pipe(distinctUntilChanged()), + this.mdSecurityConfigLevel$ + ]).pipe( + map(([mdField, securityConfigLevel]) => hasValue(mdField) && this.hasSecurityChoice(securityConfigLevel)), + shareReplay(1), + ); + } + + private hasSecurityChoice(securityConfigLevel: number[]) { + return securityConfigLevel?.length > 1; + } + + ngOnDestroy(): void { + if (hasValue(this.sub)) { + this.sub.unsubscribe(); + } + } + + initSecurityLevel(mdField: string, metadataSecurityConfig: MetadataSecurityConfiguration) { + let appliedSecurity: number[] = []; + if (hasValue(metadataSecurityConfig)) { + if (metadataSecurityConfig?.metadataCustomSecurity[mdField]) { + appliedSecurity = metadataSecurityConfig.metadataCustomSecurity[mdField]; + } else if (metadataSecurityConfig?.metadataSecurityDefault) { + appliedSecurity = metadataSecurityConfig.metadataSecurityDefault; + } + } + this.mdSecurityConfigLevel$.next(appliedSecurity); } /** diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.html b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.html index 86801db1759..995d5201470 100644 --- a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.html +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.html @@ -1,4 +1,4 @@ -
    - - + +
    - - + +
    diff --git a/src/app/entity-groups/journal-entities/item-list-elements/journal-issue/journal-issue-list-element.component.spec.ts b/src/app/entity-groups/journal-entities/item-list-elements/journal-issue/journal-issue-list-element.component.spec.ts index 8c33ffdc1e3..59af3b9e7ba 100644 --- a/src/app/entity-groups/journal-entities/item-list-elements/journal-issue/journal-issue-list-element.component.spec.ts +++ b/src/app/entity-groups/journal-entities/item-list-elements/journal-issue/journal-issue-list-element.component.spec.ts @@ -6,6 +6,8 @@ import { of as observableOf } from 'rxjs'; import { Item } from '../../../../core/shared/item.model'; import { TruncatePipe } from '../../../../shared/utils/truncate.pipe'; import { TruncatableService } from '../../../../shared/truncatable/truncatable.service'; +import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; +import { DSONameServiceMock } from '../../../../shared/mocks/dso-name.service.mock'; const mockItem: Item = Object.assign(new Item(), { bundles: observableOf({}), @@ -43,6 +45,7 @@ describe('JournalIssueListElementComponent', () => { TestBed.configureTestingModule({ declarations: [JournalIssueListElementComponent, TruncatePipe], providers: [ + { provide: DSONameService, useValue: new DSONameServiceMock() }, { provide: TruncatableService, useValue: truncatableServiceStub }, ], schemas: [NO_ERRORS_SCHEMA] diff --git a/src/app/entity-groups/journal-entities/item-list-elements/journal-volume/journal-volume-list-element.component.spec.ts b/src/app/entity-groups/journal-entities/item-list-elements/journal-volume/journal-volume-list-element.component.spec.ts index f4bf0d250b9..663c1e477ef 100644 --- a/src/app/entity-groups/journal-entities/item-list-elements/journal-volume/journal-volume-list-element.component.spec.ts +++ b/src/app/entity-groups/journal-entities/item-list-elements/journal-volume/journal-volume-list-element.component.spec.ts @@ -6,6 +6,8 @@ import { of as observableOf } from 'rxjs'; import { Item } from '../../../../core/shared/item.model'; import { TruncatePipe } from '../../../../shared/utils/truncate.pipe'; import { TruncatableService } from '../../../../shared/truncatable/truncatable.service'; +import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; +import { DSONameServiceMock } from '../../../../shared/mocks/dso-name.service.mock'; const mockItem: Item = Object.assign(new Item(), { bundles: observableOf({}), @@ -43,6 +45,7 @@ describe('JournalVolumeListElementComponent', () => { TestBed.configureTestingModule({ declarations: [JournalVolumeListElementComponent, TruncatePipe], providers: [ + { provide: DSONameService, useValue: new DSONameServiceMock() }, { provide: TruncatableService, useValue: truncatableServiceStub }, ], schemas: [NO_ERRORS_SCHEMA] diff --git a/src/app/entity-groups/journal-entities/item-list-elements/journal/journal-list-element.component.spec.ts b/src/app/entity-groups/journal-entities/item-list-elements/journal/journal-list-element.component.spec.ts index b82876e3641..e5dd55772bd 100644 --- a/src/app/entity-groups/journal-entities/item-list-elements/journal/journal-list-element.component.spec.ts +++ b/src/app/entity-groups/journal-entities/item-list-elements/journal/journal-list-element.component.spec.ts @@ -6,6 +6,8 @@ import { of as observableOf } from 'rxjs'; import { Item } from '../../../../core/shared/item.model'; import { TruncatePipe } from '../../../../shared/utils/truncate.pipe'; import { TruncatableService } from '../../../../shared/truncatable/truncatable.service'; +import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; +import { DSONameServiceMock } from '../../../../shared/mocks/dso-name.service.mock'; const mockItem: Item = Object.assign(new Item(), { bundles: observableOf({}), @@ -34,9 +36,10 @@ describe('JournalListElementComponent', () => { }; beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ + return TestBed.configureTestingModule({ declarations: [JournalListElementComponent, TruncatePipe], providers: [ + { provide: DSONameService, useValue: new DSONameServiceMock() }, { provide: TruncatableService, useValue: truncatableServiceStub }, ], schemas: [NO_ERRORS_SCHEMA] diff --git a/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal-issue/journal-issue-search-result-list-element.component.html b/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal-issue/journal-issue-search-result-list-element.component.html index 6ec5c9eb4b6..5a3416dee47 100644 --- a/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal-issue/journal-issue-search-result-list-element.component.html +++ b/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal-issue/journal-issue-search-result-list-element.component.html @@ -1,7 +1,7 @@
    - +
    - +
    - + -
    - - + +
    @@ -9,7 +9,7 @@
    - +
    - - + +
    @@ -9,7 +9,7 @@
    - +
    - - + +
    @@ -9,7 +9,7 @@
    - + { imports: [NoopAnimationsModule], declarations: [OrgUnitGridElementComponent, TruncatePipe], providers: [ + { provide: DSONameService, useValue: new DSONameServiceMock() }, { provide: TruncatableService, useValue: truncatableServiceStub }, ], schemas: [NO_ERRORS_SCHEMA] diff --git a/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.spec.ts b/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.spec.ts index b4d563b0d56..ca0784e9972 100644 --- a/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.spec.ts +++ b/src/app/entity-groups/research-entities/item-grid-elements/person/person-grid-element.component.spec.ts @@ -10,6 +10,8 @@ import { TruncatePipe } from '../../../../shared/utils/truncate.pipe'; import { TruncatableService } from '../../../../shared/truncatable/truncatable.service'; import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; import { By } from '@angular/platform-browser'; +import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; +import { DSONameServiceMock } from '../../../../shared/mocks/dso-name.service.mock'; const mockItem = Object.assign(new Item(), { bundles: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])), @@ -48,6 +50,7 @@ describe('PersonGridElementComponent', () => { imports: [NoopAnimationsModule], declarations: [PersonGridElementComponent, TruncatePipe], providers: [ + { provide: DSONameService, useValue: new DSONameServiceMock() }, { provide: TruncatableService, useValue: truncatableServiceStub }, ], schemas: [NO_ERRORS_SCHEMA] diff --git a/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.spec.ts b/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.spec.ts index 5f4808bd2a4..3f92bfe4107 100644 --- a/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.spec.ts +++ b/src/app/entity-groups/research-entities/item-grid-elements/project/project-grid-element.component.spec.ts @@ -10,6 +10,8 @@ import { TruncatePipe } from '../../../../shared/utils/truncate.pipe'; import { TruncatableService } from '../../../../shared/truncatable/truncatable.service'; import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; import { By } from '@angular/platform-browser'; +import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; +import { DSONameServiceMock } from '../../../../shared/mocks/dso-name.service.mock'; const mockItem = Object.assign(new Item(), { bundles: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])), @@ -42,6 +44,7 @@ describe('ProjectGridElementComponent', () => { imports: [NoopAnimationsModule], declarations: [ProjectGridElementComponent, TruncatePipe], providers: [ + { provide: DSONameService, useValue: new DSONameServiceMock() }, { provide: TruncatableService, useValue: truncatableServiceStub }, ], schemas: [NO_ERRORS_SCHEMA] diff --git a/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/org-unit/org-unit-search-result-grid-element.component.html b/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/org-unit/org-unit-search-result-grid-element.component.html index 2dbccd43462..502365e60a9 100644 --- a/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/org-unit/org-unit-search-result-grid-element.component.html +++ b/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/org-unit/org-unit-search-result-grid-element.component.html @@ -5,21 +5,21 @@
    - - + +
    - - + +
    diff --git a/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/person/person-search-result-grid-element.component.html b/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/person/person-search-result-grid-element.component.html index d2c9f05d256..0dc91f901c0 100644 --- a/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/person/person-search-result-grid-element.component.html +++ b/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/person/person-search-result-grid-element.component.html @@ -5,21 +5,21 @@
    - - + +
    - - + +
    diff --git a/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/person/person-search-result-grid-element.component.spec.ts b/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/person/person-search-result-grid-element.component.spec.ts index 4c8c7d3effe..9fa1743ed47 100644 --- a/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/person/person-search-result-grid-element.component.spec.ts +++ b/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/person/person-search-result-grid-element.component.spec.ts @@ -115,7 +115,7 @@ describe('PersonSearchResultGridElementComponent check different maxSize of thum }); it('should show bitstream content image src', () => { - const thum = fixture.debugElement.query(By.css('ds-thumbnail')); + const thum = fixture.debugElement.query(By.css('ds-themed-thumbnail')); expect(thum.nativeElement.thumbnail.id).toEqual('bitstream1'); }); @@ -130,7 +130,7 @@ describe('PersonSearchResultGridElementComponent check different maxSize of thum }); it('should not show bitstream content image src', () => { - const thum = fixture.debugElement.query(By.css('ds-thumbnail')); + const thum = fixture.debugElement.query(By.css('ds-themed-thumbnail')); expect(thum.nativeElement.thumbnail).toBeFalsy(); }); @@ -146,7 +146,7 @@ describe('PersonSearchResultGridElementComponent check different maxSize of thum }); it('should show thumbnail content image src', () => { - const thum = fixture.debugElement.query(By.css('ds-thumbnail')); + const thum = fixture.debugElement.query(By.css('ds-themed-thumbnail')); expect(thum.nativeElement.thumbnail.id).toEqual('thumbnail1'); }); @@ -162,7 +162,7 @@ describe('PersonSearchResultGridElementComponent check different maxSize of thum }); it('should not show thumbnail content image src', () => { - const thum = fixture.debugElement.query(By.css('ds-thumbnail')); + const thum = fixture.debugElement.query(By.css('ds-themed-thumbnail')); expect(thum.nativeElement.thumbnail).toBeFalsy(); }); diff --git a/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/project/project-search-result-grid-element.component.html b/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/project/project-search-result-grid-element.component.html index a54d136de2a..97d4016ec4a 100644 --- a/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/project/project-search-result-grid-element.component.html +++ b/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/project/project-search-result-grid-element.component.html @@ -5,21 +5,21 @@
    - - + +
    - - + +
    diff --git a/src/app/entity-groups/research-entities/item-list-elements/org-unit/org-unit-list-element.component.spec.ts b/src/app/entity-groups/research-entities/item-list-elements/org-unit/org-unit-list-element.component.spec.ts index 1cca1d33144..275accc9561 100644 --- a/src/app/entity-groups/research-entities/item-list-elements/org-unit/org-unit-list-element.component.spec.ts +++ b/src/app/entity-groups/research-entities/item-list-elements/org-unit/org-unit-list-element.component.spec.ts @@ -6,6 +6,8 @@ import { of as observableOf } from 'rxjs'; import { Item } from '../../../../core/shared/item.model'; import { TruncatePipe } from '../../../../shared/utils/truncate.pipe'; import { TruncatableService } from '../../../../shared/truncatable/truncatable.service'; +import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; +import { DSONameServiceMock } from '../../../../shared/mocks/dso-name.service.mock'; const mockItem: Item = Object.assign(new Item(), { bundles: observableOf({}), @@ -37,6 +39,7 @@ describe('OrgUnitListElementComponent', () => { TestBed.configureTestingModule({ declarations: [OrgUnitListElementComponent, TruncatePipe], providers: [ + { provide: DSONameService, useValue: new DSONameServiceMock() }, { provide: TruncatableService, useValue: truncatableServiceStub }, ], schemas: [NO_ERRORS_SCHEMA] diff --git a/src/app/entity-groups/research-entities/item-list-elements/person/person-list-element.component.spec.ts b/src/app/entity-groups/research-entities/item-list-elements/person/person-list-element.component.spec.ts index 8e86a129cec..dc874b8ec84 100644 --- a/src/app/entity-groups/research-entities/item-list-elements/person/person-list-element.component.spec.ts +++ b/src/app/entity-groups/research-entities/item-list-elements/person/person-list-element.component.spec.ts @@ -6,6 +6,8 @@ import { of as observableOf } from 'rxjs'; import { Item } from '../../../../core/shared/item.model'; import { TruncatePipe } from '../../../../shared/utils/truncate.pipe'; import { TruncatableService } from '../../../../shared/truncatable/truncatable.service'; +import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; +import { DSONameServiceMock } from '../../../../shared/mocks/dso-name.service.mock'; const mockItem: Item = Object.assign(new Item(), { bundles: observableOf({}), @@ -37,6 +39,7 @@ describe('PersonListElementComponent', () => { TestBed.configureTestingModule({ declarations: [PersonListElementComponent, TruncatePipe], providers: [ + { provide: DSONameService, useValue: new DSONameServiceMock() }, { provide: TruncatableService, useValue: truncatableServiceStub }, ], schemas: [NO_ERRORS_SCHEMA] diff --git a/src/app/entity-groups/research-entities/item-list-elements/project/project-list-element.component.spec.ts b/src/app/entity-groups/research-entities/item-list-elements/project/project-list-element.component.spec.ts index 0c08d7eaaa9..02241e3060f 100644 --- a/src/app/entity-groups/research-entities/item-list-elements/project/project-list-element.component.spec.ts +++ b/src/app/entity-groups/research-entities/item-list-elements/project/project-list-element.component.spec.ts @@ -6,6 +6,8 @@ import { Item } from '../../../../core/shared/item.model'; import { TruncatePipe } from '../../../../shared/utils/truncate.pipe'; import { TruncatableService } from '../../../../shared/truncatable/truncatable.service'; import { ProjectListElementComponent } from './project-list-element.component'; +import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; +import { DSONameServiceMock } from '../../../../shared/mocks/dso-name.service.mock'; const mockItem: Item = Object.assign(new Item(), { bundles: observableOf({}), @@ -37,6 +39,7 @@ describe('ProjectListElementComponent', () => { TestBed.configureTestingModule({ declarations: [ProjectListElementComponent, TruncatePipe], providers: [ + { provide: DSONameService, useValue: new DSONameServiceMock() }, { provide: TruncatableService, useValue: truncatableServiceStub }, ], schemas: [NO_ERRORS_SCHEMA] diff --git a/src/app/entity-groups/research-entities/item-list-elements/search-result-list-elements/org-unit/org-unit-search-result-list-element.component.html b/src/app/entity-groups/research-entities/item-list-elements/search-result-list-elements/org-unit/org-unit-search-result-list-element.component.html index c15694b3216..17c5814edc7 100644 --- a/src/app/entity-groups/research-entities/item-list-elements/search-result-list-elements/org-unit/org-unit-search-result-list-element.component.html +++ b/src/app/entity-groups/research-entities/item-list-elements/search-result-list-elements/org-unit/org-unit-search-result-list-element.component.html @@ -1,7 +1,7 @@
    - +
    - +
    - +
    - - + +
    @@ -9,12 +9,12 @@
    - - +
    - - + +
    @@ -9,11 +9,11 @@
    - - +
    - - + +
    - - + - - + diff --git a/src/app/entity-groups/research-entities/metadata-representations/org-unit/org-unit-item-metadata-list-element.component.html b/src/app/entity-groups/research-entities/metadata-representations/org-unit/org-unit-item-metadata-list-element.component.html index 1771f3d2bc6..ec4dbd43236 100644 --- a/src/app/entity-groups/research-entities/metadata-representations/org-unit/org-unit-item-metadata-list-element.component.html +++ b/src/app/entity-groups/research-entities/metadata-representations/org-unit/org-unit-item-metadata-list-element.component.html @@ -1,12 +1,12 @@ - + - + + [innerHTML]="mdRepresentation.getValue()" + [ngbTooltip]="mdRepresentation.allMetadata(['dc.description']).length > 0 ? descTemplate : null"> diff --git a/src/app/entity-groups/research-entities/metadata-representations/org-unit/org-unit-item-metadata-list-element.component.spec.ts b/src/app/entity-groups/research-entities/metadata-representations/org-unit/org-unit-item-metadata-list-element.component.spec.ts index eff6fd0b314..429f2986b94 100644 --- a/src/app/entity-groups/research-entities/metadata-representations/org-unit/org-unit-item-metadata-list-element.component.spec.ts +++ b/src/app/entity-groups/research-entities/metadata-representations/org-unit/org-unit-item-metadata-list-element.component.spec.ts @@ -34,7 +34,7 @@ describe('OrgUnitItemMetadataListElementComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(OrgUnitItemMetadataListElementComponent); comp = fixture.componentInstance; - comp.metadataRepresentation = mockItemMetadataRepresentation; + comp.mdRepresentation = mockItemMetadataRepresentation; fixture.detectChanges(); }); diff --git a/src/app/entity-groups/research-entities/metadata-representations/person/person-item-metadata-list-element.component.html b/src/app/entity-groups/research-entities/metadata-representations/person/person-item-metadata-list-element.component.html index 97632117f40..6f560567814 100644 --- a/src/app/entity-groups/research-entities/metadata-representations/person/person-item-metadata-list-element.component.html +++ b/src/app/entity-groups/research-entities/metadata-representations/person/person-item-metadata-list-element.component.html @@ -1,15 +1,15 @@ - - + - + + [innerHTML]="mdRepresentation.getValue()" + [ngbTooltip]="mdRepresentation.allMetadata(['person.jobTitle']).length > 0 ? descTemplate : null"> diff --git a/src/app/entity-groups/research-entities/metadata-representations/person/person-item-metadata-list-element.component.spec.ts b/src/app/entity-groups/research-entities/metadata-representations/person/person-item-metadata-list-element.component.spec.ts index 895cf522230..b9ebf19b676 100644 --- a/src/app/entity-groups/research-entities/metadata-representations/person/person-item-metadata-list-element.component.spec.ts +++ b/src/app/entity-groups/research-entities/metadata-representations/person/person-item-metadata-list-element.component.spec.ts @@ -36,7 +36,7 @@ describe('PersonItemMetadataListElementComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(PersonItemMetadataListElementComponent); comp = fixture.componentInstance; - comp.metadataRepresentation = mockItemMetadataRepresentation; + comp.mdRepresentation = mockItemMetadataRepresentation; fixture.detectChanges(); }); diff --git a/src/app/entity-groups/research-entities/metadata-representations/project/project-item-metadata-list-element.component.html b/src/app/entity-groups/research-entities/metadata-representations/project/project-item-metadata-list-element.component.html new file mode 100644 index 00000000000..acc9173bf7d --- /dev/null +++ b/src/app/entity-groups/research-entities/metadata-representations/project/project-item-metadata-list-element.component.html @@ -0,0 +1,12 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/app/entity-groups/research-entities/metadata-representations/project/project-item-metadata-list-element.component.spec.ts b/src/app/entity-groups/research-entities/metadata-representations/project/project-item-metadata-list-element.component.spec.ts new file mode 100644 index 00000000000..afa565ce406 --- /dev/null +++ b/src/app/entity-groups/research-entities/metadata-representations/project/project-item-metadata-list-element.component.spec.ts @@ -0,0 +1,51 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; +import { By } from '@angular/platform-browser'; + +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { ItemMetadataRepresentation } from '../../../../core/shared/metadata-representation/item/item-metadata-representation.model'; +import { Item } from '../../../../core/shared/item.model'; +import { ProjectItemMetadataListElementComponent } from './project-item-metadata-list-element.component'; +import { MetadataValue } from '../../../../core/shared/metadata.models'; +import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; +import { DSONameServiceMock } from '../../../../shared/mocks/dso-name.service.mock'; + +const projectTitle = 'Lorem ipsum dolor sit amet'; +const mockItem = Object.assign(new Item(), { metadata: { 'dc.title': [{ value: projectTitle }] } }); +const virtMD = Object.assign(new MetadataValue(), { value: projectTitle }); + +const mockItemMetadataRepresentation = Object.assign(new ItemMetadataRepresentation(virtMD), mockItem); + +describe('ProjectItemMetadataListElementComponent', () => { + let comp: ProjectItemMetadataListElementComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports:[ + NgbModule + ], + declarations: [ProjectItemMetadataListElementComponent], + providers: [ + { provide: DSONameService, useValue: new DSONameServiceMock() } + ], + schemas: [NO_ERRORS_SCHEMA] + }).overrideComponent(ProjectItemMetadataListElementComponent, { + set: { changeDetection: ChangeDetectionStrategy.Default } + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ProjectItemMetadataListElementComponent); + comp = fixture.componentInstance; + comp.mdRepresentation = mockItemMetadataRepresentation; + fixture.detectChanges(); + }); + + it('should show the project\'s name as a link', () => { + const linkText = fixture.debugElement.query(By.css('a')).nativeElement.textContent; + expect(linkText).toBe(projectTitle); + }); + +}); diff --git a/src/app/entity-groups/research-entities/metadata-representations/project/project-item-metadata-list-element.component.ts b/src/app/entity-groups/research-entities/metadata-representations/project/project-item-metadata-list-element.component.ts new file mode 100644 index 00000000000..a38a1f5cffd --- /dev/null +++ b/src/app/entity-groups/research-entities/metadata-representations/project/project-item-metadata-list-element.component.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; +import { metadataRepresentationComponent } from '../../../../shared/metadata-representation/metadata-representation.decorator'; +import { MetadataRepresentationType } from '../../../../core/shared/metadata-representation/metadata-representation.model'; +import { ItemMetadataRepresentationListElementComponent } from '../../../../shared/object-list/metadata-representation-list-element/item/item-metadata-representation-list-element.component'; +import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; + +@metadataRepresentationComponent('Project', MetadataRepresentationType.Item) +@Component({ + selector: 'ds-project-item-metadata-list-element', + templateUrl: './project-item-metadata-list-element.component.html' +}) +/** + * The component for displaying an item of the type Project as a metadata field + */ +export class ProjectItemMetadataListElementComponent extends ItemMetadataRepresentationListElementComponent { + /** + * Initialize instance variables + * + * @param dsoNameService + */ + constructor( + public dsoNameService: DSONameService + ) { + super(); + } +} diff --git a/src/app/entity-groups/research-entities/research-entities.module.ts b/src/app/entity-groups/research-entities/research-entities.module.ts index e08d424bf78..d5e9148ec44 100644 --- a/src/app/entity-groups/research-entities/research-entities.module.ts +++ b/src/app/entity-groups/research-entities/research-entities.module.ts @@ -19,6 +19,7 @@ import { OrgUnitSearchResultGridElementComponent } from './item-grid-elements/se import { ProjectSearchResultGridElementComponent } from './item-grid-elements/search-result-grid-elements/project/project-search-result-grid-element.component'; import { PersonItemMetadataListElementComponent } from './metadata-representations/person/person-item-metadata-list-element.component'; import { OrgUnitItemMetadataListElementComponent } from './metadata-representations/org-unit/org-unit-item-metadata-list-element.component'; +import { ProjectItemMetadataListElementComponent } from './metadata-representations/project/project-item-metadata-list-element.component'; import { PersonSearchResultListSubmissionElementComponent } from './submission/item-list-elements/person/person-search-result-list-submission-element.component'; import { PersonInputSuggestionsComponent } from './submission/item-list-elements/person/person-suggestions/person-input-suggestions.component'; import { NameVariantModalComponent } from './submission/name-variant-modal/name-variant-modal.component'; @@ -37,6 +38,7 @@ const ENTRY_COMPONENTS = [ // put only entry components that use custom decorator OrgUnitComponent, PersonComponent, + ProjectItemMetadataListElementComponent, ProjectComponent, OrgUnitListElementComponent, OrgUnitItemMetadataListElementComponent, diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.html b/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.html index 525a7967dcf..7c9ad0d1f5a 100644 --- a/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.html +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.html @@ -27,4 +27,35 @@
    +
    +

    + +

    + +
    diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.scss b/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.scss index e69de29bb2d..8345f56af6a 100644 --- a/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.scss +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.scss @@ -0,0 +1,6 @@ +:host .text-wrap { + width: 15rem; + white-space: nowrap !important; + overflow: hidden; + text-overflow: ellipsis; +} diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.spec.ts b/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.spec.ts index 93696554575..ca91d265700 100644 --- a/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.spec.ts +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.spec.ts @@ -4,6 +4,8 @@ import { ExternalSourceEntry } from '../../../../../core/shared/external-source- import { TranslateModule } from '@ngx-translate/core'; import { NO_ERRORS_SCHEMA } from '@angular/core'; import { By } from '@angular/platform-browser'; +import { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap'; +import { MetadataValueFilter } from 'src/app/core/shared/metadata.models'; describe('ExternalSourceEntryListSubmissionElementComponent', () => { let component: ExternalSourceEntryListSubmissionElementComponent; @@ -17,30 +19,31 @@ describe('ExternalSourceEntryListSubmissionElementComponent', () => { metadata: { 'dc.identifier.uri': [ { - value: uri - } + value: uri, + }, ], 'dc.date.issued': [ { - 'value': '2020-10-06T00:00:00Z', - 'language': null, - 'authority': null, - 'confidence': -1, - 'place': -1 - } + value: '2020-10-06T00:00:00Z', + language: null, + authority: null, + confidence: -1, + place: -1, + }, ], 'dc.contributor.author': [ { - 'value': 'Oktyabrsky, Oleg N', - 'language': null, - 'authority': null, - 'confidence': -1, - 'place': -1 - } + value: 'Oktyabrsky, Oleg N', + language: null, + authority: null, + confidence: -1, + place: -1, + }, ], 'dc.description.abstract': [ { - 'value': 'Activities of plant polyphenols (PPs), resveratrol and quercetin, alone or in combination with four conventional antibiotics against ' + + value: + 'Activities of plant polyphenols (PPs), resveratrol and quercetin, alone or in combination with four conventional antibiotics against ' + 'Escherichia coli have been investigated. In medium without antibiotics, both polyphenols caused a dose-dependent growth inhibition. ' + 'However, pretreatment with resveratrol (40 and 100 μg ml) and quercetin (40 μg ml) reduced the bacteriostatic effect of kanamycin, ' + ', cefotaxime and partially of ciprofloxacin. With few exceptions, both PPs also reduced the bactericidal effect of tested antibiotics.' + @@ -52,37 +55,65 @@ describe('ExternalSourceEntryListSubmissionElementComponent', () => { 'There is a growing interest in the use of plant-derived compounds to enhance the toxicity of traditional antibiotics.' + ' This and other studies show that, under certain conditions, the use of polyphenols as adjuvants may not exert the expected' + ' therapeutic effect, but rather to decrease antimicrobial activity of antibiotics.', - 'language': null, - 'authority': null, - 'confidence': -1, - 'place': -1 - } + language: null, + authority: null, + confidence: -1, + place: -1, + }, ], 'dc.identifier.doi': [ { - 'value': '10.1007/s11274-020-02934-y', - 'language': null, - 'authority': null, - 'confidence': -1, - 'place': -1 - } + value: '10.1007/s11274-020-02934-y', + language: null, + authority: null, + confidence: -1, + place: -1, + }, ], 'dc.identifier.pmid': [ { - 'value': '33025172', - 'language': null, - 'authority': null, - 'confidence': -1, - 'place': -1 - } + value: '33025172', + language: null, + authority: null, + confidence: -1, + place: -1, + }, ], - } + }, + matchObjects: [ + { + id: '7fd133e7-feaa-4be9-a1d2-5258694556ae', + uuid: '7fd133e7-feaa-4be9-a1d2-5258694556ae', + name: 'Public item', + handle: '123456789/4', + metadata: { + 'crisrp.name': [ + { + value: 'Public item', + language: null, + authority: null, + confidence: -1, + place: 0, + }, + ], + }, + inArchive: true, + discoverable: true, + withdrawn: false, + lastModified: '2023-10-20T09:23:12.984+00:00', + entityType: 'Publication', + type: 'item', + firstMetadataValue(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): string { + return undefined; + }, + }, + ], }); beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [ExternalSourceEntryListSubmissionElementComponent], - imports: [TranslateModule.forRoot()], + imports: [TranslateModule.forRoot(), NgbCollapseModule], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); })); @@ -117,4 +148,11 @@ describe('ExternalSourceEntryListSubmissionElementComponent', () => { it('should display the entry\'s identifiers', () => { expect(fixture.debugElement.query(By.css('[data-test="identifiers"]'))).toBeTruthy(); }); + + it('should display the entry\'s duplicate match titles when matchObjects has items', () => { + const accordionHeaderBtn = fixture.nativeElement.querySelector('.btn-link'); + accordionHeaderBtn.click(); + const matchObjectsLinks = fixture.nativeElement.querySelectorAll('ds-themed-item-list-preview'); + expect(matchObjectsLinks).toBeTruthy(); + }); }); diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.ts b/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.ts index d468c86f577..b595699a63d 100644 --- a/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.ts +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.ts @@ -6,6 +6,11 @@ import { Context } from '../../../../../core/shared/context.model'; import { Component, OnInit } from '@angular/core'; import { Metadata } from '../../../../../core/shared/metadata.utils'; import { MetadataValue } from '../../../../../core/shared/metadata.models'; +import { getItemPageRoute } from '../../../../../item-page/item-page-routing-paths'; +import { DuplicateMatchMetadataDetailConfig } from '../../../../../submission/sections/detect-duplicate/models/duplicate-detail-metadata.model'; +import { environment } from '../../../../../../environments/environment'; +import { Item } from '../../../../../core/shared/item.model'; +import { PaginationComponentOptions } from '../../../../../shared/pagination/pagination-component-options.model'; @listableObjectComponent(ExternalSourceEntry, ViewMode.ListElement, Context.EntitySearchModal) @listableObjectComponent(ExternalSourceEntry, ViewMode.ListElement, Context.EntitySearchModalWithNameVariants) @@ -43,11 +48,54 @@ export class ExternalSourceEntryListSubmissionElementComponent extends AbstractL */ identifiers: MetadataValue[]; + /** + * The search result object. + * @type {object} + */ + itemPreviewObject = { hitHighlights: [] }; + + /** + * The list of the metadata, of the possible duplication, to show in HTML. + * @type {DuplicateMatchMetadataDetailConfig} + */ + metadataList: DuplicateMatchMetadataDetailConfig[]; + + /** + * Boolean value indicating whether the external source entry matchObjects section is collapsed or not. + */ + isCollapsed = true; + + /** + * The item page route + */ + itemPageRoute: string; + + /** + * The current pagination configuration for the page + */ + pageConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { + id: 'sobm', + pageSize: 2, + currentPage: 1, + }); + ngOnInit(): void { this.uri = Metadata.first(this.object.metadata, 'dc.identifier.uri'); this.issued = Metadata.first(this.object.metadata, 'dc.date.issued'); this.abstract = Metadata.first(this.object.metadata, 'dc.description.abstract'); this.contributors = Metadata.all(this.object.metadata, 'dc.contributor.*'); this.identifiers = Metadata.all(this.object.metadata, 'dc.identifier.*'); + this.metadataList = environment.submission.detectDuplicate.metadataDetailsList || []; + } + + /** + * Returns the route for the given item. + * @param item The item to get the route for. + * @returns The route for the given item. + */ + getItemRoute(item: Item): string { + if (item) { + return getItemPageRoute(item); + } } } diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.html b/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.html index 6123dc3dc80..e2649587382 100644 --- a/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.html +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.html @@ -1,6 +1,6 @@
    - +
    + [attr.rel]="(linkType == linkTypes.ExternalLink) ? 'noopener noreferrer' : null" class="dont-break-out"> { order: 'desc', sortField: 'dc.date.accessioned', numberOfItems: 5, - titleKey: 'lastPublications' + titleKey: 'lastPublications', + showThumbnails: false, }; const searchComponent: SearchSection = { diff --git a/src/app/external-log-in/external-log-in/external-log-in.component.html b/src/app/external-log-in/external-log-in/external-log-in.component.html index 5fdb56f626c..37e54c1941e 100644 --- a/src/app/external-log-in/external-log-in/external-log-in.component.html +++ b/src/app/external-log-in/external-log-in/external-log-in.component.html @@ -34,7 +34,7 @@
    {{messagePrefix + '.table.id' | translate}}
    {{ePerson.eperson.id}}{{ePerson.eperson.name}}
    {{eperson.id}} + + {{ dsoNameService.getName(eperson) }} + + - {{messagePrefix + '.table.email' | translate}}: {{ ePerson.eperson.email ? ePerson.eperson.email : '-' }}
    - {{messagePrefix + '.table.netid' | translate}}: {{ ePerson.eperson.netid ? ePerson.eperson.netid : '-' }} + {{messagePrefix + '.table.email' | translate}}: {{ eperson.email ? eperson.email : '-' }}
    + {{messagePrefix + '.table.netid' | translate}}: {{ eperson.netid ? eperson.netid : '-' }}
    - -
    @@ -148,9 +132,10 @@

    {{messagePrefix + '.headMembers' | translate}}

    - diff --git a/src/app/access-control/group-registry/group-form/members-list/members-list.component.spec.ts b/src/app/access-control/group-registry/group-form/members-list/members-list.component.spec.ts index e2ecd2e4548..3c792fb12dd 100644 --- a/src/app/access-control/group-registry/group-form/members-list/members-list.component.spec.ts +++ b/src/app/access-control/group-registry/group-form/members-list/members-list.component.spec.ts @@ -1,5 +1,5 @@ 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 { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { BrowserModule, By } from '@angular/platform-browser'; @@ -17,7 +17,7 @@ import { Group } from '../../../../core/eperson/models/group.model'; import { PageInfo } from '../../../../core/shared/page-info.model'; import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service'; import { NotificationsService } from '../../../../shared/notifications/notifications.service'; -import { GroupMock, GroupMock2 } from '../../../../shared/testing/group-mock'; +import { GroupMock } from '../../../../shared/testing/group-mock'; import { MembersListComponent } from './members-list.component'; import { EPersonMock, EPersonMock2 } from '../../../../shared/testing/eperson.mock'; import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; @@ -28,6 +28,8 @@ import { NotificationsServiceStub } from '../../../../shared/testing/notificatio import { RouterMock } from '../../../../shared/mocks/router.mock'; import { PaginationService } from '../../../../core/pagination/pagination.service'; import { PaginationServiceStub } from '../../../../shared/testing/pagination-service.stub'; +import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; +import { DSONameServiceMock } from '../../../../shared/mocks/dso-name.service.mock'; import { UUIDService } from '../../../../core/shared/uuid.service'; import { getMockUUIDService } from '../../../../shared/mocks/uuid.service.mock'; @@ -39,28 +41,26 @@ describe('MembersListComponent', () => { let ePersonDataServiceStub: any; let groupsDataServiceStub: any; let activeGroup; - let allEPersons; - let allGroups; - let epersonMembers; - let subgroupMembers; + let epersonMembers: EPerson[]; + let epersonNonMembers: EPerson[]; let paginationService; beforeEach(waitForAsync(() => { activeGroup = GroupMock; epersonMembers = [EPersonMock2]; - subgroupMembers = [GroupMock2]; - allEPersons = [EPersonMock, EPersonMock2]; - allGroups = [GroupMock, GroupMock2]; + epersonNonMembers = [EPersonMock]; ePersonDataServiceStub = { activeGroup: activeGroup, epersonMembers: epersonMembers, - subgroupMembers: subgroupMembers, - findListByHref(href: string): Observable>> { + epersonNonMembers: epersonNonMembers, + // This method is used to get all the current members + findListByHref(_href: string): Observable>> { return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), groupsDataServiceStub.getEPersonMembers())); }, - searchByScope(scope: string, query: string): Observable>> { + // This method is used to search across *non-members* + searchNonMembers(query: string, group: string): Observable>> { if (query === '') { - return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), allEPersons)); + return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), epersonNonMembers)); } return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])); }, @@ -77,22 +77,22 @@ describe('MembersListComponent', () => { groupsDataServiceStub = { activeGroup: activeGroup, epersonMembers: epersonMembers, - subgroupMembers: subgroupMembers, - allGroups: allGroups, + epersonNonMembers: epersonNonMembers, getActiveGroup(): Observable { return observableOf(activeGroup); }, getEPersonMembers() { return this.epersonMembers; }, - searchGroups(query: string): Observable>> { - if (query === '') { - return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), this.allGroups)); - } - return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])); - }, - addMemberToGroup(parentGroup, eperson: EPerson): Observable { - this.epersonMembers = [...this.epersonMembers, eperson]; + addMemberToGroup(parentGroup, epersonToAdd: EPerson): Observable { + // Add eperson to list of members + this.epersonMembers = [...this.epersonMembers, epersonToAdd]; + // Remove eperson from list of non-members + this.epersonNonMembers.forEach( (eperson: EPerson, index: number) => { + if (eperson.id === epersonToAdd.id) { + this.epersonNonMembers.splice(index, 1); + } + }); return observableOf(new RestResponse(true, 200, 'Success')); }, clearGroupsRequests() { @@ -105,14 +105,14 @@ describe('MembersListComponent', () => { return '/access-control/groups/' + group.id; }, deleteMemberFromGroup(parentGroup, epersonToDelete: EPerson): Observable { - this.epersonMembers = this.epersonMembers.find((eperson: EPerson) => { - if (eperson.id !== epersonToDelete.id) { - return eperson; + // Remove eperson from list of members + this.epersonMembers.forEach( (eperson: EPerson, index: number) => { + if (eperson.id === epersonToDelete.id) { + this.epersonMembers.splice(index, 1); } }); - if (this.epersonMembers === undefined) { - this.epersonMembers = []; - } + // Add eperson to list of non-members + this.epersonNonMembers = [...this.epersonNonMembers, epersonToDelete]; return observableOf(new RestResponse(true, 200, 'Success')); } }; @@ -120,7 +120,7 @@ describe('MembersListComponent', () => { translateService = getMockTranslateService(); paginationService = new PaginationServiceStub(); - TestBed.configureTestingModule({ + return TestBed.configureTestingModule({ imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, TranslateModule.forRoot({ loader: { @@ -137,6 +137,7 @@ describe('MembersListComponent', () => { { provide: FormBuilderService, useValue: builderService }, { provide: Router, useValue: new RouterMock() }, { provide: PaginationService, useValue: paginationService }, + { provide: DSONameService, useValue: new DSONameServiceMock() }, { provide: UUIDService, useValue: getMockUUIDService() } ], schemas: [NO_ERRORS_SCHEMA] @@ -150,6 +151,7 @@ describe('MembersListComponent', () => { }); afterEach(fakeAsync(() => { fixture.destroy(); + fixture.debugElement.nativeElement.remove(); flush(); component = null; fixture.debugElement.nativeElement.remove(); @@ -159,19 +161,43 @@ describe('MembersListComponent', () => { expect(comp).toBeDefined(); })); - it('should show list of eperson members of current active group', () => { - const epersonIdsFound = fixture.debugElement.queryAll(By.css('#ePeopleMembersOfGroup tr td:first-child')); - expect(epersonIdsFound.length).toEqual(1); - epersonMembers.map((eperson: EPerson) => { - expect(epersonIdsFound.find((foundEl) => { - return (foundEl.nativeElement.textContent.trim() === eperson.uuid); - })).toBeTruthy(); + describe('current members list', () => { + it('should show list of eperson members of current active group', () => { + const epersonIdsFound = fixture.debugElement.queryAll(By.css('#ePeopleMembersOfGroup tr td:first-child')); + expect(epersonIdsFound.length).toEqual(1); + epersonMembers.map((eperson: EPerson) => { + expect(epersonIdsFound.find((foundEl) => { + return (foundEl.nativeElement.textContent.trim() === eperson.uuid); + })).toBeTruthy(); + }); + }); + + it('should show a delete button next to each member', () => { + const epersonsFound = fixture.debugElement.queryAll(By.css('#ePeopleMembersOfGroup tbody tr')); + epersonsFound.map((foundEPersonRowElement: DebugElement) => { + const addButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-plus')); + const deleteButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-trash-alt')); + expect(addButton).toBeNull(); + expect(deleteButton).not.toBeNull(); + }); + }); + + describe('if first delete button is pressed', () => { + beforeEach(() => { + const deleteButton: DebugElement = fixture.debugElement.query(By.css('#ePeopleMembersOfGroup tbody .fa-trash-alt')); + deleteButton.nativeElement.click(); + fixture.detectChanges(); + }); + it('then no ePerson remains as a member of the active group.', () => { + const epersonsFound = fixture.debugElement.queryAll(By.css('#ePeopleMembersOfGroup tbody tr')); + expect(epersonsFound.length).toEqual(0); + }); }); }); describe('search', () => { describe('when searching without query', () => { - let epersonsFound; + let epersonsFound: DebugElement[]; beforeEach(fakeAsync(() => { component.search({ scope: 'metadata', query: '' }); tick(); @@ -179,69 +205,34 @@ describe('MembersListComponent', () => { epersonsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr')); })); - it('should display all epersons', () => { - expect(epersonsFound.length).toEqual(2); + it('should display only non-members of the group', () => { + const epersonIdsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr td:first-child')); + expect(epersonIdsFound.length).toEqual(1); + epersonNonMembers.map((eperson: EPerson) => { + expect(epersonIdsFound.find((foundEl) => { + return (foundEl.nativeElement.textContent.trim() === eperson.uuid); + })).toBeTruthy(); + }); }); - describe('if eperson is already a eperson', () => { - it('should have delete button, else it should have add button', () => { - activeGroup.epersons.map((eperson: EPerson) => { - epersonsFound.map((foundEPersonRowElement) => { - if (foundEPersonRowElement.debugElement !== undefined) { - const epersonId = foundEPersonRowElement.debugElement.query(By.css('td:first-child')); - const addButton = foundEPersonRowElement.debugElement.query(By.css('td:last-child .fa-plus')); - const deleteButton = foundEPersonRowElement.debugElement.query(By.css('td:last-child .fa-trash-alt')); - if (epersonId.nativeElement.textContent === eperson.id) { - expect(addButton).toBeUndefined(); - expect(deleteButton).toBeDefined(); - } else { - expect(deleteButton).toBeUndefined(); - expect(addButton).toBeDefined(); - } - } - }); - }); + it('should display an add button next to non-members, not a delete button', () => { + epersonsFound.map((foundEPersonRowElement: DebugElement) => { + const addButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-plus')); + const deleteButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-trash-alt')); + expect(addButton).not.toBeNull(); + expect(deleteButton).toBeNull(); }); }); describe('if first add button is pressed', () => { - beforeEach(fakeAsync(() => { - const addButton = fixture.debugElement.query(By.css('#epersonsSearch tbody .fa-plus')); + beforeEach(() => { + const addButton: DebugElement = fixture.debugElement.query(By.css('#epersonsSearch tbody .fa-plus')); addButton.nativeElement.click(); - tick(); fixture.detectChanges(); - })); - it('all groups in search member of selected group', () => { - epersonsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr')); - expect(epersonsFound.length).toEqual(2); - epersonsFound.map((foundEPersonRowElement) => { - if (foundEPersonRowElement.debugElement !== undefined) { - const addButton = foundEPersonRowElement.debugElement.query(By.css('td:last-child .fa-plus')); - const deleteButton = foundEPersonRowElement.debugElement.query(By.css('td:last-child .fa-trash-alt')); - expect(addButton).toBeUndefined(); - expect(deleteButton).toBeDefined(); - } - }); }); - }); - - describe('if first delete button is pressed', () => { - beforeEach(fakeAsync(() => { - const addButton = fixture.debugElement.query(By.css('#epersonsSearch tbody .fa-trash-alt')); - addButton.nativeElement.click(); - tick(); - fixture.detectChanges(); - })); - it('first eperson in search delete button, because now member', () => { + it('then all (two) ePersons are member of the active group. No non-members left', () => { epersonsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr')); - epersonsFound.map((foundEPersonRowElement) => { - if (foundEPersonRowElement.debugElement !== undefined) { - const addButton = foundEPersonRowElement.debugElement.query(By.css('td:last-child .fa-plus')); - const deleteButton = foundEPersonRowElement.debugElement.query(By.css('td:last-child .fa-trash-alt')); - expect(deleteButton).toBeUndefined(); - expect(addButton).toBeDefined(); - } - }); + expect(epersonsFound.length).toEqual(0); }); }); }); diff --git a/src/app/access-control/group-registry/group-form/members-list/members-list.component.ts b/src/app/access-control/group-registry/group-form/members-list/members-list.component.ts index 81fd511d4a6..1428a44686c 100644 --- a/src/app/access-control/group-registry/group-form/members-list/members-list.component.ts +++ b/src/app/access-control/group-registry/group-form/members-list/members-list.component.ts @@ -1,32 +1,28 @@ import { Component, Input, OnDestroy, OnInit } from '@angular/core'; -import { FormBuilder } from '@angular/forms'; +import { UntypedFormBuilder } from '@angular/forms'; import { Router } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; import { Observable, - of as observableOf, Subscription, - BehaviorSubject, - combineLatest as observableCombineLatest, - ObservedValueOf, + BehaviorSubject } from 'rxjs'; -import { defaultIfEmpty, map, mergeMap, switchMap, take } from 'rxjs/operators'; -import { buildPaginatedList, PaginatedList } from '../../../../core/data/paginated-list.model'; +import { map, switchMap, take } from 'rxjs/operators'; +import { PaginatedList } from '../../../../core/data/paginated-list.model'; import { RemoteData } from '../../../../core/data/remote-data'; import { EPersonDataService } from '../../../../core/eperson/eperson-data.service'; import { GroupDataService } from '../../../../core/eperson/group-data.service'; import { EPerson } from '../../../../core/eperson/models/eperson.model'; import { Group } from '../../../../core/eperson/models/group.model'; import { - getFirstSucceededRemoteData, getFirstCompletedRemoteData, getAllCompletedRemoteData, getRemoteDataPayload } from '../../../../core/shared/operators'; import { NotificationsService } from '../../../../shared/notifications/notifications.service'; import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model'; -import { EpersonDtoModel } from '../../../../core/eperson/models/eperson-dto.model'; import { PaginationService } from '../../../../core/pagination/pagination.service'; +import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; import { UUIDService } from '../../../../core/shared/uuid.service'; /** @@ -34,8 +30,8 @@ import { UUIDService } from '../../../../core/shared/uuid.service'; */ enum SubKey { ActiveGroup, - MembersDTO, - SearchResultsDTO, + Members, + SearchResults, } /** @@ -96,11 +92,11 @@ export class MembersListComponent implements OnInit, OnDestroy { /** * EPeople being displayed in search result, initially all members, after search result of search */ - ePeopleSearchDtos: BehaviorSubject> = new BehaviorSubject>(undefined); + ePeopleSearch: BehaviorSubject> = new BehaviorSubject>(undefined); /** * List of EPeople members of currently active group being edited */ - ePeopleMembersOfGroupDtos: BehaviorSubject> = new BehaviorSubject>(undefined); + ePeopleMembersOfGroup: BehaviorSubject> = new BehaviorSubject>(undefined); /** * Pagination config used to display the list of EPeople that are result of EPeople search @@ -129,7 +125,6 @@ export class MembersListComponent implements OnInit, OnDestroy { // Current search in edit group - epeople search form currentSearchQuery: string; - currentSearchScope: string; // Whether or not user has done a EPeople search yet searchDone: boolean; @@ -142,24 +137,24 @@ export class MembersListComponent implements OnInit, OnDestroy { public ePersonDataService: EPersonDataService, protected translateService: TranslateService, protected notificationsService: NotificationsService, - protected formBuilder: FormBuilder, + protected formBuilder: UntypedFormBuilder, protected paginationService: PaginationService, - private router: Router, + protected router: Router, + public dsoNameService: DSONameService, protected uuidService: UUIDService ) { this.currentSearchQuery = ''; - this.currentSearchScope = 'metadata'; } ngOnInit(): void { this.searchForm = this.formBuilder.group(({ - scope: 'metadata', query: '', })); this.subs.set(SubKey.ActiveGroup, this.groupDataService.getActiveGroup().subscribe((activeGroup: Group) => { if (activeGroup != null) { this.groupBeingEdited = activeGroup; this.retrieveMembers(this.config.currentPage); + this.search({query: ''}); } })); } @@ -171,8 +166,8 @@ export class MembersListComponent implements OnInit, OnDestroy { * @private */ retrieveMembers(page: number): void { - this.unsubFrom(SubKey.MembersDTO); - this.subs.set(SubKey.MembersDTO, + this.unsubFrom(SubKey.Members); + this.subs.set(SubKey.Members, this.paginationService.getCurrentPagination(this.config.id, this.config).pipe( switchMap((currentPagination) => { return this.ePersonDataService.findListByHref(this.groupBeingEdited._links.epersons.href, { @@ -189,49 +184,12 @@ export class MembersListComponent implements OnInit, OnDestroy { return rd; } }), - switchMap((epersonListRD: RemoteData>) => { - const dtos$ = observableCombineLatest([...epersonListRD.payload.page.map((member: EPerson) => { - const dto$: Observable = observableCombineLatest( - this.isMemberOfGroup(member), (isMember: ObservedValueOf>) => { - const epersonDtoModel: EpersonDtoModel = new EpersonDtoModel(); - epersonDtoModel.eperson = member; - epersonDtoModel.memberOfGroup = isMember; - return epersonDtoModel; - }); - return dto$; - })]); - return dtos$.pipe(defaultIfEmpty([]), map((dtos: EpersonDtoModel[]) => { - return buildPaginatedList(epersonListRD.payload.pageInfo, dtos); - })); - })) - .subscribe((paginatedListOfDTOs: PaginatedList) => { - this.ePeopleMembersOfGroupDtos.next(paginatedListOfDTOs); + getRemoteDataPayload()) + .subscribe((paginatedListOfEPersons: PaginatedList) => { + this.ePeopleMembersOfGroup.next(paginatedListOfEPersons); })); } - /** - * 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 - */ - isMemberOfGroup(possibleMember: EPerson): Observable { - return this.groupDataService.getActiveGroup().pipe(take(1), - mergeMap((group: Group) => { - if (group != null) { - return this.ePersonDataService.findListByHref(group._links.epersons.href, { - currentPage: 1, - elementsPerPage: 9999 - }) - .pipe( - getFirstSucceededRemoteData(), - getRemoteDataPayload(), - map((listEPeopleInGroup: PaginatedList) => listEPeopleInGroup.page.filter((ePersonInList: EPerson) => ePersonInList.id === possibleMember.id)), - map((epeople: EPerson[]) => epeople.length > 0)); - } else { - return observableOf(false); - } - })); - } - /** * Unsubscribe from a subscription if it's still subscribed, and remove it from the map of * active subscriptions @@ -248,13 +206,18 @@ export class MembersListComponent implements OnInit, OnDestroy { /** * Deletes a given EPerson from the members list of the group currently being edited - * @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: EPerson) { this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => { if (activeGroup != null) { - const response = this.groupDataService.deleteMemberFromGroup(activeGroup, ePerson.eperson); - this.showNotifications('deleteMember', response, ePerson.eperson.name, activeGroup); + const response = this.groupDataService.deleteMemberFromGroup(activeGroup, eperson); + this.showNotifications('deleteMember', response, this.dsoNameService.getName(eperson), activeGroup); + // Reload search results (if there is an active query). + // This will potentially add this deleted subgroup into the list of search results. + if (this.currentSearchQuery != null) { + this.search({query: this.currentSearchQuery}); + } } else { this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup')); } @@ -263,14 +226,18 @@ export class MembersListComponent implements OnInit, OnDestroy { /** * Adds a given EPerson to the members list of the group currently being edited - * @param ePerson EPerson we want to add as member to group that is currently being edited + * @param eperson EPerson we want to add as member to group that is currently being edited */ - addMemberToGroup(ePerson: EpersonDtoModel) { - ePerson.memberOfGroup = true; + addMemberToGroup(eperson: EPerson) { this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => { if (activeGroup != null) { - const response = this.groupDataService.addMemberToGroup(activeGroup, ePerson.eperson); - this.showNotifications('addMember', response, ePerson.eperson.name, activeGroup); + const response = this.groupDataService.addMemberToGroup(activeGroup, eperson); + this.showNotifications('addMember', response, this.dsoNameService.getName(eperson), activeGroup); + // Reload search results (if there is an active query). + // This will potentially add this deleted subgroup into the list of search results. + if (this.currentSearchQuery != null) { + this.search({query: this.currentSearchQuery}); + } } else { this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup')); } @@ -278,37 +245,25 @@ export class MembersListComponent implements OnInit, OnDestroy { } /** - * Search in the EPeople by name, email or metadata - * @param data Contains scope and query param + * Search all EPeople who are NOT a member of the current group by name, email or metadata + * @param data Contains query param */ search(data: any) { - this.unsubFrom(SubKey.SearchResultsDTO); - this.subs.set(SubKey.SearchResultsDTO, + this.unsubFrom(SubKey.SearchResults); + this.subs.set(SubKey.SearchResults, this.paginationService.getCurrentPagination(this.configSearch.id, this.configSearch).pipe( switchMap((paginationOptions) => { - const query: string = data.query; - const scope: string = data.scope; if (query != null && this.currentSearchQuery !== query && this.groupBeingEdited) { - this.router.navigate([], { - queryParamsHandling: 'merge' - }); this.currentSearchQuery = query; this.paginationService.resetPage(this.configSearch.id); } - if (scope != null && this.currentSearchScope !== scope && this.groupBeingEdited) { - this.router.navigate([], { - queryParamsHandling: 'merge' - }); - this.currentSearchScope = scope; - this.paginationService.resetPage(this.configSearch.id); - } this.searchDone = true; - return this.ePersonDataService.searchByScope(this.currentSearchScope, this.currentSearchQuery, { + return this.ePersonDataService.searchNonMembers(this.currentSearchQuery, this.groupBeingEdited.id, { currentPage: paginationOptions.currentPage, elementsPerPage: paginationOptions.pageSize - }); + }, false, true); }), getAllCompletedRemoteData(), map((rd: RemoteData) => { @@ -318,23 +273,9 @@ export class MembersListComponent implements OnInit, OnDestroy { return rd; } }), - switchMap((epersonListRD: RemoteData>) => { - const dtos$ = observableCombineLatest([...epersonListRD.payload.page.map((member: EPerson) => { - const dto$: Observable = observableCombineLatest( - this.isMemberOfGroup(member), (isMember: ObservedValueOf>) => { - const epersonDtoModel: EpersonDtoModel = new EpersonDtoModel(); - epersonDtoModel.eperson = member; - epersonDtoModel.memberOfGroup = isMember; - return epersonDtoModel; - }); - return dto$; - })]); - return dtos$.pipe(defaultIfEmpty([]), map((dtos: EpersonDtoModel[]) => { - return buildPaginatedList(epersonListRD.payload.pageInfo, dtos); - })); - })) - .subscribe((paginatedListOfDTOs: PaginatedList) => { - this.ePeopleSearchDtos.next(paginatedListOfDTOs); + getRemoteDataPayload()) + .subscribe((paginatedListOfEPersons: PaginatedList) => { + this.ePeopleSearch.next(paginatedListOfEPersons); })); } diff --git a/src/app/access-control/group-registry/group-form/subgroup-list/subgroups-list.component.html b/src/app/access-control/group-registry/group-form/subgroup-list/subgroups-list.component.html index 3be45c44521..85fe8974edd 100644 --- a/src/app/access-control/group-registry/group-form/subgroup-list/subgroups-list.component.html +++ b/src/app/access-control/group-registry/group-form/subgroup-list/subgroups-list.component.html @@ -1,6 +1,55 @@

    {{messagePrefix + '.head' | translate}}

    +

    {{messagePrefix + '.headSubgroups' | translate}}

    + + + +
    + + + + + + + + + + + + + + + + + +
    {{messagePrefix + '.table.id' | translate}}{{messagePrefix + '.table.name' | translate}}{{messagePrefix + '.table.collectionOrCommunity' | translate}}{{messagePrefix + '.table.edit' | translate}}
    {{group.id}} + + {{ dsoNameService.getName(group) }} + + {{ dsoNameService.getName((group.object | async)?.payload)}} +
    + +
    +
    +
    +
    + + +
    {{group.id}}{{group.name}}{{(group.object | async)?.payload?.name}} + + {{ dsoNameService.getName(group) }} + + {{ dsoNameService.getName((group.object | async)?.payload) }}
    - - -

    {{ messagePrefix + '.table.edit.currentGroup' | translate }}

    - -
    @@ -86,49 +129,4 @@

    {{messagePrefix + '.headSubgroups' | translate}}

    - - - -
    - - - - - - - - - - - - - - - - - -
    {{messagePrefix + '.table.id' | translate}}{{messagePrefix + '.table.name' | translate}}{{messagePrefix + '.table.collectionOrCommunity' | translate}}{{messagePrefix + '.table.edit' | translate}}
    {{group.id}}{{group.name}}{{(group.object | async)?.payload?.name}} -
    - -
    -
    -
    -
    - - - diff --git a/src/app/access-control/group-registry/group-form/subgroup-list/subgroups-list.component.spec.ts b/src/app/access-control/group-registry/group-form/subgroup-list/subgroups-list.component.spec.ts index bd7e543a218..db768f2f41f 100644 --- a/src/app/access-control/group-registry/group-form/subgroup-list/subgroups-list.component.spec.ts +++ b/src/app/access-control/group-registry/group-form/subgroup-list/subgroups-list.component.spec.ts @@ -1,20 +1,12 @@ import { CommonModule } from '@angular/common'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { - ComponentFixture, - fakeAsync, - flush, - inject, - TestBed, - tick, - waitForAsync -} from '@angular/core/testing'; +import { NO_ERRORS_SCHEMA, DebugElement } from '@angular/core'; +import { ComponentFixture, fakeAsync, flush, inject, TestBed, waitForAsync } from '@angular/core/testing'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { BrowserModule, By } from '@angular/platform-browser'; import { Router } from '@angular/router'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core'; -import { Observable, of as observableOf, BehaviorSubject } from 'rxjs'; +import { Observable, of as observableOf } from 'rxjs'; import { RestResponse } from '../../../../core/cache/response.models'; import { buildPaginatedList, PaginatedList } from '../../../../core/data/paginated-list.model'; import { RemoteData } from '../../../../core/data/remote-data'; @@ -26,17 +18,18 @@ import { NotificationsService } from '../../../../shared/notifications/notificat import { GroupMock, GroupMock2 } from '../../../../shared/testing/group-mock'; import { SubgroupsListComponent } from './subgroups-list.component'; import { - createSuccessfulRemoteDataObject$, - createSuccessfulRemoteDataObject + createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; import { RouterMock } from '../../../../shared/mocks/router.mock'; import { getMockFormBuilderService } from '../../../../shared/mocks/form-builder-service.mock'; import { getMockTranslateService } from '../../../../shared/mocks/translate.service.mock'; import { TranslateLoaderMock } from '../../../../shared/testing/translate-loader.mock'; import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service.stub'; -import { map } from 'rxjs/operators'; import { PaginationService } from '../../../../core/pagination/pagination.service'; import { PaginationServiceStub } from '../../../../shared/testing/pagination-service.stub'; +import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; +import { DSONameServiceMock } from '../../../../shared/mocks/dso-name.service.mock'; +import { EPersonMock2 } from 'src/app/shared/testing/eperson.mock'; import { UUIDService } from '../../../../core/shared/uuid.service'; import { getMockUUIDService } from '../../../../shared/mocks/uuid.service.mock'; @@ -47,44 +40,70 @@ describe('SubgroupsListComponent', () => { let builderService: FormBuilderService; let ePersonDataServiceStub: any; let groupsDataServiceStub: any; - let activeGroup; - let subgroups; - let allGroups; + let activeGroup: Group; + let subgroups: Group[]; + let groupNonMembers: Group[]; let routerStub; let paginationService; + // Define a new mock activegroup for all tests below + let mockActiveGroup: Group = Object.assign(new Group(), { + handle: null, + subgroups: [GroupMock2], + epersons: [EPersonMock2], + selfRegistered: false, + permanent: false, + _links: { + self: { + href: 'https://rest.api/server/api/eperson/groups/activegroupid', + }, + subgroups: { href: 'https://rest.api/server/api/eperson/groups/activegroupid/subgroups' }, + object: { href: 'https://rest.api/server/api/eperson/groups/activegroupid/object' }, + epersons: { href: 'https://rest.api/server/api/eperson/groups/activegroupid/epersons' } + }, + _name: 'activegroupname', + id: 'activegroupid', + uuid: 'activegroupid', + type: 'group', + }); beforeEach(waitForAsync(() => { - activeGroup = GroupMock; + activeGroup = mockActiveGroup; subgroups = [GroupMock2]; - allGroups = [GroupMock, GroupMock2]; + groupNonMembers = [GroupMock]; ePersonDataServiceStub = {}; groupsDataServiceStub = { activeGroup: activeGroup, - subgroups$: new BehaviorSubject(subgroups), + subgroups: subgroups, + groupNonMembers: groupNonMembers, getActiveGroup(): Observable { return observableOf(this.activeGroup); }, getSubgroups(): Group { - return this.activeGroup; + return this.subgroups; }, - findListByHref(href: string): Observable>> { - return this.subgroups$.pipe( - map((currentGroups: Group[]) => { - return createSuccessfulRemoteDataObject(buildPaginatedList(new PageInfo(), currentGroups)); - }) - ); + // This method is used to get all the current subgroups + findListByHref(_href: string): Observable>> { + return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), groupsDataServiceStub.getSubgroups())); }, getGroupEditPageRouterLink(group: Group): string { return '/access-control/groups/' + group.id; }, - searchGroups(query: string): Observable>> { + // This method is used to get all groups which are NOT currently a subgroup member + searchNonMemberGroups(query: string, group: string): Observable>> { if (query === '') { - return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), allGroups)); + return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), groupNonMembers)); } return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])); }, - addSubGroupToGroup(parentGroup, subgroup: Group): Observable { - this.subgroups$.next([...this.subgroups$.getValue(), subgroup]); + addSubGroupToGroup(parentGroup, subgroupToAdd: Group): Observable { + // Add group to list of subgroups + this.subgroups = [...this.subgroups, subgroupToAdd]; + // Remove group from list of non-members + this.groupNonMembers.forEach( (group: Group, index: number) => { + if (group.id === subgroupToAdd.id) { + this.groupNonMembers.splice(index, 1); + } + }); return observableOf(new RestResponse(true, 200, 'Success')); }, clearGroupsRequests() { @@ -93,12 +112,15 @@ describe('SubgroupsListComponent', () => { clearGroupLinkRequests() { // empty }, - deleteSubGroupFromGroup(parentGroup, subgroup: Group): Observable { - this.subgroups$.next(this.subgroups$.getValue().filter((group: Group) => { - if (group.id !== subgroup.id) { - return group; + deleteSubGroupFromGroup(parentGroup, subgroupToDelete: Group): Observable { + // Remove group from list of subgroups + this.subgroups.forEach( (group: Group, index: number) => { + if (group.id === subgroupToDelete.id) { + this.subgroups.splice(index, 1); } - })); + }); + // Add group to list of non-members + this.groupNonMembers = [...this.groupNonMembers, subgroupToDelete]; return observableOf(new RestResponse(true, 200, 'Success')); } }; @@ -107,7 +129,7 @@ describe('SubgroupsListComponent', () => { translateService = getMockTranslateService(); paginationService = new PaginationServiceStub(); - TestBed.configureTestingModule({ + return TestBed.configureTestingModule({ imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, TranslateModule.forRoot({ loader: { @@ -118,6 +140,7 @@ describe('SubgroupsListComponent', () => { ], declarations: [SubgroupsListComponent], providers: [SubgroupsListComponent, + { provide: DSONameService, useValue: new DSONameServiceMock() }, { provide: GroupDataService, useValue: groupsDataServiceStub }, { provide: NotificationsService, useValue: new NotificationsServiceStub() }, { provide: FormBuilderService, useValue: builderService }, @@ -136,6 +159,7 @@ describe('SubgroupsListComponent', () => { }); afterEach(fakeAsync(() => { fixture.destroy(); + fixture.debugElement.nativeElement.remove(); flush(); component = null; })); @@ -144,86 +168,78 @@ describe('SubgroupsListComponent', () => { expect(comp).toBeDefined(); })); - it('should show list of subgroups of current active group', () => { - const groupIdsFound = fixture.debugElement.queryAll(By.css('#subgroupsOfGroup tr td:first-child')); - expect(groupIdsFound.length).toEqual(1); - activeGroup.subgroups.map((group: Group) => { - expect(groupIdsFound.find((foundEl) => { - return (foundEl.nativeElement.textContent.trim() === group.uuid); - })).toBeTruthy(); + describe('current subgroup list', () => { + it('should show list of subgroups of current active group', () => { + const groupIdsFound = fixture.debugElement.queryAll(By.css('#subgroupsOfGroup tr td:first-child')); + expect(groupIdsFound.length).toEqual(1); + subgroups.map((group: Group) => { + expect(groupIdsFound.find((foundEl) => { + return (foundEl.nativeElement.textContent.trim() === group.uuid); + })).toBeTruthy(); + }); }); - }); - describe('if first group delete button is pressed', () => { - let groupsFound; - beforeEach(fakeAsync(() => { - const addButton = fixture.debugElement.query(By.css('#subgroupsOfGroup tbody .deleteButton')); - addButton.triggerEventHandler('click', { - preventDefault: () => {/**/ - } + it('should show a delete button next to each subgroup', () => { + const subgroupsFound = fixture.debugElement.queryAll(By.css('#subgroupsOfGroup tbody tr')); + subgroupsFound.map((foundGroupRowElement: DebugElement) => { + const addButton: DebugElement = foundGroupRowElement.query(By.css('td:last-child .fa-plus')); + const deleteButton: DebugElement = foundGroupRowElement.query(By.css('td:last-child .fa-trash-alt')); + expect(addButton).toBeNull(); + expect(deleteButton).not.toBeNull(); + }); + }); + + describe('if first group delete button is pressed', () => { + let groupsFound: DebugElement[]; + beforeEach(() => { + const deleteButton = fixture.debugElement.query(By.css('#subgroupsOfGroup tbody .deleteButton')); + deleteButton.nativeElement.click(); + fixture.detectChanges(); + }); + it('then no subgroup remains as a member of the active group', () => { + groupsFound = fixture.debugElement.queryAll(By.css('#subgroupsOfGroup tbody tr')); + expect(groupsFound.length).toEqual(0); }); - tick(); - fixture.detectChanges(); - })); - it('one less subgroup in list from 1 to 0 (of 2 total groups)', () => { - groupsFound = fixture.debugElement.queryAll(By.css('#subgroupsOfGroup tbody tr')); - expect(groupsFound.length).toEqual(0); }); }); describe('search', () => { describe('when searching with empty query', () => { - let groupsFound; + let groupsFound: DebugElement[]; beforeEach(fakeAsync(() => { component.search({ query: '' }); + fixture.detectChanges(); groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr')); })); - it('should display all groups', () => { - fixture.detectChanges(); - groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr')); - expect(groupsFound.length).toEqual(2); - groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr')); - const groupIdsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr td:first-child')); - allGroups.map((group: Group) => { - expect(groupIdsFound.find((foundEl) => { + it('should display only non-member groups (i.e. groups that are not a subgroup)', () => { + const groupIdsFound: DebugElement[] = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr td:first-child')); + expect(groupIdsFound.length).toEqual(1); + groupNonMembers.map((group: Group) => { + expect(groupIdsFound.find((foundEl: DebugElement) => { return (foundEl.nativeElement.textContent.trim() === group.uuid); })).toBeTruthy(); }); }); - describe('if group is already a subgroup', () => { - it('should have delete button, else it should have add button', () => { + it('should display an add button next to non-member groups, not a delete button', () => { + groupsFound.map((foundGroupRowElement: DebugElement) => { + const addButton: DebugElement = foundGroupRowElement.query(By.css('td:last-child .fa-plus')); + const deleteButton: DebugElement = foundGroupRowElement.query(By.css('td:last-child .fa-trash-alt')); + expect(addButton).not.toBeNull(); + expect(deleteButton).toBeNull(); + }); + }); + + describe('if first add button is pressed', () => { + beforeEach(() => { + const addButton: DebugElement = fixture.debugElement.query(By.css('#groupsSearch tbody .fa-plus')); + addButton.nativeElement.click(); fixture.detectChanges(); + }); + it('then all (two) Groups are subgroups of the active group. No non-members left', () => { groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr')); - const getSubgroups = groupsDataServiceStub.getSubgroups().subgroups; - if (getSubgroups !== undefined && getSubgroups.length > 0) { - groupsFound.map((foundGroupRowElement) => { - if (foundGroupRowElement.debugElement !== undefined) { - const addButton = foundGroupRowElement.debugElement.query(By.css('td:last-child .fa-plus')); - const deleteButton = foundGroupRowElement.debugElement.query(By.css('td:last-child .fa-trash-alt')); - expect(addButton).toBeUndefined(); - expect(deleteButton).toBeDefined(); - } - }); - } else { - getSubgroups.map((group: Group) => { - groupsFound.map((foundGroupRowElement) => { - if (foundGroupRowElement.debugElement !== undefined) { - const groupId = foundGroupRowElement.debugElement.query(By.css('td:first-child')); - const addButton = foundGroupRowElement.debugElement.query(By.css('td:last-child .fa-plus')); - const deleteButton = foundGroupRowElement.debugElement.query(By.css('td:last-child .fa-trash-alt')); - if (groupId.nativeElement.textContent === group.id) { - expect(addButton).toBeUndefined(); - expect(deleteButton).toBeDefined(); - } else { - expect(deleteButton).toBeUndefined(); - expect(addButton).toBeDefined(); - } - } - }); - }); - } + expect(groupsFound.length).toEqual(0); }); }); }); diff --git a/src/app/access-control/group-registry/group-form/subgroup-list/subgroups-list.component.ts b/src/app/access-control/group-registry/group-form/subgroup-list/subgroups-list.component.ts index cbe230c2676..0eda80867db 100644 --- a/src/app/access-control/group-registry/group-form/subgroup-list/subgroups-list.component.ts +++ b/src/app/access-control/group-registry/group-form/subgroup-list/subgroups-list.component.ts @@ -1,23 +1,23 @@ import { Component, Input, OnDestroy, OnInit } from '@angular/core'; -import { FormBuilder } from '@angular/forms'; +import { UntypedFormBuilder } from '@angular/forms'; import { Router } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; -import { BehaviorSubject, Observable, of as observableOf, Subscription } from 'rxjs'; -import { map, mergeMap, switchMap, take } from 'rxjs/operators'; +import { BehaviorSubject, Observable, Subscription } from 'rxjs'; +import { map, switchMap, take } from 'rxjs/operators'; import { PaginatedList } from '../../../../core/data/paginated-list.model'; import { RemoteData } from '../../../../core/data/remote-data'; import { GroupDataService } from '../../../../core/eperson/group-data.service'; import { Group } from '../../../../core/eperson/models/group.model'; import { - getFirstCompletedRemoteData, - getFirstSucceededRemoteData, - getRemoteDataPayload + getAllCompletedRemoteData, + getFirstCompletedRemoteData } from '../../../../core/shared/operators'; import { NotificationsService } from '../../../../shared/notifications/notifications.service'; import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model'; import { NoContent } from '../../../../core/shared/NoContent.model'; import { PaginationService } from '../../../../core/pagination/pagination.service'; import { followLink } from '../../../../shared/utils/follow-link-config.model'; +import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; import { UUIDService } from '../../../../core/shared/uuid.service'; /** @@ -87,9 +87,10 @@ export class SubgroupsListComponent implements OnInit, OnDestroy { constructor(public groupDataService: GroupDataService, private translateService: TranslateService, private notificationsService: NotificationsService, - private formBuilder: FormBuilder, + private formBuilder: UntypedFormBuilder, private paginationService: PaginationService, private router: Router, + public dsoNameService: DSONameService, private uuidService: UUIDService) { this.currentSearchQuery = ''; } @@ -102,6 +103,7 @@ export class SubgroupsListComponent implements OnInit, OnDestroy { if (activeGroup != null) { this.groupBeingEdited = activeGroup; this.retrieveSubGroups(); + this.search({query: ''}); } })); } @@ -130,47 +132,6 @@ export class SubgroupsListComponent implements OnInit, OnDestroy { })); } - /** - * Whether or not the given group is a subgroup of the group currently being edited - * @param possibleSubgroup Group that is a possible subgroup (being tested) of the group currently being edited - */ - isSubgroupOfGroup(possibleSubgroup: Group): Observable { - return this.groupDataService.getActiveGroup().pipe(take(1), - mergeMap((activeGroup: Group) => { - if (activeGroup != null) { - if (activeGroup.uuid === possibleSubgroup.uuid) { - return observableOf(false); - } else { - return this.groupDataService.findListByHref(activeGroup._links.subgroups.href, { - currentPage: 1, - elementsPerPage: 9999 - }) - .pipe( - getFirstSucceededRemoteData(), - getRemoteDataPayload(), - map((listTotalGroups: PaginatedList) => listTotalGroups.page.filter((groupInList: Group) => groupInList.id === possibleSubgroup.id)), - map((groups: Group[]) => groups.length > 0)); - } - } else { - return observableOf(false); - } - })); - } - - /** - * Whether or not the given group is the current group being edited - * @param group Group that is possibly the current group being edited - */ - isActiveGroup(group: Group): Observable { - return this.groupDataService.getActiveGroup().pipe(take(1), - mergeMap((activeGroup: Group) => { - if (activeGroup != null && activeGroup.uuid === group.uuid) { - return observableOf(true); - } - return observableOf(false); - })); - } - /** * Deletes given subgroup from the group currently being edited * @param subgroup Group we want to delete from the subgroups of the group currently being edited @@ -179,7 +140,12 @@ export class SubgroupsListComponent implements OnInit, OnDestroy { this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => { if (activeGroup != null) { const response = this.groupDataService.deleteSubGroupFromGroup(activeGroup, subgroup); - this.showNotifications('deleteSubgroup', response, subgroup.name, activeGroup); + this.showNotifications('deleteSubgroup', response, this.dsoNameService.getName(subgroup), activeGroup); + // Reload search results (if there is an active query). + // This will potentially add this deleted subgroup into the list of search results. + if (this.currentSearchQuery != null) { + this.search({query: this.currentSearchQuery}); + } } else { this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup')); } @@ -195,7 +161,12 @@ export class SubgroupsListComponent implements OnInit, OnDestroy { if (activeGroup != null) { if (activeGroup.uuid !== subgroup.uuid) { const response = this.groupDataService.addSubGroupToGroup(activeGroup, subgroup); - this.showNotifications('addSubgroup', response, subgroup.name, activeGroup); + this.showNotifications('addSubgroup', response, this.dsoNameService.getName(subgroup), activeGroup); + // Reload search results (if there is an active query). + // This will potentially remove this added subgroup from search results. + if (this.currentSearchQuery != null) { + this.search({query: this.currentSearchQuery}); + } } else { this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.subgroupToAddIsActiveGroup')); } @@ -206,28 +177,38 @@ export class SubgroupsListComponent implements OnInit, OnDestroy { } /** - * Search in the groups (searches by group name and by uuid exact match) + * Search all non-member groups (searches by group name and by uuid exact match). Used to search for + * groups that could be added to current group as a subgroup. * @param data Contains query param */ search(data: any) { - const query: string = data.query; - if (query != null && this.currentSearchQuery !== query) { - this.router.navigateByUrl(this.groupDataService.getGroupEditPageRouterLink(this.groupBeingEdited)); - this.currentSearchQuery = query; - this.configSearch.currentPage = 1; - } - this.searchDone = true; - this.unsubFrom(SubKey.SearchResults); - this.subs.set(SubKey.SearchResults, this.paginationService.getCurrentPagination(this.configSearch.id, this.configSearch).pipe( - switchMap((config) => this.groupDataService.searchGroups(this.currentSearchQuery, { - currentPage: config.currentPage, - elementsPerPage: config.pageSize - }, true, true, followLink('object') - )) - ).subscribe((rd: RemoteData>) => { - this.searchResults$.next(rd); - })); + this.subs.set(SubKey.SearchResults, + this.paginationService.getCurrentPagination(this.configSearch.id, this.configSearch).pipe( + switchMap((paginationOptions) => { + const query: string = data.query; + if (query != null && this.currentSearchQuery !== query && this.groupBeingEdited) { + this.currentSearchQuery = query; + this.paginationService.resetPage(this.configSearch.id); + } + this.searchDone = true; + + return this.groupDataService.searchNonMemberGroups(this.currentSearchQuery, this.groupBeingEdited.id, { + currentPage: paginationOptions.currentPage, + elementsPerPage: paginationOptions.pageSize + }, false, true, followLink('object')); + }), + getAllCompletedRemoteData(), + map((rd: RemoteData) => { + if (rd.hasFailed) { + this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure', { cause: rd.errorMessage })); + } else { + return rd; + } + })) + .subscribe((rd: RemoteData>) => { + this.searchResults$.next(rd); + })); } /** diff --git a/src/app/access-control/group-registry/groups-registry.component.html b/src/app/access-control/group-registry/groups-registry.component.html index ebbd223599c..27cec262c44 100644 --- a/src/app/access-control/group-registry/groups-registry.component.html +++ b/src/app/access-control/group-registry/groups-registry.component.html @@ -5,9 +5,9 @@
    @@ -17,7 +17,7 @@
    {{groupDto.group.id}}{{groupDto.group.name}}{{(groupDto.group.object | async)?.payload?.name}}{{ dsoNameService.getName(groupDto.group) }}{{ dsoNameService.getName((groupDto.group.object | async)?.payload) }} {{groupDto.epersons?.totalElements + groupDto.subgroups?.totalElements}}
    @@ -65,7 +65,7 @@
    diff --git a/src/app/access-control/group-registry/groups-registry.component.spec.ts b/src/app/access-control/group-registry/groups-registry.component.spec.ts index b421d950e1f..6312e5abc59 100644 --- a/src/app/access-control/group-registry/groups-registry.component.spec.ts +++ b/src/app/access-control/group-registry/groups-registry.component.spec.ts @@ -32,10 +32,12 @@ import { PaginationService } from '../../core/pagination/pagination.service'; import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub'; import { FeatureID } from '../../core/data/feature-authorization/feature-id'; import { NoContent } from '../../core/shared/NoContent.model'; +import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; +import { DSONameServiceMock, UNDEFINED_NAME } from '../../shared/mocks/dso-name.service.mock'; import { UUIDService } from '../../core/shared/uuid.service'; import { getMockUUIDService } from '../../shared/mocks/uuid.service.mock'; -describe('GroupRegistryComponent', () => { +describe('GroupsRegistryComponent', () => { let component: GroupsRegistryComponent; let fixture: ComponentFixture; let ePersonDataServiceStub: any; @@ -162,7 +164,7 @@ describe('GroupRegistryComponent', () => { authorizationService = jasmine.createSpyObj('authorizationService', ['isAuthorized']); setIsAuthorized(true, true); paginationService = new PaginationServiceStub(); - TestBed.configureTestingModule({ + return TestBed.configureTestingModule({ imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, TranslateModule.forRoot({ loader: { @@ -173,6 +175,7 @@ describe('GroupRegistryComponent', () => { ], declarations: [GroupsRegistryComponent], providers: [GroupsRegistryComponent, + { provide: DSONameService, useValue: new DSONameServiceMock() }, { provide: EPersonDataService, useValue: ePersonDataServiceStub }, { provide: GroupDataService, useValue: groupsDataServiceStub }, { provide: DSpaceObjectDataService, useValue: dsoDataServiceStub }, @@ -211,7 +214,7 @@ describe('GroupRegistryComponent', () => { it('should display community/collection name if present', () => { const collectionNamesFound = fixture.debugElement.queryAll(By.css('#groups tr td:nth-child(3)')); expect(collectionNamesFound.length).toEqual(2); - expect(collectionNamesFound[0].nativeElement.textContent).toEqual(''); + expect(collectionNamesFound[0].nativeElement.textContent).toEqual(UNDEFINED_NAME); expect(collectionNamesFound[1].nativeElement.textContent).toEqual('testgroupid2objectName'); }); diff --git a/src/app/access-control/group-registry/groups-registry.component.ts b/src/app/access-control/group-registry/groups-registry.component.ts index 62c14309366..d25025b393e 100644 --- a/src/app/access-control/group-registry/groups-registry.component.ts +++ b/src/app/access-control/group-registry/groups-registry.component.ts @@ -1,5 +1,5 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; -import { FormBuilder } from '@angular/forms'; +import { UntypedFormBuilder } from '@angular/forms'; import { Router } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; import { @@ -37,6 +37,7 @@ import { PaginationComponentOptions } from '../../shared/pagination/pagination-c import { NoContent } from '../../core/shared/NoContent.model'; import { PaginationService } from '../../core/pagination/pagination.service'; import { followLink } from '../../shared/utils/follow-link-config.model'; +import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { UUIDService } from '../../core/shared/uuid.service'; @Component({ @@ -100,13 +101,14 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy { private dSpaceObjectDataService: DSpaceObjectDataService, private translateService: TranslateService, private notificationsService: NotificationsService, - private formBuilder: FormBuilder, + private formBuilder: UntypedFormBuilder, protected routeService: RouteService, private router: Router, private authorizationService: AuthorizationDataService, private paginationService: PaginationService, - private uuidService: UUIDService, - public requestService: RequestService) { + public requestService: RequestService, + public dsoNameService: DSONameService, + private uuidService: UUIDService) { this.currentSearchQuery = ''; this.searchForm = this.formBuilder.group(({ query: this.currentSearchQuery, @@ -203,10 +205,10 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy { .subscribe((rd: RemoteData) => { if (rd.hasSucceeded) { this.deletedGroupsIds = [...this.deletedGroupsIds, group.group.id]; - this.notificationsService.success(this.translateService.get(this.messagePrefix + 'notification.deleted.success', { name: group.group.name })); + this.notificationsService.success(this.translateService.get(this.messagePrefix + 'notification.deleted.success', { name: this.dsoNameService.getName(group.group) })); } else { this.notificationsService.error( - this.translateService.get(this.messagePrefix + 'notification.deleted.failure.title', { name: group.group.name }), + this.translateService.get(this.messagePrefix + 'notification.deleted.failure.title', { name: this.dsoNameService.getName(group.group) }), this.translateService.get(this.messagePrefix + 'notification.deleted.failure.content', { cause: rd.errorMessage })); } }); @@ -215,18 +217,28 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy { /** * Get the members (epersons embedded value of a group) + * NOTE: At this time we only grab the *first* member in order to receive the `totalElements` value + * needed for our HTML template. * @param group */ getMembers(group: Group): Observable>> { - return this.ePersonDataService.findListByHref(group._links.epersons.href).pipe(getFirstSucceededRemoteData()); + return this.ePersonDataService.findListByHref(group._links.epersons.href, { + currentPage: 1, + elementsPerPage: 1, + }).pipe(getFirstSucceededRemoteData()); } /** * Get the subgroups (groups embedded value of a group) + * NOTE: At this time we only grab the *first* subgroup in order to receive the `totalElements` value + * needed for our HTML template. * @param group */ getSubgroups(group: Group): Observable>> { - return this.groupService.findListByHref(group._links.subgroups.href).pipe(getFirstSucceededRemoteData()); + return this.groupService.findListByHref(group._links.subgroups.href, { + currentPage: 1, + elementsPerPage: 1, + }).pipe(getFirstSucceededRemoteData()); } /** diff --git a/src/app/admin/admin-import-batch-page/batch-import-page.component.html b/src/app/admin/admin-import-batch-page/batch-import-page.component.html index dbc8c744377..10924434369 100644 --- a/src/app/admin/admin-import-batch-page/batch-import-page.component.html +++ b/src/app/admin/admin-import-batch-page/batch-import-page.component.html @@ -20,12 +20,29 @@ + + + {{'admin.batch-import.page.toggle.help' | translate}} + + + +
    + +
    +
    diff --git a/src/app/admin/admin-import-batch-page/batch-import-page.component.spec.ts b/src/app/admin/admin-import-batch-page/batch-import-page.component.spec.ts index 36ba1137c94..341aefb7044 100644 --- a/src/app/admin/admin-import-batch-page/batch-import-page.component.spec.ts +++ b/src/app/admin/admin-import-batch-page/batch-import-page.component.spec.ts @@ -86,10 +86,18 @@ describe('BatchImportPageComponent', () => { let fileMock: File; beforeEach(() => { + component.isUpload = true; fileMock = new File([''], 'filename.zip', { type: 'application/zip' }); component.setFile(fileMock); }); + it('should show the file dropzone', () => { + const fileDropzone = fixture.debugElement.query(By.css('[data-test="file-dropzone"]')); + const fileUrlInput = fixture.debugElement.query(By.css('[data-test="file-url-input"]')); + expect(fileDropzone).toBeTruthy(); + expect(fileUrlInput).toBeFalsy(); + }); + describe('if proceed button is pressed without validate only', () => { beforeEach(fakeAsync(() => { component.validateOnly = false; @@ -99,9 +107,9 @@ describe('BatchImportPageComponent', () => { })); it('metadata-import script is invoked with --zip fileName and the mockFile', () => { const parameterValues: ProcessParameter[] = [ - Object.assign(new ProcessParameter(), { name: '--zip', value: 'filename.zip' }), + Object.assign(new ProcessParameter(), { name: '--add' }), + Object.assign(new ProcessParameter(), { name: '--zip', value: 'filename.zip' }) ]; - parameterValues.push(Object.assign(new ProcessParameter(), { name: '--add' })); expect(scriptService.invoke).toHaveBeenCalledWith(BATCH_IMPORT_SCRIPT_NAME, parameterValues, [fileMock]); }); it('success notification is shown', () => { @@ -121,8 +129,8 @@ describe('BatchImportPageComponent', () => { })); it('metadata-import script is invoked with --zip fileName and the mockFile and -v validate-only', () => { const parameterValues: ProcessParameter[] = [ - Object.assign(new ProcessParameter(), { name: '--zip', value: 'filename.zip' }), Object.assign(new ProcessParameter(), { name: '--add' }), + Object.assign(new ProcessParameter(), { name: '--zip', value: 'filename.zip' }), Object.assign(new ProcessParameter(), { name: '-v', value: true }), ]; expect(scriptService.invoke).toHaveBeenCalledWith(BATCH_IMPORT_SCRIPT_NAME, parameterValues, [fileMock]); @@ -148,4 +156,77 @@ describe('BatchImportPageComponent', () => { }); }); }); + + describe('if url is set', () => { + beforeEach(fakeAsync(() => { + component.isUpload = false; + component.fileURL = 'example.fileURL.com'; + fixture.detectChanges(); + })); + + it('should show the file url input', () => { + const fileDropzone = fixture.debugElement.query(By.css('[data-test="file-dropzone"]')); + const fileUrlInput = fixture.debugElement.query(By.css('[data-test="file-url-input"]')); + expect(fileDropzone).toBeFalsy(); + expect(fileUrlInput).toBeTruthy(); + }); + + describe('if proceed button is pressed without validate only', () => { + beforeEach(fakeAsync(() => { + component.validateOnly = false; + const proceed = fixture.debugElement.query(By.css('#proceedButton')).nativeElement; + proceed.click(); + fixture.detectChanges(); + })); + it('metadata-import script is invoked with --url and the file url', () => { + const parameterValues: ProcessParameter[] = [ + Object.assign(new ProcessParameter(), { name: '--add' }), + Object.assign(new ProcessParameter(), { name: '--url', value: 'example.fileURL.com' }) + ]; + expect(scriptService.invoke).toHaveBeenCalledWith(BATCH_IMPORT_SCRIPT_NAME, parameterValues, [null]); + }); + it('success notification is shown', () => { + expect(notificationService.success).toHaveBeenCalled(); + }); + it('redirected to process page', () => { + expect(router.navigateByUrl).toHaveBeenCalledWith('/processes/46'); + }); + }); + + describe('if proceed button is pressed with validate only', () => { + beforeEach(fakeAsync(() => { + component.validateOnly = true; + const proceed = fixture.debugElement.query(By.css('#proceedButton')).nativeElement; + proceed.click(); + fixture.detectChanges(); + })); + it('metadata-import script is invoked with --url and the file url and -v validate-only', () => { + const parameterValues: ProcessParameter[] = [ + Object.assign(new ProcessParameter(), { name: '--add' }), + Object.assign(new ProcessParameter(), { name: '--url', value: 'example.fileURL.com' }), + Object.assign(new ProcessParameter(), { name: '-v', value: true }), + ]; + expect(scriptService.invoke).toHaveBeenCalledWith(BATCH_IMPORT_SCRIPT_NAME, parameterValues, [null]); + }); + it('success notification is shown', () => { + expect(notificationService.success).toHaveBeenCalled(); + }); + it('redirected to process page', () => { + expect(router.navigateByUrl).toHaveBeenCalledWith('/processes/46'); + }); + }); + + describe('if proceed is pressed; but script invoke fails', () => { + beforeEach(fakeAsync(() => { + jasmine.getEnv().allowRespy(true); + spyOn(scriptService, 'invoke').and.returnValue(createFailedRemoteDataObject$('Error', 500)); + const proceed = fixture.debugElement.query(By.css('#proceedButton')).nativeElement; + proceed.click(); + fixture.detectChanges(); + })); + it('error notification is shown', () => { + expect(notificationService.error).toHaveBeenCalled(); + }); + }); + }); }); diff --git a/src/app/admin/admin-import-batch-page/batch-import-page.component.ts b/src/app/admin/admin-import-batch-page/batch-import-page.component.ts index 7171c67585f..673e1f23f54 100644 --- a/src/app/admin/admin-import-batch-page/batch-import-page.component.ts +++ b/src/app/admin/admin-import-batch-page/batch-import-page.component.ts @@ -8,7 +8,7 @@ import { ProcessParameter } from '../../process-page/processes/process-parameter import { getFirstCompletedRemoteData } from '../../core/shared/operators'; import { RemoteData } from '../../core/data/remote-data'; import { Process } from '../../process-page/processes/process.model'; -import { isNotEmpty } from '../../shared/empty.util'; +import { isEmpty, isNotEmpty } from '../../shared/empty.util'; import { getProcessDetailRoute } from '../../process-page/process-page-routing.paths'; import { ImportBatchSelectorComponent @@ -32,11 +32,22 @@ export class BatchImportPageComponent { * The validate only flag */ validateOnly = true; + /** * dso object for community or collection */ dso: DSpaceObject = null; + /** + * The flag between upload and url + */ + isUpload = true; + + /** + * File URL when flag is for url + */ + fileURL: string; + public constructor(private location: Location, protected translate: TranslateService, protected notificationsService: NotificationsService, @@ -72,13 +83,22 @@ export class BatchImportPageComponent { * Starts import-metadata script with --zip fileName (and the selected file) */ public importMetadata() { - if (this.fileObject == null) { - this.notificationsService.error(this.translate.get('admin.metadata-import.page.error.addFile')); + if (this.fileObject == null && isEmpty(this.fileURL)) { + if (this.isUpload) { + this.notificationsService.error(this.translate.get('admin.metadata-import.page.error.addFile')); + } else { + this.notificationsService.error(this.translate.get('admin.metadata-import.page.error.addFileUrl')); + } } else { const parameterValues: ProcessParameter[] = [ - Object.assign(new ProcessParameter(), { name: '--zip', value: this.fileObject.name }), Object.assign(new ProcessParameter(), { name: '--add' }) ]; + if (this.isUpload) { + parameterValues.push(Object.assign(new ProcessParameter(), { name: '--zip', value: this.fileObject.name })); + } else { + this.fileObject = null; + parameterValues.push(Object.assign(new ProcessParameter(), { name: '--url', value: this.fileURL })); + } if (this.dso) { parameterValues.push(Object.assign(new ProcessParameter(), { name: '--collection', value: this.dso.uuid })); } @@ -97,9 +117,15 @@ export class BatchImportPageComponent { this.router.navigateByUrl(getProcessDetailRoute(rd.payload.processId)); } } else { - const title = this.translate.get('process.new.notification.error.title'); - const content = this.translate.get('process.new.notification.error.content'); - this.notificationsService.error(title, content); + if (rd.statusCode === 413) { + const title = this.translate.get('process.new.notification.error.title'); + const content = this.translate.get('process.new.notification.error.max-upload.content'); + this.notificationsService.error(title, content); + } else { + const title = this.translate.get('process.new.notification.error.title'); + const content = this.translate.get('process.new.notification.error.content'); + this.notificationsService.error(title, content); + } } }); } @@ -121,4 +147,11 @@ export class BatchImportPageComponent { removeDspaceObject(): void { this.dso = null; } + + /** + * toggle the flag between upload and url + */ + toggleUpload() { + this.isUpload = !this.isUpload; + } } diff --git a/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.html b/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.html index c4304806ce0..0a2e9f0f926 100644 --- a/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.html +++ b/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.html @@ -29,7 +29,7 @@
    -
    -
    - {{field.id}}{{schema?.prefix}}.{{field.element}}{{field.qualifier}}{{schema?.prefix}}.{{field.element}}{{field.qualifier ? '.' + field.qualifier : ''}} {{field.scopeNote}}
    - +
    diff --git a/src/app/item-page/full/full-item-page.component.spec.ts b/src/app/item-page/full/full-item-page.component.spec.ts index 66c6488b8e4..9fc078c2cd7 100644 --- a/src/app/item-page/full/full-item-page.component.spec.ts +++ b/src/app/item-page/full/full-item-page.component.spec.ts @@ -2,7 +2,7 @@ import { ComponentFixture, fakeAsync, TestBed, waitForAsync } from '@angular/cor import { ItemDataService } from '../../core/data/item-data.service'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock'; -import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; +import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA, PLATFORM_ID } from '@angular/core'; import { TruncatePipe } from '../../shared/utils/truncate.pipe'; import { FullItemPageComponent } from './full-item-page.component'; import { MetadataService } from '../../core/metadata/metadata.service'; @@ -20,6 +20,9 @@ import { createPaginatedList } from '../../shared/testing/utils.test'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; import { createRelationshipsObservable } from '../simple/item-types/shared/item.component.spec'; import { RemoteData } from '../../core/data/remote-data'; +import { ServerResponseService } from '../../core/services/server-response.service'; +import { SignpostingDataService } from '../../core/data/signposting-data.service'; +import { LinkHeadService } from '../../core/services/link-head.service'; const mockItem: Item = Object.assign(new Item(), { bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])), @@ -55,8 +58,21 @@ describe('FullItemPageComponent', () => { let routeStub: ActivatedRouteStub; let routeData; let authorizationDataService: AuthorizationDataService; - - + let serverResponseService: jasmine.SpyObj; + let signpostingDataService: jasmine.SpyObj; + let linkHeadService: jasmine.SpyObj; + + const mocklink = { + href: 'http://test.org', + rel: 'test', + type: 'test' + }; + + const mocklink2 = { + href: 'http://test2.org', + rel: 'test', + type: 'test' + }; beforeEach(waitForAsync(() => { authService = jasmine.createSpyObj('authService', { @@ -76,6 +92,19 @@ describe('FullItemPageComponent', () => { isAuthorized: observableOf(false), }); + serverResponseService = jasmine.createSpyObj('ServerResponseService', { + setHeader: jasmine.createSpy('setHeader'), + }); + + signpostingDataService = jasmine.createSpyObj('SignpostingDataService', { + getLinks: observableOf([mocklink, mocklink2]), + }); + + linkHeadService = jasmine.createSpyObj('LinkHeadService', { + addTag: jasmine.createSpy('setHeader'), + removeTag: jasmine.createSpy('removeTag'), + }); + TestBed.configureTestingModule({ imports: [TranslateModule.forRoot({ loader: { @@ -90,8 +119,11 @@ describe('FullItemPageComponent', () => { { provide: MetadataService, useValue: metadataServiceStub }, { provide: AuthService, useValue: authService }, { provide: AuthorizationDataService, useValue: authorizationDataService }, + { provide: ServerResponseService, useValue: serverResponseService }, + { provide: SignpostingDataService, useValue: signpostingDataService }, + { provide: LinkHeadService, useValue: linkHeadService }, + { provide: PLATFORM_ID, useValue: 'server' } ], - schemas: [NO_ERRORS_SCHEMA] }).overrideComponent(FullItemPageComponent, { set: { changeDetection: ChangeDetectionStrategy.Default } @@ -104,9 +136,13 @@ describe('FullItemPageComponent', () => { fixture.detectChanges(); })); + afterEach(() => { + fixture.debugElement.nativeElement.remove(); + }); + it('should display the item\'s metadata', () => { const table = fixture.debugElement.query(By.css('table')); - for (const metadatum of mockItem.allMetadata([])) { + for (const metadatum of mockItem.allMetadata(Object.keys(mockItem.metadata))) { expect(table.nativeElement.innerHTML).toContain(metadatum.value); } }); @@ -137,7 +173,12 @@ describe('FullItemPageComponent', () => { it('should display the item', () => { const objectLoader = fixture.debugElement.query(By.css('.full-item-info')); - expect(objectLoader.nativeElement).toBeDefined(); + expect(objectLoader.nativeElement).not.toBeNull(); + }); + + it('should add the signposting links', () => { + expect(serverResponseService.setHeader).toHaveBeenCalled(); + expect(linkHeadService.addTag).toHaveBeenCalledTimes(2); }); }); describe('when the item is withdrawn and the user is not an admin', () => { @@ -161,7 +202,12 @@ describe('FullItemPageComponent', () => { it('should display the item', () => { const objectLoader = fixture.debugElement.query(By.css('.full-item-info')); - expect(objectLoader.nativeElement).toBeDefined(); + expect(objectLoader).not.toBeNull(); + }); + + it('should add the signposting links', () => { + expect(serverResponseService.setHeader).toHaveBeenCalled(); + expect(linkHeadService.addTag).toHaveBeenCalledTimes(2); }); }); @@ -173,7 +219,12 @@ describe('FullItemPageComponent', () => { it('should display the item', () => { const objectLoader = fixture.debugElement.query(By.css('.full-item-info')); - expect(objectLoader.nativeElement).toBeDefined(); + expect(objectLoader).not.toBeNull(); + }); + + it('should add the signposting links', () => { + expect(serverResponseService.setHeader).toHaveBeenCalled(); + expect(linkHeadService.addTag).toHaveBeenCalledTimes(2); }); }); }); diff --git a/src/app/item-page/full/full-item-page.component.ts b/src/app/item-page/full/full-item-page.component.ts index 118e4360048..31dd2c5fc28 100644 --- a/src/app/item-page/full/full-item-page.component.ts +++ b/src/app/item-page/full/full-item-page.component.ts @@ -1,5 +1,5 @@ import { filter, map } from 'rxjs/operators'; -import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core'; import { ActivatedRoute, Data, Router } from '@angular/router'; import { BehaviorSubject, Observable } from 'rxjs'; @@ -16,7 +16,9 @@ import { hasValue } from '../../shared/empty.util'; import { AuthService } from '../../core/auth/auth.service'; import { Location } from '@angular/common'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; - +import { ServerResponseService } from '../../core/services/server-response.service'; +import { SignpostingDataService } from '../../core/data/signposting-data.service'; +import { LinkHeadService } from '../../core/services/link-head.service'; /** * This component renders a full item page. @@ -43,13 +45,19 @@ export class FullItemPageComponent extends ItemPageComponent implements OnInit, subs = []; - constructor(protected route: ActivatedRoute, - router: Router, - items: ItemDataService, - authService: AuthService, - authorizationService: AuthorizationDataService, - private _location: Location) { - super(route, router, items, authService, authorizationService); + constructor( + protected route: ActivatedRoute, + protected router: Router, + protected items: ItemDataService, + protected authService: AuthService, + protected authorizationService: AuthorizationDataService, + protected _location: Location, + protected responseService: ServerResponseService, + protected signpostingDataService: SignpostingDataService, + protected linkHeadService: LinkHeadService, + @Inject(PLATFORM_ID) protected platformId: string, + ) { + super(route, router, items, authService, authorizationService, responseService, signpostingDataService, linkHeadService, platformId); } /*** AoT inheritance fix, will hopefully be resolved in the near future **/ diff --git a/src/app/item-page/item-page-routing.module.ts b/src/app/item-page/item-page-routing.module.ts index cfb28240f74..378bcd717dc 100644 --- a/src/app/item-page/item-page-routing.module.ts +++ b/src/app/item-page/item-page-routing.module.ts @@ -29,8 +29,7 @@ import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver'; resolve: { dso: ItemPageResolver, breadcrumb: ItemBreadcrumbResolver, - menu: DSOEditMenuResolver, - tabs: CrisItemPageTabResolver + menu: DSOEditMenuResolver }, runGuardsAndResolvers: 'always', children: [ @@ -38,6 +37,9 @@ import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver'; path: '', component: ThemedItemPageComponent, pathMatch: 'full', + resolve: { + tabs: CrisItemPageTabResolver + } }, { path: 'full', @@ -63,6 +65,13 @@ import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver'; path: ORCID_PATH, component: OrcidPageComponent, canActivate: [AuthenticatedGuard, OrcidPageGuard] + }, + { + path: ':tab', + component: ThemedItemPageComponent, + resolve: { + tabs: CrisItemPageTabResolver + }, } ], data: { diff --git a/src/app/item-page/item-page.module.ts b/src/app/item-page/item-page.module.ts index 9565900f035..c490a41782e 100644 --- a/src/app/item-page/item-page.module.ts +++ b/src/app/item-page/item-page.module.ts @@ -36,8 +36,11 @@ import { ResearchEntitiesModule } from '../entity-groups/research-entities/resea import { ThemedItemPageComponent } from './simple/themed-item-page.component'; import { ThemedFullItemPageComponent } from './full/themed-full-item-page.component'; import { MediaViewerComponent } from './media-viewer/media-viewer.component'; +import { ThemedMediaViewerComponent } from './media-viewer/themed-media-viewer.component'; import { MediaViewerVideoComponent } from './media-viewer/media-viewer-video/media-viewer-video.component'; +import { ThemedMediaViewerVideoComponent } from './media-viewer/media-viewer-video/themed-media-viewer-video.component'; import { MediaViewerImageComponent } from './media-viewer/media-viewer-image/media-viewer-image.component'; +import { ThemedMediaViewerImageComponent } from './media-viewer/media-viewer-image/themed-media-viewer-image.component'; import { NgxGalleryModule } from '@kolkov/ngx-gallery'; import { MiradorViewerModule } from './mirador-viewer/mirador-viewer.module'; import { VersionPageComponent } from './version-page/version-page/version-page.component'; @@ -54,7 +57,10 @@ import { BitstreamRequestACopyPageComponent } from './bitstreams/request-a-copy/ import { FileSectionComponent } from './simple/field-components/file-section/file-section.component'; import { ItemSharedModule } from './item-shared.module'; import { DsoPageModule } from '../shared/dso-page/dso-page.module'; - +import { ThemedItemAlertsComponent } from './alerts/themed-item-alerts.component'; +import { + ThemedFullFileSectionComponent +} from './full/field-components/file-section/themed-full-file-section.component'; const ENTRY_COMPONENTS = [ // put only entry components that use custom decorator @@ -77,19 +83,24 @@ const DECLARATIONS = [ ItemPageFieldComponent, CollectionsComponent, FullFileSectionComponent, + ThemedFullFileSectionComponent, PublicationComponent, UntypedItemComponent, ItemComponent, UploadBitstreamComponent, AbstractIncrementalListComponent, MediaViewerComponent, + ThemedMediaViewerComponent, MediaViewerVideoComponent, + ThemedMediaViewerVideoComponent, MediaViewerImageComponent, + ThemedMediaViewerImageComponent, VersionPageComponent, OrcidPageComponent, OrcidAuthComponent, OrcidSyncSettingsComponent, OrcidQueueComponent, + ThemedItemAlertsComponent, BitstreamRequestACopyPageComponent, ]; diff --git a/src/app/item-page/item-page.resolver.spec.ts b/src/app/item-page/item-page.resolver.spec.ts index e9b9a246721..5d91d2177cc 100644 --- a/src/app/item-page/item-page.resolver.spec.ts +++ b/src/app/item-page/item-page.resolver.spec.ts @@ -6,6 +6,7 @@ import { ItemPageResolver } from './item-page.resolver'; import { TestBed } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; import { Router } from '@angular/router'; +import { HardRedirectService } from '../core/services/hard-redirect.service'; describe('ItemPageResolver', () => { beforeEach(() => { @@ -23,6 +24,7 @@ describe('ItemPageResolver', () => { let store; let router; + let hardRedirectService: HardRedirectService ; const uuid = '1234-65487-12354-1235'; const item = Object.assign(new Item(), { id: uuid, @@ -64,8 +66,12 @@ describe('ItemPageResolver', () => { dispatch: {}, }); + hardRedirectService = jasmine.createSpyObj('HardRedirectService', { + 'redirect': jasmine.createSpy('redirect') + }); + spyOn(router, 'navigateByUrl'); - resolver = new ItemPageResolver(itemService, store, router); + resolver = new ItemPageResolver(hardRedirectService, itemService, store, router); }); it('should resolve a an item from from the item with the url redirect', (done) => { @@ -106,7 +112,7 @@ describe('ItemPageResolver', () => { .pipe(first()) .subscribe( (resolved) => { - expect(router.navigateByUrl).not.toHaveBeenCalledWith('/entities/person/customurl/edit'); + expect(hardRedirectService.redirect).not.toHaveBeenCalledWith('/entities/person/customurl/edit'); done(); } ); @@ -126,16 +132,31 @@ describe('ItemPageResolver', () => { dispatch: {}, }); + hardRedirectService = jasmine.createSpyObj('HardRedirectService', { + 'redirect': jasmine.createSpy('redirect') + }); + spyOn(router, 'navigateByUrl'); - resolver = new ItemPageResolver(itemService, store, router); + resolver = new ItemPageResolver(hardRedirectService, itemService, store, router); }); - it('should not call custom url', (done) => { - resolver.resolve({ params: { id: uuid } } as any, { url: 'test-url/1234-65487-12354-1235/edit' } as any) + it('should redirect if it has not the new item url', (done) => { + resolver.resolve({ params: { id: uuid } } as any, { url: '/items/1234-65487-12354-1235/edit' } as any) + .pipe(first()) + .subscribe( + (resolved) => { + expect(hardRedirectService.redirect).toHaveBeenCalledWith('/entities/person/1234-65487-12354-1235/edit', 301); + done(); + } + ); + }); + + it('should not redirect if it has the new item url', (done) => { + resolver.resolve({ params: { id: uuid } } as any, { url: '/entities/person/1234-65487-12354-1235/edit' } as any) .pipe(first()) .subscribe( (resolved) => { - expect(router.navigateByUrl).toHaveBeenCalledWith('/entities/person/1234-65487-12354-1235/edit'); + expect(hardRedirectService.redirect).not.toHaveBeenCalled(); done(); } ); diff --git a/src/app/item-page/item-page.resolver.ts b/src/app/item-page/item-page.resolver.ts index 6fd9b9435d8..c289bae5dad 100644 --- a/src/app/item-page/item-page.resolver.ts +++ b/src/app/item-page/item-page.resolver.ts @@ -9,6 +9,7 @@ import { map } from 'rxjs/operators'; import { hasValue, isNotEmpty } from '../shared/empty.util'; import { getItemPageRoute } from './item-page-routing-paths'; import { ItemResolver } from './item.resolver'; +import { HardRedirectService } from '../core/services/hard-redirect.service'; /** * This class represents a resolver that requests a specific item before the route is activated and will redirect to the @@ -17,6 +18,7 @@ import { ItemResolver } from './item.resolver'; @Injectable() export class ItemPageResolver extends ItemResolver { constructor( + protected hardRedirectService: HardRedirectService, protected itemService: ItemDataService, protected store: Store, protected router: Router @@ -53,7 +55,7 @@ export class ItemPageResolver extends ItemResolver { if (!thisRoute.startsWith(itemRoute)) { const itemId = rd.payload.uuid; const subRoute = thisRoute.substring(thisRoute.indexOf(itemId) + itemId.length, thisRoute.length); - this.router.navigateByUrl(itemRoute + subRoute); + this.hardRedirectService.redirect(itemRoute + subRoute, 301); } } } diff --git a/src/app/item-page/item-shared.module.ts b/src/app/item-page/item-shared.module.ts index 257efc3728c..4a083dc1a94 100644 --- a/src/app/item-page/item-shared.module.ts +++ b/src/app/item-page/item-shared.module.ts @@ -1,6 +1,4 @@ -import { - RelatedEntitiesSearchComponent -} from './simple/related-entities/related-entities-search/related-entities-search.component'; +import { RelatedEntitiesSearchComponent } from './simple/related-entities/related-entities-search/related-entities-search.component'; import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { SearchModule } from '../shared/search/search.module'; @@ -15,6 +13,9 @@ import { MetadataValuesComponent } from './field-components/metadata-values/meta import { GenericItemPageFieldComponent } from './simple/field-components/specific-field/generic/generic-item-page-field.component'; import { MetadataRepresentationListComponent } from './simple/metadata-representation-list/metadata-representation-list.component'; import { RelatedItemsComponent } from './simple/related-items/related-items-component'; +import { + ThemedMetadataRepresentationListComponent +} from './simple/metadata-representation-list/themed-metadata-representation-list.component'; import { ItemAlertsComponent } from './alerts/item-alerts.component'; const ENTRY_COMPONENTS = [ @@ -30,6 +31,7 @@ const COMPONENTS = [ MetadataValuesComponent, GenericItemPageFieldComponent, MetadataRepresentationListComponent, + ThemedMetadataRepresentationListComponent, RelatedItemsComponent, ItemAlertsComponent ]; @@ -55,5 +57,4 @@ const COMPONENTS = [ ...ENTRY_COMPONENTS, ] }) -export class ItemSharedModule { -} +export class ItemSharedModule { } diff --git a/src/app/item-page/media-viewer/media-viewer-image/media-viewer-image.component.scss b/src/app/item-page/media-viewer/media-viewer-image/media-viewer-image.component.scss index 72ce4b04d9c..cba963b6fa8 100644 --- a/src/app/item-page/media-viewer/media-viewer-image/media-viewer-image.component.scss +++ b/src/app/item-page/media-viewer/media-viewer-image/media-viewer-image.component.scss @@ -1,6 +1,20 @@ -.ngx-gallery { - display: inline-block; - margin-bottom: 20px; - width: 340px !important; - height: 279px !important; +:host ::ng-deep { + .ngx-gallery { + width: unset !important; + height: unset !important; + } + + ngx-gallery-image { + max-width: 340px !important; + + .ngx-gallery-image { + background-position: left; + } + } + + ngx-gallery-image:after { + padding-top: 75%; + display: block; + content: ''; + } } diff --git a/src/app/item-page/media-viewer/media-viewer-image/media-viewer-image.component.ts b/src/app/item-page/media-viewer/media-viewer-image/media-viewer-image.component.ts index 0c32b5603de..2ad43f6883c 100644 --- a/src/app/item-page/media-viewer/media-viewer-image/media-viewer-image.component.ts +++ b/src/app/item-page/media-viewer/media-viewer-image/media-viewer-image.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, OnInit } from '@angular/core'; +import { Component, Input, OnChanges, OnInit } from '@angular/core'; import { NgxGalleryImage, NgxGalleryOptions } from '@kolkov/ngx-gallery'; import { MediaViewerItem } from '../../../core/shared/media-viewer-item.model'; import { NgxGalleryAnimation } from '@kolkov/ngx-gallery'; @@ -13,28 +13,28 @@ import { AuthService } from '../../../core/auth/auth.service'; templateUrl: './media-viewer-image.component.html', styleUrls: ['./media-viewer-image.component.scss'], }) -export class MediaViewerImageComponent implements OnInit { +export class MediaViewerImageComponent implements OnChanges, OnInit { @Input() images: MediaViewerItem[]; @Input() preview?: boolean; @Input() image?: string; - loggedin: boolean; + thumbnailPlaceholder = './assets/images/replacement_image.svg'; - galleryOptions: NgxGalleryOptions[]; - galleryImages: NgxGalleryImage[]; + galleryOptions: NgxGalleryOptions[] = []; + + galleryImages: NgxGalleryImage[] = []; /** * Whether or not the current user is authenticated */ isAuthenticated$: Observable; - constructor(private authService: AuthService) {} + constructor( + protected authService: AuthService, + ) { + } - /** - * Thi method sets up the gallery settings and data - */ - ngOnInit(): void { - this.isAuthenticated$ = this.authService.isAuthenticated(); + ngOnChanges(): void { this.galleryOptions = [ { preview: this.preview !== undefined ? this.preview : true, @@ -50,7 +50,6 @@ export class MediaViewerImageComponent implements OnInit { previewFullscreen: true, }, ]; - if (this.image) { this.galleryImages = [ { @@ -64,25 +63,30 @@ export class MediaViewerImageComponent implements OnInit { } } + ngOnInit(): void { + this.isAuthenticated$ = this.authService.isAuthenticated(); + this.ngOnChanges(); + } + /** * This method convert an array of MediaViewerItem into NgxGalleryImage array * @param medias input NgxGalleryImage array */ convertToGalleryImage(medias: MediaViewerItem[]): NgxGalleryImage[] { - const mappadImages = []; + const mappedImages = []; for (const image of medias) { if (image.format === 'image') { - mappadImages.push({ + mappedImages.push({ small: image.thumbnail ? image.thumbnail - : './assets/images/replacement_image.svg', + : this.thumbnailPlaceholder, medium: image.thumbnail ? image.thumbnail - : './assets/images/replacement_image.svg', + : this.thumbnailPlaceholder, big: image.bitstream._links.content.href, }); } } - return mappadImages; + return mappedImages; } } diff --git a/src/app/item-page/media-viewer/media-viewer-image/themed-media-viewer-image.component.ts b/src/app/item-page/media-viewer/media-viewer-image/themed-media-viewer-image.component.ts new file mode 100644 index 00000000000..85ac779817d --- /dev/null +++ b/src/app/item-page/media-viewer/media-viewer-image/themed-media-viewer-image.component.ts @@ -0,0 +1,38 @@ +import { Component, Input } from '@angular/core'; +import { ThemedComponent } from '../../../shared/theme-support/themed.component'; +import { MediaViewerImageComponent } from './media-viewer-image.component'; +import { MediaViewerItem } from '../../../core/shared/media-viewer-item.model'; + +/** + * Themed wrapper for {@link MediaViewerImageComponent}. + */ +@Component({ + selector: 'ds-themed-media-viewer-image', + styleUrls: [], + templateUrl: '../../../shared/theme-support/themed.component.html', +}) +export class ThemedMediaViewerImageComponent extends ThemedComponent { + + @Input() images: MediaViewerItem[]; + @Input() preview?: boolean; + @Input() image?: string; + + protected inAndOutputNames: (keyof MediaViewerImageComponent & keyof this)[] = [ + 'images', + 'preview', + 'image', + ]; + + protected getComponentName(): string { + return 'MediaViewerImageComponent'; + } + + protected importThemedComponent(themeName: string): Promise { + return import(`../../../../themes/${themeName}/app/item-page/media-viewer/media-viewer-image/media-viewer-image.component`); + } + + protected importUnthemedComponent(): Promise { + return import('./media-viewer-image.component'); + } + +} diff --git a/src/app/item-page/media-viewer/media-viewer-video/media-viewer-video.component.html b/src/app/item-page/media-viewer/media-viewer-video/media-viewer-video.component.html index 0cc854b2721..32176808153 100644 --- a/src/app/item-page/media-viewer/media-viewer-video/media-viewer-video.component.html +++ b/src/app/item-page/media-viewer/media-viewer-video/media-viewer-video.component.html @@ -1,23 +1,22 @@ -
    +
    diff --git a/src/app/item-page/media-viewer/media-viewer-video/media-viewer-video.component.scss b/src/app/item-page/media-viewer/media-viewer-video/media-viewer-video.component.scss index 7702da7361d..bb8b9d360ea 100644 --- a/src/app/item-page/media-viewer/media-viewer-video/media-viewer-video.component.scss +++ b/src/app/item-page/media-viewer/media-viewer-video/media-viewer-video.component.scss @@ -1,4 +1,10 @@ video { - width: 340px; - height: 279px; + width: 100%; + height: auto; + max-width: 340px; +} + +.buttons { + display: flex; + gap: .25rem; } diff --git a/src/app/item-page/media-viewer/media-viewer-video/media-viewer-video.component.spec.ts b/src/app/item-page/media-viewer/media-viewer-video/media-viewer-video.component.spec.ts index 846b5878f43..92aa229b01c 100644 --- a/src/app/item-page/media-viewer/media-viewer-video/media-viewer-video.component.spec.ts +++ b/src/app/item-page/media-viewer/media-viewer-video/media-viewer-video.component.spec.ts @@ -83,7 +83,6 @@ describe('MediaViewerVideoComponent', () => { fixture = TestBed.createComponent(MediaViewerVideoComponent); component = fixture.componentInstance; component.medias = mockMediaViewerItem; - component.filteredMedias = mockMediaViewerItem; fixture.detectChanges(); }); @@ -94,7 +93,6 @@ describe('MediaViewerVideoComponent', () => { describe('should show controller buttons when the having mode then one video', () => { beforeEach(() => { component.medias = mockMediaViewerItems; - component.filteredMedias = mockMediaViewerItems; fixture.detectChanges(); }); diff --git a/src/app/item-page/media-viewer/media-viewer-video/media-viewer-video.component.ts b/src/app/item-page/media-viewer/media-viewer-video/media-viewer-video.component.ts index 647bbacdc36..52cd3cac34e 100644 --- a/src/app/item-page/media-viewer/media-viewer-video/media-viewer-video.component.ts +++ b/src/app/item-page/media-viewer/media-viewer-video/media-viewer-video.component.ts @@ -1,22 +1,25 @@ -import { Component, Input, OnInit } from '@angular/core'; +import { Component, Input } from '@angular/core'; import { MediaViewerItem } from '../../../core/shared/media-viewer-item.model'; +import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; import { languageHelper } from './language-helper'; -import { CaptionInfo} from './caption-info'; +import { CaptionInfo } from './caption-info'; +import { Bitstream } from 'src/app/core/shared/bitstream.model'; /** - * This componenet renders a video viewer and playlist for the media viewer + * This component renders a video viewer and playlist for the media viewer */ @Component({ selector: 'ds-media-viewer-video', templateUrl: './media-viewer-video.component.html', styleUrls: ['./media-viewer-video.component.scss'], }) -export class MediaViewerVideoComponent implements OnInit { +export class MediaViewerVideoComponent { @Input() medias: MediaViewerItem[]; - filteredMedias: MediaViewerItem[]; + @Input() captions: Bitstream[] = []; + + isCollapsed = false; - isCollapsed: boolean; currentIndex = 0; replacements = { @@ -24,11 +27,9 @@ export class MediaViewerVideoComponent implements OnInit { audio: './assets/images/replacement_audio.svg', }; - replacementThumbnail: string; - - ngOnInit() { - this.isCollapsed = false; - this.filteredMedias = this.medias.filter((media) => media.format === 'audio' || media.format === 'video'); + constructor( + public dsoNameService: DSONameService, + ) { } /** @@ -41,29 +42,24 @@ export class MediaViewerVideoComponent implements OnInit { * Two letter language code reference * https://www.w3schools.com/tags/ref_language_codes.asp */ - getMediaCap(name: string): CaptionInfo[] { - let filteredCapMedias: MediaViewerItem[]; - let capInfos: CaptionInfo[] = []; - filteredCapMedias = this.medias - .filter((media) => media.mimetype === 'text/vtt') - .filter((media) => media.bitstream.name.substring(0, (media.bitstream.name.length - 7) ).toLowerCase() === name.toLowerCase()); + getMediaCap(name: string, captions: Bitstream[]): CaptionInfo[] { + const capInfos: CaptionInfo[] = []; + const filteredCapMedias: Bitstream[] = captions + .filter((media: Bitstream) => media.name.substring(0, (media.name.length - 7)).toLowerCase() === name.toLowerCase()); - if (filteredCapMedias) { - filteredCapMedias - .forEach((media, index) => { - let srclang: string = media.bitstream.name.slice(-6, -4).toLowerCase(); - capInfos.push(new CaptionInfo( - media.bitstream._links.content.href, - srclang, - languageHelper[srclang] - )); - }); + for (const media of filteredCapMedias) { + let srclang: string = media.name.slice(-6, -4).toLowerCase(); + capInfos.push(new CaptionInfo( + media._links.content.href, + srclang, + languageHelper[srclang], + )); } return capInfos; } /** - * This method sets the reviced index into currentIndex + * This method sets the received index into currentIndex * @param index Selected index */ selectedMedia(index: number) { @@ -71,14 +67,14 @@ export class MediaViewerVideoComponent implements OnInit { } /** - * This method increade the number of the currentIndex + * This method increases the number of the currentIndex */ nextMedia() { this.currentIndex++; } /** - * This method decrese the number of the currentIndex + * This method decreases the number of the currentIndex */ prevMedia() { this.currentIndex--; diff --git a/src/app/item-page/media-viewer/media-viewer-video/themed-media-viewer-video.component.ts b/src/app/item-page/media-viewer/media-viewer-video/themed-media-viewer-video.component.ts new file mode 100644 index 00000000000..8ae45b2dd3f --- /dev/null +++ b/src/app/item-page/media-viewer/media-viewer-video/themed-media-viewer-video.component.ts @@ -0,0 +1,38 @@ +import { Component, Input } from '@angular/core'; +import { ThemedComponent } from '../../../shared/theme-support/themed.component'; +import { MediaViewerItem } from '../../../core/shared/media-viewer-item.model'; +import { MediaViewerVideoComponent } from './media-viewer-video.component'; +import { Bitstream } from '../../../core/shared/bitstream.model'; + +/** + * Themed wrapper for {@link MediaViewerVideoComponent}. + */ +@Component({ + selector: 'ds-themed-media-viewer-video', + styleUrls: [], + templateUrl: '../../../shared/theme-support/themed.component.html', +}) +export class ThemedMediaViewerVideoComponent extends ThemedComponent { + + @Input() medias: MediaViewerItem[]; + + @Input() captions: Bitstream[]; + + protected inAndOutputNames: (keyof MediaViewerVideoComponent & keyof this)[] = [ + 'medias', + 'captions', + ]; + + protected getComponentName(): string { + return 'MediaViewerVideoComponent'; + } + + protected importThemedComponent(themeName: string): Promise { + return import(`../../../../themes/${themeName}/app/item-page/media-viewer/media-viewer-video/media-viewer-video.component`); + } + + protected importUnthemedComponent(): Promise { + return import('./media-viewer-video.component'); + } + +} diff --git a/src/app/item-page/media-viewer/media-viewer.component.html b/src/app/item-page/media-viewer/media-viewer.component.html index 4259af52508..c8a02e039c7 100644 --- a/src/app/item-page/media-viewer/media-viewer.component.html +++ b/src/app/item-page/media-viewer/media-viewer.component.html @@ -5,32 +5,23 @@ [showMessage]="false" >
    - - - - + + + + + + - - - - - -
    + + + + + diff --git a/src/app/item-page/media-viewer/media-viewer.component.spec.ts b/src/app/item-page/media-viewer/media-viewer.component.spec.ts index 3369574f202..0c170ac8cf2 100644 --- a/src/app/item-page/media-viewer/media-viewer.component.spec.ts +++ b/src/app/item-page/media-viewer/media-viewer.component.spec.ts @@ -61,7 +61,7 @@ describe('MediaViewerComponent', () => { ); beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ + return TestBed.configureTestingModule({ imports: [ TranslateModule.forRoot({ loader: { @@ -94,7 +94,10 @@ describe('MediaViewerComponent', () => { describe('when the bitstreams are loading', () => { beforeEach(() => { comp.mediaList$.next([mockMediaViewerItem]); - comp.videoOptions = true; + comp.mediaOptions = { + image: true, + video: true, + }; comp.isLoading = true; fixture.detectChanges(); }); @@ -118,7 +121,10 @@ describe('MediaViewerComponent', () => { describe('when the bitstreams loading is failed', () => { beforeEach(() => { comp.mediaList$.next([]); - comp.videoOptions = true; + comp.mediaOptions = { + image: true, + video: true, + }; comp.isLoading = false; fixture.detectChanges(); }); @@ -135,7 +141,7 @@ describe('MediaViewerComponent', () => { it('should display a default, thumbnail', () => { const defaultThumbnail = fixture.debugElement.query( - By.css('ds-media-viewer-image') + By.css('ds-themed-media-viewer-image') ); expect(defaultThumbnail.nativeElement).toBeDefined(); }); diff --git a/src/app/item-page/media-viewer/media-viewer.component.ts b/src/app/item-page/media-viewer/media-viewer.component.ts index 233ae0e6f67..242e50646e3 100644 --- a/src/app/item-page/media-viewer/media-viewer.component.ts +++ b/src/app/item-page/media-viewer/media-viewer.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, OnInit } from '@angular/core'; +import { Component, Input, OnDestroy, OnInit } from '@angular/core'; import { BehaviorSubject, Observable } from 'rxjs'; import { filter, take } from 'rxjs/operators'; import { BitstreamDataService } from '../../core/data/bitstream-data.service'; @@ -11,61 +11,83 @@ import { MediaViewerItem } from '../../core/shared/media-viewer-item.model'; import { getFirstSucceededRemoteDataPayload } from '../../core/shared/operators'; import { hasValue } from '../../shared/empty.util'; import { followLink } from '../../shared/utils/follow-link-config.model'; +import { MediaViewerConfig } from '../../../config/media-viewer-config.interface'; +import { environment } from '../../../environments/environment'; +import { Subscription } from 'rxjs/internal/Subscription'; /** - * This componenet renders the media viewers + * This component renders the media viewers */ - @Component({ selector: 'ds-media-viewer', templateUrl: './media-viewer.component.html', styleUrls: ['./media-viewer.component.scss'], }) -export class MediaViewerComponent implements OnInit { +export class MediaViewerComponent implements OnDestroy, OnInit { @Input() item: Item; - @Input() videoOptions: boolean; - mediaList$: BehaviorSubject; + @Input() mediaOptions: MediaViewerConfig = environment.mediaViewer; + + mediaList$: BehaviorSubject = new BehaviorSubject([]); - isLoading: boolean; + captions$: BehaviorSubject = new BehaviorSubject([]); + + isLoading = true; thumbnailPlaceholder = './assets/images/replacement_document.svg'; - constructor(protected bitstreamDataService: BitstreamDataService) {} + thumbnailsRD$: Observable>>; + + subs: Subscription[] = []; + + constructor( + protected bitstreamDataService: BitstreamDataService, + ) { + } + + ngOnDestroy(): void { + this.subs.forEach((subscription: Subscription) => subscription.unsubscribe()); + } /** - * This metod loads all the Bitstreams and Thumbnails and contert it to media item + * This method loads all the Bitstreams and Thumbnails and converts it to {@link MediaViewerItem}s */ ngOnInit(): void { - this.mediaList$ = new BehaviorSubject([]); - this.isLoading = true; - this.loadRemoteData('ORIGINAL').subscribe((bitstreamsRD) => { + const types: string[] = [ + ...(this.mediaOptions.image ? ['image'] : []), + ...(this.mediaOptions.video ? ['audio', 'video'] : []), + ]; + this.thumbnailsRD$ = this.loadRemoteData('THUMBNAIL'); + this.subs.push(this.loadRemoteData('ORIGINAL').subscribe((bitstreamsRD: RemoteData>) => { if (bitstreamsRD.payload.page.length === 0) { this.isLoading = false; this.mediaList$.next([]); } else { - this.loadRemoteData('THUMBNAIL').subscribe((thumbnailsRD) => { + this.subs.push(this.thumbnailsRD$.subscribe((thumbnailsRD: RemoteData>) => { for ( let index = 0; index < bitstreamsRD.payload.page.length; index++ ) { - bitstreamsRD.payload.page[index].format + this.subs.push(bitstreamsRD.payload.page[index].format .pipe(getFirstSucceededRemoteDataPayload()) - .subscribe((format) => { - const current = this.mediaList$.getValue(); + .subscribe((format: BitstreamFormat) => { const mediaItem = this.createMediaViewerItem( bitstreamsRD.payload.page[index], format, thumbnailsRD.payload && thumbnailsRD.payload.page[index] ); - this.mediaList$.next([...current, mediaItem]); - }); + if (types.includes(mediaItem.format)) { + this.mediaList$.next([...this.mediaList$.getValue(), mediaItem]); + } else if (format.mimetype === 'text/vtt') { + this.captions$.next([...this.captions$.getValue(), bitstreamsRD.payload.page[index]]); + } + })); } this.isLoading = false; - }); + })); } - }); + })); } /** @@ -95,16 +117,12 @@ export class MediaViewerComponent implements OnInit { } /** - * This method create MediaViewerItem from incoming bitstreams - * @param original original remote data bitstream + * This method creates a {@link MediaViewerItem} from incoming {@link Bitstream}s + * @param original original bitstream * @param format original bitstream format - * @param thumbnail trunbnail remote data bitstream + * @param thumbnail thumbnail bitstream */ - createMediaViewerItem( - original: Bitstream, - format: BitstreamFormat, - thumbnail: Bitstream - ): MediaViewerItem { + createMediaViewerItem(original: Bitstream, format: BitstreamFormat, thumbnail: Bitstream): MediaViewerItem { const mediaItem = new MediaViewerItem(); mediaItem.bitstream = original; mediaItem.format = format.mimetype.split('/')[0]; diff --git a/src/app/item-page/media-viewer/themed-media-viewer.component.ts b/src/app/item-page/media-viewer/themed-media-viewer.component.ts new file mode 100644 index 00000000000..6acf9486163 --- /dev/null +++ b/src/app/item-page/media-viewer/themed-media-viewer.component.ts @@ -0,0 +1,37 @@ +import { Component, Input } from '@angular/core'; +import { ThemedComponent } from '../../shared/theme-support/themed.component'; +import { MediaViewerComponent } from './media-viewer.component'; +import { Item } from '../../core/shared/item.model'; +import { MediaViewerConfig } from '../../../config/media-viewer-config.interface'; + +/** + * Themed wrapper for {@link MediaViewerComponent}. + */ +@Component({ + selector: 'ds-themed-media-viewer', + styleUrls: [], + templateUrl: '../../shared/theme-support/themed.component.html', +}) +export class ThemedMediaViewerComponent extends ThemedComponent { + + @Input() item: Item; + @Input() mediaOptions: MediaViewerConfig; + + protected inAndOutputNames: (keyof MediaViewerComponent & keyof this)[] = [ + 'item', + 'mediaOptions', + ]; + + protected getComponentName(): string { + return 'MediaViewerComponent'; + } + + protected importThemedComponent(themeName: string): Promise { + return import(`../../../themes/${themeName}/app/item-page/media-viewer/media-viewer.component`); + } + + protected importUnthemedComponent(): Promise { + return import('./media-viewer.component'); + } + +} diff --git a/src/app/item-page/mirador-viewer/mirador-viewer.component.spec.ts b/src/app/item-page/mirador-viewer/mirador-viewer.component.spec.ts index 40ad0fd5d01..2727391dffd 100644 --- a/src/app/item-page/mirador-viewer/mirador-viewer.component.spec.ts +++ b/src/app/item-page/mirador-viewer/mirador-viewer.component.spec.ts @@ -253,7 +253,7 @@ describe('MiradorViewerComponent in development mode', () => { it('should show message', (() => { const value = fixture.debugElement .nativeElement.querySelector('#viewer-message'); - expect(value).toBeDefined(); + expect(value).not.toBeNull(); })); }); diff --git a/src/app/item-page/mirador-viewer/mirador-viewer.component.ts b/src/app/item-page/mirador-viewer/mirador-viewer.component.ts index fee80462721..15ebfc61bc6 100644 --- a/src/app/item-page/mirador-viewer/mirador-viewer.component.ts +++ b/src/app/item-page/mirador-viewer/mirador-viewer.component.ts @@ -70,7 +70,8 @@ export class MiradorViewerComponent implements OnInit { const manifestApiEndpoint = encodeURIComponent(environment.rest.baseUrl + '/iiif/' + this.object.id + '/manifest'); // The Express path to Mirador viewer. - let viewerPath = '/iiif/mirador/index.html?manifest=' + manifestApiEndpoint; + let viewerPath = `${environment.ui.nameSpace}${environment.ui.nameSpace.length > 1 ? '/' : ''}` + + `iiif/mirador/index.html?manifest=${manifestApiEndpoint}`; if (this.searchable) { // Tell the viewer add search to menu. viewerPath += '&searchable=' + this.searchable; diff --git a/src/app/item-page/orcid-page/orcid-queue/orcid-queue.component.html b/src/app/item-page/orcid-page/orcid-queue/orcid-queue.component.html index 9358bcf8351..6ba318f7fd5 100644 --- a/src/app/item-page/orcid-page/orcid-queue/orcid-queue.component.html +++ b/src/app/item-page/orcid-page/orcid-queue/orcid-queue.component.html @@ -34,11 +34,13 @@

    {{ 'person.orcid.registry.queue' | translate }}

    diff --git a/src/app/item-page/orcid-page/orcid-queue/orcid-queue.component.ts b/src/app/item-page/orcid-page/orcid-queue/orcid-queue.component.ts index 9a52d41a69d..74cff399540 100644 --- a/src/app/item-page/orcid-page/orcid-queue/orcid-queue.component.ts +++ b/src/app/item-page/orcid-page/orcid-queue/orcid-queue.component.ts @@ -15,7 +15,7 @@ import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; import { hasValue } from '../../../shared/empty.util'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; -import { AlertType } from '../../../shared/alert/aletr-type'; +import { AlertType } from '../../../shared/alert/alert-type'; import { Item } from '../../../core/shared/item.model'; import { OrcidAuthService } from '../../../core/orcid/orcid-auth.service'; import { UUIDService } from '../../../core/shared/uuid.service'; diff --git a/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.spec.ts b/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.spec.ts index cd466ae4a43..f2fa9d2440b 100644 --- a/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.spec.ts +++ b/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.spec.ts @@ -1,6 +1,6 @@ import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { UntypedFormControl, UntypedFormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { RouterTestingModule } from '@angular/router/testing'; import { By } from '@angular/platform-browser'; @@ -26,7 +26,7 @@ describe('OrcidSyncSettingsComponent test suite', () => { let scheduler: TestScheduler; let researcherProfileService: jasmine.SpyObj; let notificationsService; - let formGroup: FormGroup; + let formGroup: UntypedFormGroup; const mockResearcherProfile: ResearcherProfile = Object.assign(new ResearcherProfile(), { id: 'test-id', @@ -186,12 +186,12 @@ describe('OrcidSyncSettingsComponent test suite', () => { beforeEach(() => { scheduler = getTestScheduler(); notificationsService = (comp as any).notificationsService; - formGroup = new FormGroup({ - syncMode: new FormControl('MANUAL'), - syncFundings: new FormControl('ALL'), - syncPublications: new FormControl('ALL'), - syncProfile_BIOGRAPHICAL: new FormControl(true), - syncProfile_IDENTIFIERS: new FormControl(true), + formGroup = new UntypedFormGroup({ + syncMode: new UntypedFormControl('MANUAL'), + syncFundings: new UntypedFormControl('ALL'), + syncPublications: new UntypedFormControl('ALL'), + syncProfile_BIOGRAPHICAL: new UntypedFormControl(true), + syncProfile_IDENTIFIERS: new UntypedFormControl(true), }); spyOn(comp.settingsUpdated, 'emit'); }); diff --git a/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.ts b/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.ts index 6ef5f20c6e5..79b307da88b 100644 --- a/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.ts +++ b/src/app/item-page/orcid-page/orcid-sync-settings/orcid-sync-settings.component.ts @@ -1,5 +1,5 @@ import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; -import { FormGroup } from '@angular/forms'; +import { UntypedFormGroup } from '@angular/forms'; import { TranslateService } from '@ngx-translate/core'; import { Operation } from 'fast-json-patch'; @@ -127,7 +127,7 @@ export class OrcidSyncSettingsComponent implements OnInit { * * @param form The form group */ - onSubmit(form: FormGroup): void { + onSubmit(form: UntypedFormGroup): void { const operations: Operation[] = []; this.fillOperationsFor(operations, '/orcid/mode', form.value.syncMode); this.fillOperationsFor(operations, '/orcid/publications', form.value.syncPublications); diff --git a/src/app/item-page/simple/field-components/file-section/file-section.component.html b/src/app/item-page/simple/field-components/file-section/file-section.component.html index 8e9fb63eda8..cd708510e8c 100644 --- a/src/app/item-page/simple/field-components/file-section/file-section.component.html +++ b/src/app/item-page/simple/field-components/file-section/file-section.component.html @@ -2,16 +2,16 @@
    - {{file?.name}} - ({{(file?.sizeBytes) | dsFileSize }}) + {{ dsoNameService.getName(file) }} + ({{(file?.sizeBytes) | dsFileSize }})
    - {{'item.page.bitstreams.view-more' | translate}} +
    - {{'item.page.bitstreams.collapse' | translate}} +
    diff --git a/src/app/item-page/simple/field-components/file-section/file-section.component.spec.ts b/src/app/item-page/simple/field-components/file-section/file-section.component.spec.ts index ded3ea054bd..8acf405b55f 100644 --- a/src/app/item-page/simple/field-components/file-section/file-section.component.spec.ts +++ b/src/app/item-page/simple/field-components/file-section/file-section.component.spec.ts @@ -146,7 +146,7 @@ describe('FileSectionComponent', () => { it('should contain a view less link', () => { const viewLess = fixture.debugElement.query(By.css('.bitstream-collapse')); - expect(viewLess).toBeDefined(); + expect(viewLess).not.toBeNull(); }); it('clicking on the view less link should reset the pages and call getNextPage()', () => { diff --git a/src/app/item-page/simple/field-components/file-section/file-section.component.ts b/src/app/item-page/simple/field-components/file-section/file-section.component.ts index 08e792fc8b7..3c41731c5f4 100644 --- a/src/app/item-page/simple/field-components/file-section/file-section.component.ts +++ b/src/app/item-page/simple/field-components/file-section/file-section.component.ts @@ -11,6 +11,7 @@ import { NotificationsService } from '../../../../shared/notifications/notificat import { TranslateService } from '@ngx-translate/core'; import { getFirstCompletedRemoteData } from '../../../../core/shared/operators'; import { AppConfig, APP_CONFIG } from 'src/config/app-config.interface'; +import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; /** * This component renders the file section of the item @@ -42,6 +43,7 @@ export class FileSectionComponent implements OnInit { protected bitstreamDataService: BitstreamDataService, protected notificationsService: NotificationsService, protected translateService: TranslateService, + public dsoNameService: DSONameService, @Inject(APP_CONFIG) protected appConfig: AppConfig ) { this.pageSize = this.appConfig.item.bitstream.pageSize; diff --git a/src/app/item-page/simple/field-components/specific-field/abstract/item-page-abstract-field.component.spec.ts b/src/app/item-page/simple/field-components/specific-field/abstract/item-page-abstract-field.component.spec.ts index bfed3847c51..4fb88894401 100644 --- a/src/app/item-page/simple/field-components/specific-field/abstract/item-page-abstract-field.component.spec.ts +++ b/src/app/item-page/simple/field-components/specific-field/abstract/item-page-abstract-field.component.spec.ts @@ -44,6 +44,6 @@ describe('ItemPageAbstractFieldComponent', () => { })); it('should render a ds-metadata-values', () => { - expect(fixture.debugElement.query(By.css('ds-metadata-values'))).toBeDefined(); + expect(fixture.debugElement.query(By.css('ds-metadata-values'))).not.toBeNull(); }); }); diff --git a/src/app/item-page/simple/field-components/specific-field/title/item-page-title-field.component.html b/src/app/item-page/simple/field-components/specific-field/title/item-page-title-field.component.html index 36e48e769c5..67305ef73c6 100644 --- a/src/app/item-page/simple/field-components/specific-field/title/item-page-title-field.component.html +++ b/src/app/item-page/simple/field-components/specific-field/title/item-page-title-field.component.html @@ -1,8 +1,8 @@ -

    +

    {{ type.toLowerCase() + '.page.titleprefix' | translate }}
    {{ dsoNameService.getName(item) }}
    -

    +

    diff --git a/src/app/item-page/simple/field-components/specific-field/title/themed-item-page-field.component.ts b/src/app/item-page/simple/field-components/specific-field/title/themed-item-page-field.component.ts new file mode 100644 index 00000000000..7007b8fed3f --- /dev/null +++ b/src/app/item-page/simple/field-components/specific-field/title/themed-item-page-field.component.ts @@ -0,0 +1,33 @@ +import { Component, Input } from '@angular/core'; +import { ThemedComponent } from '../../../../../shared/theme-support/themed.component'; +import { ItemPageTitleFieldComponent } from './item-page-title-field.component'; +import { Item } from '../../../../../core/shared/item.model'; + +/** + * Themed wrapper for {@link ItemPageTitleFieldComponent} + */ +@Component({ + selector: 'ds-themed-item-page-title-field', + styleUrls: [], + templateUrl: '../../../../../shared/theme-support/themed.component.html', +}) +export class ThemedItemPageTitleFieldComponent extends ThemedComponent { + + protected inAndOutputNames: (keyof ItemPageTitleFieldComponent & keyof this)[] = [ + 'item', + ]; + + @Input() item: Item; + + protected getComponentName(): string { + return 'ItemPageTitleFieldComponent'; + } + + protected importThemedComponent(themeName: string): Promise { + return import(`../../../../../../themes/${themeName}/app/item-page/simple/field-components/specific-field/title/item-page-title-field.component`); + } + + protected importUnthemedComponent(): Promise { + return import('./item-page-title-field.component'); + } +} diff --git a/src/app/item-page/simple/item-page.component.html b/src/app/item-page/simple/item-page.component.html index f09e954a7d9..a7263a43be8 100644 --- a/src/app/item-page/simple/item-page.component.html +++ b/src/app/item-page/simple/item-page.component.html @@ -4,7 +4,7 @@ *ngVar="(itemRD$ | async) as itemRD">
    - + diff --git a/src/app/item-page/simple/item-page.component.spec.ts b/src/app/item-page/simple/item-page.component.spec.ts index 9b0e87939df..b3202108f43 100644 --- a/src/app/item-page/simple/item-page.component.spec.ts +++ b/src/app/item-page/simple/item-page.component.spec.ts @@ -2,7 +2,7 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock'; import { ItemDataService } from '../../core/data/item-data.service'; -import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; +import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA, PLATFORM_ID } from '@angular/core'; import { ItemPageComponent } from './item-page.component'; import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRouteStub } from '../../shared/testing/active-router.stub'; @@ -22,6 +22,10 @@ import { import { AuthService } from '../../core/auth/auth.service'; import { createPaginatedList } from '../../shared/testing/utils.test'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; +import { ServerResponseService } from '../../core/services/server-response.service'; +import { SignpostingDataService } from '../../core/data/signposting-data.service'; +import { LinkDefinition, LinkHeadService } from '../../core/services/link-head.service'; +import { SignpostingLink } from '../../core/data/signposting-links.model'; const mockItem: Item = Object.assign(new Item(), { bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])), @@ -36,11 +40,28 @@ const mockWithdrawnItem: Item = Object.assign(new Item(), { isWithdrawn: true }); +const mocklink = { + href: 'http://test.org', + rel: 'rel1', + type: 'type1' +}; + +const mocklink2 = { + href: 'http://test2.org', + rel: 'rel2', + type: undefined +}; + +const mockSignpostingLinks: SignpostingLink[] = [mocklink, mocklink2]; + describe('ItemPageComponent', () => { let comp: ItemPageComponent; let fixture: ComponentFixture; let authService: AuthService; let authorizationDataService: AuthorizationDataService; + let serverResponseService: jasmine.SpyObj; + let signpostingDataService: jasmine.SpyObj; + let linkHeadService: jasmine.SpyObj; const mockMetadataService = { /* eslint-disable no-empty,@typescript-eslint/no-empty-function */ @@ -60,6 +81,18 @@ describe('ItemPageComponent', () => { authorizationDataService = jasmine.createSpyObj('authorizationDataService', { isAuthorized: observableOf(false), }); + serverResponseService = jasmine.createSpyObj('ServerResponseService', { + setHeader: jasmine.createSpy('setHeader'), + }); + + signpostingDataService = jasmine.createSpyObj('SignpostingDataService', { + getLinks: observableOf([mocklink, mocklink2]), + }); + + linkHeadService = jasmine.createSpyObj('LinkHeadService', { + addTag: jasmine.createSpy('setHeader'), + removeTag: jasmine.createSpy('removeTag'), + }); TestBed.configureTestingModule({ imports: [TranslateModule.forRoot({ @@ -76,6 +109,10 @@ describe('ItemPageComponent', () => { { provide: Router, useValue: {} }, { provide: AuthService, useValue: authService }, { provide: AuthorizationDataService, useValue: authorizationDataService }, + { provide: ServerResponseService, useValue: serverResponseService }, + { provide: SignpostingDataService, useValue: signpostingDataService }, + { provide: LinkHeadService, useValue: linkHeadService }, + { provide: PLATFORM_ID, useValue: 'server' }, ], schemas: [NO_ERRORS_SCHEMA] @@ -126,6 +163,33 @@ describe('ItemPageComponent', () => { const objectLoader = fixture.debugElement.query(By.css('ds-listable-object-component-loader')); expect(objectLoader.nativeElement).toBeDefined(); }); + + it('should add the signposting links', () => { + expect(serverResponseService.setHeader).toHaveBeenCalled(); + expect(linkHeadService.addTag).toHaveBeenCalledTimes(2); + }); + + + it('should add link tags correctly', () => { + + expect(comp.signpostingLinks).toEqual([mocklink, mocklink2]); + + // Check if linkHeadService.addTag() was called with the correct arguments + expect(linkHeadService.addTag).toHaveBeenCalledTimes(mockSignpostingLinks.length); + let expected: LinkDefinition = mockSignpostingLinks[0] as LinkDefinition; + expect(linkHeadService.addTag).toHaveBeenCalledWith(expected); + expected = { + href: 'http://test2.org', + rel: 'rel2' + }; + expect(linkHeadService.addTag).toHaveBeenCalledWith(expected); + }); + + it('should set Link header on the server', () => { + + expect(serverResponseService.setHeader).toHaveBeenCalledWith('Link', ' ; rel="rel1" ; type="type1" , ; rel="rel2" '); + }); + }); describe('when the item is withdrawn and the user is not an admin', () => { beforeEach(() => { @@ -150,6 +214,11 @@ describe('ItemPageComponent', () => { const objectLoader = fixture.debugElement.query(By.css('ds-listable-object-component-loader')); expect(objectLoader.nativeElement).toBeDefined(); }); + + it('should add the signposting links', () => { + expect(serverResponseService.setHeader).toHaveBeenCalled(); + expect(linkHeadService.addTag).toHaveBeenCalledTimes(2); + }); }); describe('when the item is not withdrawn and the user is not an admin', () => { @@ -162,6 +231,11 @@ describe('ItemPageComponent', () => { const objectLoader = fixture.debugElement.query(By.css('ds-listable-object-component-loader')); expect(objectLoader.nativeElement).toBeDefined(); }); + + it('should add the signposting links', () => { + expect(serverResponseService.setHeader).toHaveBeenCalled(); + expect(linkHeadService.addTag).toHaveBeenCalledTimes(2); + }); }); }); diff --git a/src/app/item-page/simple/item-page.component.ts b/src/app/item-page/simple/item-page.component.ts index 3234e0402c2..089e82566f5 100644 --- a/src/app/item-page/simple/item-page.component.ts +++ b/src/app/item-page/simple/item-page.component.ts @@ -1,8 +1,9 @@ -import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; +import { isPlatformServer } from '@angular/common'; import { Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; +import { map, take } from 'rxjs/operators'; import { ItemDataService } from '../../core/data/item-data.service'; import { RemoteData } from '../../core/data/remote-data'; @@ -15,6 +16,11 @@ import { getItemPageRoute } from '../item-page-routing-paths'; import { redirectOn204, redirectOn4xx } from '../../core/shared/authorized.operators'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; import { FeatureID } from '../../core/data/feature-authorization/feature-id'; +import { ServerResponseService } from '../../core/services/server-response.service'; +import { SignpostingDataService } from '../../core/data/signposting-data.service'; +import { SignpostingLink } from '../../core/data/signposting-links.model'; +import { isNotEmpty } from '../../shared/empty.util'; +import { LinkDefinition, LinkHeadService } from '../../core/services/link-head.service'; import { CrisLayoutTab } from '../../core/layout/models/tab.model'; import { PaginatedList } from '../../core/data/paginated-list.model'; @@ -30,7 +36,7 @@ import { PaginatedList } from '../../core/data/paginated-list.model'; changeDetection: ChangeDetectionStrategy.OnPush, animations: [fadeInOut] }) -export class ItemPageComponent implements OnInit { +export class ItemPageComponent implements OnInit, OnDestroy { /** * The item's id @@ -59,6 +65,11 @@ export class ItemPageComponent implements OnInit { itemUrl: string; + /** + * Contains a list of SignpostingLink related to the item + */ + signpostingLinks: SignpostingLink[] = []; + /** * The configured tabs for layout of current item */ @@ -66,11 +77,16 @@ export class ItemPageComponent implements OnInit { constructor( protected route: ActivatedRoute, - private router: Router, - private items: ItemDataService, - private authService: AuthService, - private authorizationService: AuthorizationDataService + protected router: Router, + protected items: ItemDataService, + protected authService: AuthService, + protected authorizationService: AuthorizationDataService, + protected responseService: ServerResponseService, + protected signpostingDataService: SignpostingDataService, + protected linkHeadService: LinkHeadService, + @Inject(PLATFORM_ID) protected platformId: string ) { + this.initPageLinks(); } /** @@ -93,4 +109,42 @@ export class ItemPageComponent implements OnInit { this.isAdmin$ = this.authorizationService.isAuthorized(FeatureID.AdministratorOf); } + + /** + * Create page links if any are retrieved by signposting endpoint + * + * @private + */ + private initPageLinks(): void { + this.route.params.subscribe(params => { + this.signpostingDataService.getLinks(params.id).pipe(take(1)).subscribe((signpostingLinks: SignpostingLink[]) => { + let links = ''; + this.signpostingLinks = signpostingLinks; + + signpostingLinks.forEach((link: SignpostingLink) => { + links = links + (isNotEmpty(links) ? ', ' : '') + `<${link.href}> ; rel="${link.rel}"` + (isNotEmpty(link.type) ? ` ; type="${link.type}" ` : ' '); + let tag: LinkDefinition = { + href: link.href, + rel: link.rel + }; + if (isNotEmpty(link.type)) { + tag = Object.assign(tag, { + type: link.type + }); + } + this.linkHeadService.addTag(tag); + }); + + if (isPlatformServer(this.platformId)) { + this.responseService.setHeader('Link', links); + } + }); + }); + } + + ngOnDestroy(): void { + this.signpostingLinks.forEach((link: SignpostingLink) => { + this.linkHeadService.removeTag(`href='${link.href}'`); + }); + } } diff --git a/src/app/item-page/simple/item-types/publication/publication.component.html b/src/app/item-page/simple/item-types/publication/publication.component.html index f4d4e627d6f..cd7249e5526 100644 --- a/src/app/item-page/simple/item-types/publication/publication.component.html +++ b/src/app/item-page/simple/item-types/publication/publication.component.html @@ -9,28 +9,28 @@
    - - + +
    - + - + - - - +
    + +
    - - + diff --git a/src/app/item-page/simple/item-types/shared/item-relationships-utils.ts b/src/app/item-page/simple/item-types/shared/item-relationships-utils.ts index 56d59f81472..29e6bc149ac 100644 --- a/src/app/item-page/simple/item-types/shared/item-relationships-utils.ts +++ b/src/app/item-page/simple/item-types/shared/item-relationships-utils.ts @@ -4,7 +4,7 @@ import { PaginatedList } from '../../../../core/data/paginated-list.model'; import { RemoteData } from '../../../../core/data/remote-data'; import { Relationship } from '../../../../core/shared/item-relationships/relationship.model'; import { Item } from '../../../../core/shared/item.model'; -import { getFirstSucceededRemoteData, getFirstSucceededRemoteDataPayload } from '../../../../core/shared/operators'; +import { getFirstCompletedRemoteData } from '../../../../core/shared/operators'; import { hasValue } from '../../../../shared/empty.util'; import { InjectionToken } from '@angular/core'; @@ -74,24 +74,42 @@ export const relationsToItems = (thisId: string) => * @param {string} thisId The item's id of which the relations belong to * @returns {(source: Observable) => Observable} */ -export const paginatedRelationsToItems = (thisId: string) => - (source: Observable>>): Observable>> => +export const paginatedRelationsToItems = (thisId: string) => (source: Observable>>): Observable>> => source.pipe( - getFirstSucceededRemoteData(), + getFirstCompletedRemoteData(), switchMap((relationshipsRD: RemoteData>) => { return observableCombineLatest( relationshipsRD.payload.page.map((rel: Relationship) => observableCombineLatest([ - rel.leftItem.pipe(getFirstSucceededRemoteDataPayload()), - rel.rightItem.pipe(getFirstSucceededRemoteDataPayload())] + rel.leftItem.pipe( + getFirstCompletedRemoteData(), + map((rd: RemoteData) => { + if (rd.hasSucceeded) { + return rd.payload; + } else { + return null; + } + }) + ), + rel.rightItem.pipe( + getFirstCompletedRemoteData(), + map((rd: RemoteData) => { + if (rd.hasSucceeded) { + return rd.payload; + } else { + return null; + } + }) + ), + ] ) - )).pipe( + ) + ).pipe( map((arr) => - arr - .map(([leftItem, rightItem]) => { - if (leftItem.id === thisId) { + arr.map(([leftItem, rightItem]) => { + if (hasValue(leftItem) && leftItem.id === thisId) { return rightItem; - } else if (rightItem.id === thisId) { + } else if (hasValue(rightItem) && rightItem.id === thisId) { return leftItem; } }) diff --git a/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.html b/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.html index aeeaa76d77a..2fc443fcc3d 100644 --- a/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.html +++ b/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.html @@ -10,28 +10,28 @@
    - - + +
    - + - + - - - +
    + +
    - - + diff --git a/src/app/item-page/simple/metadata-representation-list/metadata-representation-list.component.html b/src/app/item-page/simple/metadata-representation-list/metadata-representation-list.component.html index 65660eaa346..efbe9206d18 100644 --- a/src/app/item-page/simple/metadata-representation-list/metadata-representation-list.component.html +++ b/src/app/item-page/simple/metadata-representation-list/metadata-representation-list.component.html @@ -7,12 +7,12 @@
    - {{'item.page.related-items.view-more' | - translate:{ amount: (total - (objects.length * incrementBy) < incrementBy) ? total - (objects.length * incrementBy) : incrementBy } }} +
    - {{'item.page.related-items.view-less' | - translate:{ amount: representations?.length } }} +
    diff --git a/src/app/item-page/simple/metadata-representation-list/metadata-representation-list.component.ts b/src/app/item-page/simple/metadata-representation-list/metadata-representation-list.component.ts index d5e6547778a..59a5377f772 100644 --- a/src/app/item-page/simple/metadata-representation-list/metadata-representation-list.component.ts +++ b/src/app/item-page/simple/metadata-representation-list/metadata-representation-list.component.ts @@ -59,8 +59,10 @@ export class MetadataRepresentationListComponent extends AbstractIncrementalList */ total: number; - constructor(public relationshipService: RelationshipDataService, - private browseDefinitionDataService: BrowseDefinitionDataService) { + constructor( + public relationshipService: RelationshipDataService, + protected browseDefinitionDataService: BrowseDefinitionDataService, + ) { super(); } diff --git a/src/app/item-page/simple/metadata-representation-list/themed-metadata-representation-list.component.ts b/src/app/item-page/simple/metadata-representation-list/themed-metadata-representation-list.component.ts new file mode 100644 index 00000000000..a290b82dd9d --- /dev/null +++ b/src/app/item-page/simple/metadata-representation-list/themed-metadata-representation-list.component.ts @@ -0,0 +1,35 @@ +import { ThemedComponent } from '../../../shared/theme-support/themed.component'; +import { MetadataRepresentationListComponent } from './metadata-representation-list.component'; +import { Component, Input } from '@angular/core'; +import { Item } from '../../../core/shared/item.model'; + +@Component({ + selector: 'ds-themed-metadata-representation-list', + styleUrls: [], + templateUrl: '../../../shared/theme-support/themed.component.html', +}) +export class ThemedMetadataRepresentationListComponent extends ThemedComponent { + protected inAndOutputNames: (keyof MetadataRepresentationListComponent & keyof this)[] = ['parentItem', 'itemType', 'metadataFields', 'label', 'incrementBy']; + + @Input() parentItem: Item; + + @Input() itemType: string; + + @Input() metadataFields: string[]; + + @Input() label: string; + + @Input() incrementBy: number; + + protected getComponentName(): string { + return 'MetadataRepresentationListComponent'; + } + + protected importThemedComponent(themeName: string): Promise { + return import(`../../../../themes/${themeName}/app/item-page/simple/metadata-representation-list/metadata-representation-list.component`); + } + + protected importUnthemedComponent(): Promise { + return import(`./metadata-representation-list.component`); + } +} diff --git a/src/app/item-page/simple/related-entities/related-entities-search/related-entities-search.component.html b/src/app/item-page/simple/related-entities/related-entities-search/related-entities-search.component.html index 2a08efeb2ca..36340bebfa0 100644 --- a/src/app/item-page/simple/related-entities/related-entities-search/related-entities-search.component.html +++ b/src/app/item-page/simple/related-entities/related-entities-search/related-entities-search.component.html @@ -2,5 +2,6 @@ [fixedFilterQuery]="fixedFilter" [configuration]="configuration" [searchEnabled]="searchEnabled" - [sideBarWidth]="sideBarWidth"> + [sideBarWidth]="sideBarWidth" + [showCsvExport]="true"> diff --git a/src/app/item-page/simple/related-items/related-items.component.html b/src/app/item-page/simple/related-items/related-items.component.html index 0d1e14941d8..bee1f345fd0 100644 --- a/src/app/item-page/simple/related-items/related-items.component.html +++ b/src/app/item-page/simple/related-items/related-items.component.html @@ -7,12 +7,12 @@
    - {{'item.page.related-items.view-more' | - translate:{ amount: (itemsRD?.payload?.totalElements - (incrementBy * objects.length) < incrementBy) ? itemsRD?.payload?.totalElements - (incrementBy * objects.length) : incrementBy } }} +
    - {{'item.page.related-items.view-less' | - translate:{ amount: itemsRD?.payload?.page?.length } }} +
    diff --git a/src/app/item-page/versions/item-versions.component.spec.ts b/src/app/item-page/versions/item-versions.component.spec.ts index c3eff29f8bf..41dc382083b 100644 --- a/src/app/item-page/versions/item-versions.component.spec.ts +++ b/src/app/item-page/versions/item-versions.component.spec.ts @@ -18,7 +18,7 @@ import { PaginationServiceStub } from '../../shared/testing/pagination-service.s import { AuthService } from '../../core/auth/auth.service'; import { VersionDataService } from '../../core/data/version-data.service'; import { ItemDataService } from '../../core/data/item-data.service'; -import { FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { UntypedFormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; @@ -142,7 +142,7 @@ describe('ItemVersionsComponent', () => { imports: [TranslateModule.forRoot(), CommonModule, FormsModule, ReactiveFormsModule, BrowserModule, ItemSharedModule], providers: [ {provide: PaginationService, useValue: new PaginationServiceStub()}, - {provide: FormBuilder, useValue: new FormBuilder()}, + {provide: UntypedFormBuilder, useValue: new UntypedFormBuilder()}, {provide: NotificationsService, useValue: new NotificationsServiceStub()}, {provide: AuthService, useValue: authenticationServiceSpy}, {provide: AuthorizationDataService, useValue: authorizationServiceSpy}, diff --git a/src/app/item-page/versions/item-versions.component.ts b/src/app/item-page/versions/item-versions.component.ts index defe9fe7ddc..6ca6abfbb11 100644 --- a/src/app/item-page/versions/item-versions.component.ts +++ b/src/app/item-page/versions/item-versions.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, OnInit } from '@angular/core'; +import { Component, Input, OnDestroy, OnInit } from '@angular/core'; import { Item } from '../../core/shared/item.model'; import { Version } from '../../core/shared/version.model'; import { RemoteData } from '../../core/data/remote-data'; @@ -23,7 +23,7 @@ import { PaginatedList } from '../../core/data/paginated-list.model'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { VersionHistoryDataService } from '../../core/data/version-history-data.service'; import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model'; -import { AlertType } from '../../shared/alert/aletr-type'; +import { AlertType } from '../../shared/alert/alert-type'; import { followLink } from '../../shared/utils/follow-link-config.model'; import { hasValue, hasValueOperator } from '../../shared/empty.util'; import { PaginationService } from '../../core/pagination/pagination.service'; @@ -32,7 +32,7 @@ import { getItemPageRoute, getItemVersionRoute } from '../item-page-routing-paths'; -import { FormBuilder } from '@angular/forms'; +import { UntypedFormBuilder } from '@angular/forms'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { ItemVersionsSummaryModalComponent } from './item-versions-summary-modal/item-versions-summary-modal.component'; import { NotificationsService } from '../../shared/notifications/notifications.service'; @@ -59,7 +59,7 @@ import { UUIDService } from '../../core/shared/uuid.service'; /** * Component listing all available versions of the history the provided item is a part of */ -export class ItemVersionsComponent implements OnInit { +export class ItemVersionsComponent implements OnDestroy, OnInit { /** * The item to display a version history for @@ -172,7 +172,7 @@ export class ItemVersionsComponent implements OnInit { private versionService: VersionDataService, private itemService: ItemDataService, private paginationService: PaginationService, - private formBuilder: FormBuilder, + private formBuilder: UntypedFormBuilder, private modalService: NgbModal, private notificationsService: NotificationsService, private translateService: TranslateService, diff --git a/src/app/item-page/versions/notice/item-versions-notice.component.ts b/src/app/item-page/versions/notice/item-versions-notice.component.ts index 8a8f5ff76f7..0e5e45806b7 100644 --- a/src/app/item-page/versions/notice/item-versions-notice.component.ts +++ b/src/app/item-page/versions/notice/item-versions-notice.component.ts @@ -12,7 +12,7 @@ import { } from '../../../core/shared/operators'; import { map, startWith, switchMap } from 'rxjs/operators'; import { VersionHistoryDataService } from '../../../core/data/version-history-data.service'; -import { AlertType } from '../../../shared/alert/aletr-type'; +import { AlertType } from '../../../shared/alert/alert-type'; import { getItemPageRoute } from '../../item-page-routing-paths'; @Component({ diff --git a/src/app/login-page/login-page.component.html b/src/app/login-page/login-page.component.html index 2a95e0ce1c5..c38444bec8c 100644 --- a/src/app/login-page/login-page.component.html +++ b/src/app/login-page/login-page.component.html @@ -3,8 +3,8 @@

    {{"login.form.header" | translate}}

    - +
    diff --git a/src/app/lookup-by-id/objectgone/objectgone.component.ts b/src/app/lookup-by-id/objectgone/objectgone.component.ts index 08b9e86a76f..e3e92ced10f 100644 --- a/src/app/lookup-by-id/objectgone/objectgone.component.ts +++ b/src/app/lookup-by-id/objectgone/objectgone.component.ts @@ -1,5 +1,5 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; -import { AlertType } from '../../shared/alert/aletr-type'; +import { AlertType } from '../../shared/alert/alert-type'; @Component({ selector: 'ds-objectgone', diff --git a/src/app/menu.resolver.spec.ts b/src/app/menu.resolver.spec.ts index d9f50089e52..0aa6668189d 100644 --- a/src/app/menu.resolver.spec.ts +++ b/src/app/menu.resolver.spec.ts @@ -72,6 +72,7 @@ describe('MenuResolver', () => { beforeEach(waitForAsync(() => { menuService = new MenuServiceStub(); spyOn(menuService, 'getMenu').and.returnValue(observableOf(MENU_STATE)); + spyOn(menuService, 'addSection'); sectionsService = jasmine.createSpyObj('SectionDataService', { findVisibleSections: createSuccessfulRemoteDataObject$(createPaginatedList(EXPLORE_SECTIONS_DEFINITIONS)) @@ -101,8 +102,6 @@ describe('MenuResolver', () => { schemas: [NO_ERRORS_SCHEMA] }); resolver = TestBed.inject(MenuResolver); - - spyOn(menuService, 'addSection'); })); it('should be created', () => { diff --git a/src/app/menu.resolver.ts b/src/app/menu.resolver.ts index af7f80eb69b..4eaca1ab6a6 100644 --- a/src/app/menu.resolver.ts +++ b/src/app/menu.resolver.ts @@ -659,6 +659,19 @@ export class MenuResolver implements Resolve { createSiteAdministratorMenuSections() { this.authorizationService.isAuthorized(FeatureID.AdministratorOf).subscribe((authorized) => { const menuList = [ + /* Communities & Collections */ + { + id: 'browse_global_communities_and_collections', + active: false, + visible: authorized && !environment.layout.navbar.showCommunityCollection, + model: { + type: MenuItemType.LINK, + text: `menu.section.communities_and_collections`, + link: `/community-list` + } as LinkMenuItemModel, + icon: 'list-alt', + index: 2 + }, /* Notifications */ { id: 'notifications', @@ -846,6 +859,17 @@ export class MenuResolver implements Resolve { link: '/access-control/groups' } as LinkMenuItemModel, }, + { + id: 'access_control_bulk', + parentID: 'access_control', + active: false, + visible: isSiteAdmin, + model: { + type: MenuItemType.LINK, + text: 'menu.section.access_control_bulk', + link: '/access-control/bulk-access' + } as LinkMenuItemModel, + }, // TODO: enable this menu item once the feature has been implemented // { // id: 'access_control_authorizations', diff --git a/src/app/my-dspace-page/my-dspace-search.module.ts b/src/app/my-dspace-page/my-dspace-search.module.ts index 5df12baaf7b..eeec2e01fb6 100644 --- a/src/app/my-dspace-page/my-dspace-search.module.ts +++ b/src/app/my-dspace-page/my-dspace-search.module.ts @@ -17,12 +17,8 @@ import { PoolSearchResultDetailElementComponent } from '../shared/object-detail/ import { ClaimedApprovedSearchResultListElementComponent } from '../shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-approved-search-result/claimed-approved-search-result-list-element.component'; import { ClaimedDeclinedSearchResultListElementComponent } from '../shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-declined-search-result/claimed-declined-search-result-list-element.component'; import { ResearchEntitiesModule } from '../entity-groups/research-entities/research-entities.module'; -import { ItemCollectionComponent } from '../shared/object-collection/shared/mydspace-item-collection/item-collection.component'; import { ItemDetailPreviewComponent } from '../shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component'; import { ItemDetailPreviewFieldComponent } from '../shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview-field/item-detail-preview-field.component'; -import { ItemListPreviewComponent } from '../shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component'; -import { ThemedItemListPreviewComponent } from '../shared/object-list/my-dspace-result-list-element/item-list-preview/themed-item-list-preview.component'; -import { MyDSpaceItemStatusComponent } from '../shared/object-collection/shared/mydspace-item-status/my-dspace-item-status.component'; import { JournalEntitiesModule } from '../entity-groups/journal-entities/journal-entities.module'; import { MyDSpaceActionsModule } from '../shared/mydspace-actions/mydspace-actions.module'; import { ClaimedDeclinedTaskSearchResultListElementComponent } from '../shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-declined-task-search-result/claimed-declined-task-search-result-list-element.component'; @@ -45,12 +41,8 @@ const ENTRY_COMPONENTS = [ const DECLARATIONS = [ ...ENTRY_COMPONENTS, - ItemCollectionComponent, ItemDetailPreviewComponent, ItemDetailPreviewFieldComponent, - ItemListPreviewComponent, - ThemedItemListPreviewComponent, - MyDSpaceItemStatusComponent, ]; @NgModule({ diff --git a/src/app/my-dspace-page/themed-my-dspace-page.component.ts b/src/app/my-dspace-page/themed-my-dspace-page.component.ts index 2c74da052e8..55ebc51c8d6 100644 --- a/src/app/my-dspace-page/themed-my-dspace-page.component.ts +++ b/src/app/my-dspace-page/themed-my-dspace-page.component.ts @@ -11,7 +11,6 @@ import { MyDSpacePageComponent } from './my-dspace-page.component'; templateUrl: './../shared/theme-support/themed.component.html' }) export class ThemedMyDSpacePageComponent extends ThemedComponent { - protected inAndOutputNames: (keyof MyDSpacePageComponent & keyof this)[]; protected getComponentName(): string { return 'MyDSpacePageComponent'; diff --git a/src/app/navbar/expandable-navbar-section/expandable-navbar-section.component.html b/src/app/navbar/expandable-navbar-section/expandable-navbar-section.component.html index b5023261647..053968834e1 100644 --- a/src/app/navbar/expandable-navbar-section/expandable-navbar-section.component.html +++ b/src/app/navbar/expandable-navbar-section/expandable-navbar-section.component.html @@ -14,9 +14,9 @@
    diff --git a/src/app/navbar/expandable-navbar-section/expandable-navbar-section.component.scss b/src/app/navbar/expandable-navbar-section/expandable-navbar-section.component.scss index 65de77b6007..28db981f115 100644 --- a/src/app/navbar/expandable-navbar-section/expandable-navbar-section.component.scss +++ b/src/app/navbar/expandable-navbar-section/expandable-navbar-section.component.scss @@ -6,14 +6,20 @@ } .dropdown-menu { + background-color: var(--ds-expandable-navbar-bg); overflow: hidden; min-width: 100%; border-top-left-radius: 0; border-top-right-radius: 0; ::ng-deep a.nav-link { + color: var(--ds-expandable-navbar-link-color) !important; padding-right: var(--bs-spacer); padding-left: var(--bs-spacer); white-space: nowrap; + + &:hover, &:focus { + color: var(--ds-expandable-navbar-link-color-hover) !important; + } } } diff --git a/src/app/navbar/expandable-navbar-section/expandable-navbar-section.component.ts b/src/app/navbar/expandable-navbar-section/expandable-navbar-section.component.ts index 5bc69bcbb4e..d32fa46a327 100644 --- a/src/app/navbar/expandable-navbar-section/expandable-navbar-section.component.ts +++ b/src/app/navbar/expandable-navbar-section/expandable-navbar-section.component.ts @@ -4,7 +4,6 @@ import { MenuService } from '../../shared/menu/menu.service'; import { slide } from '../../shared/animations/slide'; import { first } from 'rxjs/operators'; import { HostWindowService } from '../../shared/host-window.service'; -import { rendersSectionForMenu } from '../../shared/menu/menu-section.decorator'; import { MenuID } from '../../shared/menu/menu-id.model'; /** @@ -16,7 +15,6 @@ import { MenuID } from '../../shared/menu/menu-id.model'; styleUrls: ['./expandable-navbar-section.component.scss'], animations: [slide] }) -@rendersSectionForMenu(MenuID.PUBLIC, true) export class ExpandableNavbarSectionComponent extends NavbarSectionComponent implements OnInit { /** * This section resides in the Public Navbar diff --git a/src/app/navbar/expandable-navbar-section/themed-expandable-navbar-section.component.ts b/src/app/navbar/expandable-navbar-section/themed-expandable-navbar-section.component.ts index e33dca41049..8f474e99490 100644 --- a/src/app/navbar/expandable-navbar-section/themed-expandable-navbar-section.component.ts +++ b/src/app/navbar/expandable-navbar-section/themed-expandable-navbar-section.component.ts @@ -8,8 +8,7 @@ import { MenuID } from '../../shared/menu/menu-id.model'; * Themed wrapper for ExpandableNavbarSectionComponent */ @Component({ - /* eslint-disable @angular-eslint/component-selector */ - selector: 'li[ds-themed-expandable-navbar-section]', + selector: 'ds-themed-expandable-navbar-section', styleUrls: [], templateUrl: '../../shared/theme-support/themed.component.html', }) diff --git a/src/app/navbar/navbar-section/navbar-section.component.ts b/src/app/navbar/navbar-section/navbar-section.component.ts index 9f75a96f6e7..9b86aa10f2b 100644 --- a/src/app/navbar/navbar-section/navbar-section.component.ts +++ b/src/app/navbar/navbar-section/navbar-section.component.ts @@ -8,8 +8,7 @@ import { MenuID } from '../../shared/menu/menu-id.model'; * Represents a non-expandable section in the navbar */ @Component({ - /* eslint-disable @angular-eslint/component-selector */ - selector: 'li[ds-navbar-section]', + selector: 'ds-navbar-section', templateUrl: './navbar-section.component.html', styleUrls: ['./navbar-section.component.scss'] }) diff --git a/src/app/navbar/navbar.component.html b/src/app/navbar/navbar.component.html index bc1e04f5130..b691cfb3f9e 100644 --- a/src/app/navbar/navbar.component.html +++ b/src/app/navbar/navbar.component.html @@ -6,11 +6,11 @@
    diff --git a/src/app/navbar/navbar.component.scss b/src/app/navbar/navbar.component.scss index 441ee82c968..dac8c0927f3 100644 --- a/src/app/navbar/navbar.component.scss +++ b/src/app/navbar/navbar.component.scss @@ -1,5 +1,5 @@ nav.navbar { - border-bottom: 1px var(--ds-header-navbar-border-bottom-color) solid; + background-color: var(--ds-navbar-bg); align-items: baseline; } @@ -11,9 +11,11 @@ nav.navbar { position: absolute; overflow: hidden; height: 0; + z-index: var(--ds-nav-z-index); &.open { height: auto; min-height: 100vh; //doesn't matter because wrapper is sticky + border-bottom: 1px var(--ds-header-navbar-border-bottom-color) solid; // open navbar covers header-navbar-wrapper border } } } @@ -38,8 +40,9 @@ nav.navbar { .navbar-nav { ::ng-deep a.nav-link { color: var(--ds-navbar-link-color); - } - ::ng-deep a.nav-link:hover { - color: var(--ds-navbar-link-color-hover); + + &:hover, &:focus { + color: var(--ds-navbar-link-color-hover); + } } } diff --git a/src/app/navbar/navbar.component.spec.ts b/src/app/navbar/navbar.component.spec.ts index 102af33b5f6..2f2767a2266 100644 --- a/src/app/navbar/navbar.component.spec.ts +++ b/src/app/navbar/navbar.component.spec.ts @@ -16,7 +16,6 @@ import { RouterTestingModule } from '@angular/router/testing'; import { BrowseService } from '../core/browse/browse.service'; import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; import { buildPaginatedList } from '../core/data/paginated-list.model'; -import { BrowseDefinition } from '../core/shared/browse-definition.model'; import { BrowseByDataType } from '../browse-by/browse-by-switcher/browse-by-decorator'; import { Item } from '../core/shared/item.model'; import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service'; @@ -29,6 +28,9 @@ import { authReducer } from '../core/auth/auth.reducer'; import { provideMockStore } from '@ngrx/store/testing'; import { AuthTokenInfo } from '../core/auth/models/auth-token-info.model'; import { EPersonMock } from '../shared/testing/eperson.mock'; +import { FlatBrowseDefinition } from '../core/shared/flat-browse-definition.model'; +import { ValueListBrowseDefinition } from '../core/shared/value-list-browse-definition.model'; +import { HierarchicalBrowseDefinition } from '../core/shared/hierarchical-browse-definition.model'; let comp: NavbarComponent; let fixture: ComponentFixture; @@ -67,30 +69,35 @@ describe('NavbarComponent', () => { beforeEach(waitForAsync(() => { browseDefinitions = [ Object.assign( - new BrowseDefinition(), { + new FlatBrowseDefinition(), { id: 'title', dataType: BrowseByDataType.Title, } ), Object.assign( - new BrowseDefinition(), { + new FlatBrowseDefinition(), { id: 'dateissued', dataType: BrowseByDataType.Date, metadataKeys: ['dc.date.issued'] } ), Object.assign( - new BrowseDefinition(), { + new ValueListBrowseDefinition(), { id: 'author', dataType: BrowseByDataType.Metadata, } ), Object.assign( - new BrowseDefinition(), { + new ValueListBrowseDefinition(), { id: 'subject', dataType: BrowseByDataType.Metadata, } ), + Object.assign( + new HierarchicalBrowseDefinition(), { + id: 'srsc', + } + ), ]; initialState = { core: { diff --git a/src/app/openaire/broker/events/openaire-broker-events.component.ts b/src/app/openaire/broker/events/openaire-broker-events.component.ts index 1785f56fa62..75e778117ff 100644 --- a/src/app/openaire/broker/events/openaire-broker-events.component.ts +++ b/src/app/openaire/broker/events/openaire-broker-events.component.ts @@ -242,7 +242,7 @@ export class OpenaireBrokerEventsComponent implements OnInit { this.subs.push( this.openaireBrokerEventRestService.patchEvent(action, eventData.event, eventData.reason).pipe(getFirstCompletedRemoteData()) .subscribe((rd: RemoteData) => { - if (rd.isSuccess && rd.statusCode === 200) { + if (rd.hasSucceeded) { this.notificationsService.success( this.translateService.instant('openaire.broker.event.action.saved') ); diff --git a/src/app/openaire/broker/topics/openaire-broker-topics.effects.ts b/src/app/openaire/broker/topics/openaire-broker-topics.effects.ts index b590b122f52..3ba5cf527c5 100644 --- a/src/app/openaire/broker/topics/openaire-broker-topics.effects.ts +++ b/src/app/openaire/broker/topics/openaire-broker-topics.effects.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; -import { Actions, Effect, ofType } from '@ngrx/effects'; +import { Actions, createEffect, ofType } from '@ngrx/effects'; import { TranslateService } from '@ngx-translate/core'; import { catchError, map, switchMap, tap, withLatestFrom } from 'rxjs/operators'; import { of as observableOf } from 'rxjs'; @@ -26,7 +26,7 @@ export class OpenaireBrokerTopicsEffects { /** * Retrieve all OpenAIRE Broker topics managing pagination and errors. */ - @Effect() retrieveAllTopics$ = this.actions$.pipe( + retrieveAllTopics$ = createEffect(() => this.actions$.pipe( ofType(OpenaireBrokerTopicActionTypes.RETRIEVE_ALL_TOPICS), withLatestFrom(this.store$), switchMap(([action, currentState]: [RetrieveAllTopicsAction, any]) => { @@ -45,27 +45,27 @@ export class OpenaireBrokerTopicsEffects { }) ); }) - ); + )); /** * Show a notification on error. */ - @Effect({ dispatch: false }) retrieveAllTopicsErrorAction$ = this.actions$.pipe( + retrieveAllTopicsErrorAction$ = createEffect(() => this.actions$.pipe( ofType(OpenaireBrokerTopicActionTypes.RETRIEVE_ALL_TOPICS_ERROR), tap(() => { this.notificationsService.error(null, this.translate.get('openaire.broker.topic.error.service.retrieve')); }) - ); + ), { dispatch: false }); /** * Clear find all topics requests from cache. */ - @Effect({ dispatch: false }) addTopicsAction$ = this.actions$.pipe( + addTopicsAction$ = createEffect(() => this.actions$.pipe( ofType(OpenaireBrokerTopicActionTypes.ADD_TOPICS), tap(() => { this.openaireBrokerTopicDataService.clearFindAllTopicsRequests(); }) - ); + ), { dispatch: false }); /** * Initialize the effect class variables. diff --git a/src/app/openaire/openaire.module.ts b/src/app/openaire/openaire.module.ts index b3a9dcd8ff4..91767a51439 100644 --- a/src/app/openaire/openaire.module.ts +++ b/src/app/openaire/openaire.module.ts @@ -51,7 +51,8 @@ const MODULES = [ CoreModule.forRoot(), StoreModule.forFeature('openaire', openaireReducers, storeModuleConfig as StoreConfig), EffectsModule.forFeature(openaireEffects), - TranslateModule + TranslateModule, + SearchModule ]; const COMPONENTS = [ @@ -86,7 +87,6 @@ const PROVIDERS = [ @NgModule({ imports: [ ...MODULES, - SearchModule ], declarations: [ ...COMPONENTS, @@ -96,9 +96,6 @@ const PROVIDERS = [ providers: [ ...PROVIDERS ], - entryComponents: [ - ...ENTRY_COMPONENTS - ], exports: [ ...COMPONENTS, ...DIRECTIVES diff --git a/src/app/openaire/reciter-suggestions/selectors.ts b/src/app/openaire/reciter-suggestions/selectors.ts index d5db946817b..6b4b97cdd8b 100644 --- a/src/app/openaire/reciter-suggestions/selectors.ts +++ b/src/app/openaire/reciter-suggestions/selectors.ts @@ -1,16 +1,10 @@ -import { createFeatureSelector, createSelector, MemoizedSelector } from '@ngrx/store'; +import { createSelector, MemoizedSelector } from '@ngrx/store'; import { subStateSelector } from '../../shared/selector.util'; import { openaireSelector, OpenaireState } from '../openaire.reducer'; -import { OpenaireSuggestionTarget } from '../../core/openaire/reciter-suggestions/models/openaire-suggestion-target.model'; -import { SuggestionTargetState } from './suggestion-targets/suggestion-targets.reducer'; - -/** - * Returns the Reciter Suggestion Target state. - * @function _getReciterSuggestionTargetState - * @param {AppState} state Top level state. - * @return {OpenaireState} - */ -const _getReciterSuggestionTargetState = createFeatureSelector('openaire'); +import { + OpenaireSuggestionTarget +} from '../../core/openaire/reciter-suggestions/models/openaire-suggestion-target.model'; +import { SuggestionTargetEntry, SuggestionTargetState } from './suggestion-targets/suggestion-targets.reducer'; // Reciter Suggestion Targets // ---------------------------------------------------------------------------- @@ -25,12 +19,21 @@ export function reciterSuggestionTargetStateSelector(): MemoizedSelector { + return createSelector(reciterSuggestionTargetStateSelector(),(state: SuggestionTargetState) => state.sources[source]); +} + +/** + * Returns the Reciter Suggestion Targets list by source. * @function reciterSuggestionTargetObjectSelector - * @return {OpenaireReciterSuggestionTarget[]} + * @return {OpenaireSuggestionTarget[]} */ -export function reciterSuggestionTargetObjectSelector(): MemoizedSelector { - return subStateSelector(reciterSuggestionTargetStateSelector(), 'targets'); +export function reciterSuggestionTargetObjectSelector(source: string): MemoizedSelector { + return createSelector(reciterSuggestionSourceSelector(source), (state: SuggestionTargetEntry) => state.targets); } /** @@ -38,60 +41,60 @@ export function reciterSuggestionTargetObjectSelector(): MemoizedSelector state.suggestionTarget.loaded -); +export const isReciterSuggestionTargetLoadedSelector = (source: string) => { + return createSelector(reciterSuggestionSourceSelector(source), (state: SuggestionTargetEntry) => state?.loaded || false); +}; /** * Returns true if the deduplication sets are processing. * @function isDeduplicationSetsProcessingSelector * @return {boolean} */ -export const isreciterSuggestionTargetProcessingSelector = createSelector(_getReciterSuggestionTargetState, - (state: OpenaireState) => state.suggestionTarget.processing -); +export const isreciterSuggestionTargetProcessingSelector = (source: string) => { + return createSelector(reciterSuggestionSourceSelector(source), (state: SuggestionTargetEntry) => state?.processing || false); +}; /** * Returns the total available pages of Reciter Suggestion Targets. * @function getreciterSuggestionTargetTotalPagesSelector * @return {number} */ -export const getreciterSuggestionTargetTotalPagesSelector = createSelector(_getReciterSuggestionTargetState, - (state: OpenaireState) => state.suggestionTarget.totalPages -); +export const getReciterSuggestionTargetTotalPagesSelector = (source: string) => { + return createSelector(reciterSuggestionSourceSelector(source), (state: SuggestionTargetEntry) => state?.totalPages || 0); +}; /** * Returns the current page of Reciter Suggestion Targets. - * @function getreciterSuggestionTargetCurrentPageSelector + * @function getReciterSuggestionTargetCurrentPageSelector * @return {number} */ -export const getreciterSuggestionTargetCurrentPageSelector = createSelector(_getReciterSuggestionTargetState, - (state: OpenaireState) => state.suggestionTarget.currentPage -); +export const getReciterSuggestionTargetCurrentPageSelector = (source: string) => { + return createSelector(reciterSuggestionSourceSelector(source), (state: SuggestionTargetEntry) => state?.currentPage || 0); +}; /** * Returns the total number of Reciter Suggestion Targets. - * @function getreciterSuggestionTargetTotalsSelector + * @function getReciterSuggestionTargetTotalsSelector * @return {number} */ -export const getreciterSuggestionTargetTotalsSelector = createSelector(_getReciterSuggestionTargetState, - (state: OpenaireState) => state.suggestionTarget.totalElements -); +export const getReciterSuggestionTargetTotalsSelector = (source: string) => { + return createSelector(reciterSuggestionSourceSelector(source), (state: SuggestionTargetEntry) => state?.totalElements || 0); +}; /** * Returns Suggestion Targets for the current user. * @function getCurrentUserReciterSuggestionTargetSelector * @return {OpenaireSuggestionTarget[]} */ -export const getCurrentUserSuggestionTargetsSelector = createSelector(_getReciterSuggestionTargetState, - (state: OpenaireState) => state.suggestionTarget.currentUserTargets -); +export const getCurrentUserSuggestionTargetsSelector = () => { + return createSelector(reciterSuggestionTargetStateSelector(), (state: SuggestionTargetState) => state?.currentUserTargets || []); +}; /** - * Returns whether or not the user has consulted their suggestions + * Returns whether the user has consulted their suggestions * @function getCurrentUserReciterSuggestionTargetSelector * @return {boolean} */ -export const getCurrentUserSuggestionTargetsVisitedSelector = createSelector(_getReciterSuggestionTargetState, - (state: OpenaireState) => state.suggestionTarget.currentUserTargetsVisited -); +export const getCurrentUserSuggestionTargetsVisitedSelector = () => { + return createSelector(reciterSuggestionTargetStateSelector(), (state: SuggestionTargetState) => state?.currentUserTargetsVisited || false); +}; diff --git a/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.actions.ts b/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.actions.ts index 40bc204b7cc..c2d3e800dcb 100644 --- a/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.actions.ts +++ b/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.actions.ts @@ -2,10 +2,12 @@ import { Action } from '@ngrx/store'; import { type } from '../../../shared/ngrx/type'; -import { OpenaireSuggestionTarget } from '../../../core/openaire/reciter-suggestions/models/openaire-suggestion-target.model'; +import { + OpenaireSuggestionTarget +} from '../../../core/openaire/reciter-suggestions/models/openaire-suggestion-target.model'; /** - * For each action type in an action group, make a simple + * For each action type in A action group, make a simple * enum object for all of this group's action types. * * The 'type' utility function coerces strings into string @@ -23,7 +25,7 @@ export const SuggestionTargetActionTypes = { }; /** - * An ngrx action to retrieve all the Suggestion Targets. + * A ngrx action to retrieve all the Suggestion Targets. */ export class RetrieveTargetsBySourceAction implements Action { type = SuggestionTargetActionTypes.RETRIEVE_TARGETS_BY_SOURCE; @@ -53,18 +55,34 @@ export class RetrieveTargetsBySourceAction implements Action { } /** - * An ngrx action for retrieving 'all Suggestion Targets' error. + * A ngrx action for notifying error. */ -export class RetrieveAllTargetsErrorAction implements Action { +export class RetrieveTargetsBySourceErrorAction implements Action { type = SuggestionTargetActionTypes.RETRIEVE_TARGETS_BY_SOURCE_ERROR; + payload: { + source: string; + }; + + /** + * Create a new RetrieveTargetsBySourceAction. + * + * @param source + * the source for which to retrieve suggestion targets + */ + constructor(source: string) { + this.payload = { + source + }; + } } /** - * An ngrx action to load the Suggestion Target objects. + * A ngrx action to load the Suggestion Target objects. */ export class AddTargetAction implements Action { type = SuggestionTargetActionTypes.ADD_TARGETS; payload: { + source: string; targets: OpenaireSuggestionTarget[]; totalPages: number; currentPage: number; @@ -74,6 +92,8 @@ export class AddTargetAction implements Action { /** * Create a new AddTargetAction. * + * @param source + * the source of suggestion targets * @param targets * the list of targets * @param totalPages @@ -83,8 +103,9 @@ export class AddTargetAction implements Action { * @param totalElements * the total available Suggestion Targets */ - constructor(targets: OpenaireSuggestionTarget[], totalPages: number, currentPage: number, totalElements: number) { + constructor(source: string, targets: OpenaireSuggestionTarget[], totalPages: number, currentPage: number, totalElements: number) { this.payload = { + source, targets, totalPages, currentPage, @@ -95,7 +116,7 @@ export class AddTargetAction implements Action { } /** - * An ngrx action to load the user Suggestion Target object. + * A ngrx action to load the user Suggestion Target object. * Called by the ??? effect. */ export class AddUserSuggestionsAction implements Action { @@ -117,7 +138,7 @@ export class AddUserSuggestionsAction implements Action { } /** - * An ngrx action to reload the user Suggestion Target object. + * A ngrx action to reload the user Suggestion Target object. * Called by the ??? effect. */ export class RefreshUserSuggestionsAction implements Action { @@ -125,7 +146,7 @@ export class RefreshUserSuggestionsAction implements Action { } /** - * An ngrx action to Mark User Suggestions As Visited. + * A ngrx action to Mark User Suggestions As Visited. * Called by the ??? effect. */ export class MarkUserSuggestionsAsVisitedAction implements Action { @@ -133,10 +154,25 @@ export class MarkUserSuggestionsAsVisitedAction implements Action { } /** - * An ngrx action to clear targets state. + * A ngrx action to clear targets state. */ export class ClearSuggestionTargetsAction implements Action { type = SuggestionTargetActionTypes.CLEAR_TARGETS; + payload: { + source: string; + }; + + /** + * Create a new ClearSuggestionTargetsAction. + * + * @param source + * the source of suggestion targets + */ + constructor(source: string) { + this.payload = { + source + }; + } } /** @@ -149,4 +185,4 @@ export type SuggestionTargetsActions | ClearSuggestionTargetsAction | MarkUserSuggestionsAsVisitedAction | RetrieveTargetsBySourceAction - | RetrieveAllTargetsErrorAction; + | RetrieveTargetsBySourceErrorAction; diff --git a/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.component.html b/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.component.html index 791e694ba90..d7e669051b0 100644 --- a/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.component.html +++ b/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.component.html @@ -3,8 +3,8 @@
    - - + { this.getSuggestionTargets(); @@ -95,8 +97,8 @@ export class SuggestionTargetsComponent implements OnInit { * @return Observable * 'true' if the targets are loading, 'false' otherwise. */ - public isTargetsLoading(): Observable { - return this.suggestionTargetsStateService.isReciterSuggestionTargetsLoading(); + public isTargetsLoading(source: string): Observable { + return this.suggestionTargetsStateService.isReciterSuggestionTargetsLoading(source); } /** @@ -106,7 +108,7 @@ export class SuggestionTargetsComponent implements OnInit { * 'true' if there are operations running on the targets (ex.: a REST call), 'false' otherwise. */ public isTargetsProcessing(): Observable { - return this.suggestionTargetsStateService.isReciterSuggestionTargetsProcessing(); + return this.suggestionTargetsStateService.isReciterSuggestionTargetsProcessing(this.source); } /** @@ -125,7 +127,7 @@ export class SuggestionTargetsComponent implements OnInit { * Unsubscribe from all subscriptions. */ ngOnDestroy(): void { - this.suggestionTargetsStateService.dispatchClearSuggestionTargetsAction(); + this.suggestionTargetsStateService.dispatchClearSuggestionTargetsAction(this.source); this.subs .filter((sub) => hasValue(sub)) .forEach((sub) => sub.unsubscribe()); diff --git a/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.effects.ts b/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.effects.ts index fe9311b5412..3f3db5373e2 100644 --- a/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.effects.ts +++ b/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.effects.ts @@ -10,8 +10,8 @@ import { AddTargetAction, AddUserSuggestionsAction, RefreshUserSuggestionsAction, - RetrieveAllTargetsErrorAction, RetrieveTargetsBySourceAction, + RetrieveTargetsBySourceErrorAction, SuggestionTargetActionTypes, } from './suggestion-targets.actions'; import { PaginatedList } from '../../../core/data/paginated-list.model'; @@ -41,13 +41,13 @@ export class SuggestionTargetsEffects { action.payload.currentPage ).pipe( map((targets: PaginatedList) => - new AddTargetAction(targets.page, targets.totalPages, targets.currentPage, targets.totalElements) + new AddTargetAction(action.payload.source, targets.page, targets.totalPages, targets.currentPage, targets.totalElements) ), catchError((error: Error) => { if (error) { console.error(error.message); } - return of(new RetrieveAllTargetsErrorAction()); + return of(new RetrieveTargetsBySourceErrorAction(action.payload.source)); }) ); }) diff --git a/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.reducer.ts b/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.reducer.ts index f8bd53ec050..0df541974d4 100644 --- a/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.reducer.ts +++ b/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.reducer.ts @@ -1,16 +1,27 @@ import { SuggestionTargetActionTypes, SuggestionTargetsActions } from './suggestion-targets.actions'; -import { OpenaireSuggestionTarget } from '../../../core/openaire/reciter-suggestions/models/openaire-suggestion-target.model'; +import { + OpenaireSuggestionTarget +} from '../../../core/openaire/reciter-suggestions/models/openaire-suggestion-target.model'; /** * The interface representing the OpenAIRE suggestion targets state. */ -export interface SuggestionTargetState { +export interface SuggestionTargetEntry { targets: OpenaireSuggestionTarget[]; processing: boolean; loaded: boolean; totalPages: number; currentPage: number; totalElements: number; + +} + +export interface SuggestionSourcesState { + [source: string]: SuggestionTargetEntry; +} + +export interface SuggestionTargetState { + sources: SuggestionSourcesState; currentUserTargets: OpenaireSuggestionTarget[]; currentUserTargetsVisited: boolean; } @@ -18,13 +29,17 @@ export interface SuggestionTargetState { /** * Used for the OpenAIRE Suggestion Target state initialization. */ -const SuggestionTargetInitialState: SuggestionTargetState = { +const suggestionSourceTargetsInitialState: SuggestionTargetEntry = { targets: [], processing: false, loaded: false, totalPages: 0, currentPage: 0, totalElements: 0, +}; + +const SuggestionTargetInitialState: SuggestionTargetState = { + sources: {}, currentUserTargets: null, currentUserTargetsVisited: false }; @@ -42,25 +57,42 @@ const SuggestionTargetInitialState: SuggestionTargetState = { export function SuggestionTargetsReducer(state = SuggestionTargetInitialState, action: SuggestionTargetsActions): SuggestionTargetState { switch (action.type) { case SuggestionTargetActionTypes.RETRIEVE_TARGETS_BY_SOURCE: { - return Object.assign({}, state, { + const sourceState = state.sources[action.payload.source] || Object.assign({}, suggestionSourceTargetsInitialState); + const newSourceState = Object.assign({}, sourceState, { targets: [], - processing: true + processing: true, + }); + + return Object.assign({}, state, { + sources: + Object.assign({}, state.sources, { + [action.payload.source]: newSourceState + }) }); } case SuggestionTargetActionTypes.ADD_TARGETS: { - return Object.assign({}, state, { - targets: state.targets.concat(action.payload.targets), + const sourceState = state.sources[action.payload.source] || Object.assign({}, suggestionSourceTargetsInitialState); + const newSourceState = Object.assign({}, sourceState, { + targets: sourceState.targets.concat(action.payload.targets), processing: false, loaded: true, totalPages: action.payload.totalPages, - currentPage: state.currentPage, + currentPage: action.payload.currentPage, totalElements: action.payload.totalElements }); + + return Object.assign({}, state, { + sources: + Object.assign({}, state.sources, { + [action.payload.source]: newSourceState + }) + }); } case SuggestionTargetActionTypes.RETRIEVE_TARGETS_BY_SOURCE_ERROR: { - return Object.assign({}, state, { + const sourceState = state.sources[action.payload.source] || Object.assign({}, suggestionSourceTargetsInitialState); + const newSourceState = Object.assign({}, sourceState, { targets: [], processing: false, loaded: true, @@ -68,6 +100,13 @@ export function SuggestionTargetsReducer(state = SuggestionTargetInitialState, a currentPage: 0, totalElements: 0, }); + + return Object.assign({}, state, { + sources: + Object.assign({}, state.sources, { + [action.payload.source]: newSourceState + }) + }); } case SuggestionTargetActionTypes.ADD_USER_SUGGESTIONS: { @@ -83,14 +122,22 @@ export function SuggestionTargetsReducer(state = SuggestionTargetInitialState, a } case SuggestionTargetActionTypes.CLEAR_TARGETS: { - return Object.assign({}, state, { + const sourceState = state.sources[action.payload.source] || Object.assign({}, suggestionSourceTargetsInitialState); + const newSourceState = Object.assign({}, sourceState, { targets: [], processing: false, - loaded: false, + loaded: true, totalPages: 0, currentPage: 0, totalElements: 0, }); + + return Object.assign({}, state, { + sources: + Object.assign({}, state.sources, { + [action.payload.source]: newSourceState + }) + }); } default: { diff --git a/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.state.service.ts b/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.state.service.ts index 2e05bce0a9b..92cf1dba3b0 100644 --- a/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.state.service.ts +++ b/src/app/openaire/reciter-suggestions/suggestion-targets/suggestion-targets.state.service.ts @@ -7,13 +7,15 @@ import { map } from 'rxjs/operators'; import { getCurrentUserSuggestionTargetsSelector, getCurrentUserSuggestionTargetsVisitedSelector, - getreciterSuggestionTargetCurrentPageSelector, - getreciterSuggestionTargetTotalsSelector, + getReciterSuggestionTargetCurrentPageSelector, + getReciterSuggestionTargetTotalsSelector, isReciterSuggestionTargetLoadedSelector, isreciterSuggestionTargetProcessingSelector, reciterSuggestionTargetObjectSelector } from '../selectors'; -import { OpenaireSuggestionTarget } from '../../../core/openaire/reciter-suggestions/models/openaire-suggestion-target.model'; +import { + OpenaireSuggestionTarget +} from '../../../core/openaire/reciter-suggestions/models/openaire-suggestion-target.model'; import { ClearSuggestionTargetsAction, MarkUserSuggestionsAsVisitedAction, @@ -40,8 +42,8 @@ export class SuggestionTargetsStateService { * @return Observable * The list of Reciter Suggestion Targets. */ - public getReciterSuggestionTargets(): Observable { - return this.store.pipe(select(reciterSuggestionTargetObjectSelector())); + public getReciterSuggestionTargets(source: string): Observable { + return this.store.pipe(select(reciterSuggestionTargetObjectSelector(source))); } /** @@ -50,9 +52,9 @@ export class SuggestionTargetsStateService { * @return Observable * 'true' if the targets are loading, 'false' otherwise. */ - public isReciterSuggestionTargetsLoading(): Observable { + public isReciterSuggestionTargetsLoading(source: string): Observable { return this.store.pipe( - select(isReciterSuggestionTargetLoadedSelector), + select(isReciterSuggestionTargetLoadedSelector(source)), map((loaded: boolean) => !loaded) ); } @@ -63,8 +65,8 @@ export class SuggestionTargetsStateService { * @return Observable * 'true' if the targets are loaded, 'false' otherwise. */ - public isReciterSuggestionTargetsLoaded(): Observable { - return this.store.pipe(select(isReciterSuggestionTargetLoadedSelector)); + public isReciterSuggestionTargetsLoaded(source: string): Observable { + return this.store.pipe(select(isReciterSuggestionTargetLoadedSelector(source))); } /** @@ -73,8 +75,8 @@ export class SuggestionTargetsStateService { * @return Observable * 'true' if there are operations running on the targets (ex.: a REST call), 'false' otherwise. */ - public isReciterSuggestionTargetsProcessing(): Observable { - return this.store.pipe(select(isreciterSuggestionTargetProcessingSelector)); + public isReciterSuggestionTargetsProcessing(source: string): Observable { + return this.store.pipe(select(isreciterSuggestionTargetProcessingSelector(source))); } /** @@ -83,8 +85,8 @@ export class SuggestionTargetsStateService { * @return Observable * The number of the Reciter Suggestion Targets pages. */ - public getReciterSuggestionTargetsTotalPages(): Observable { - return this.store.pipe(select(getreciterSuggestionTargetTotalsSelector)); + public getReciterSuggestionTargetsTotalPages(source: string): Observable { + return this.store.pipe(select(getReciterSuggestionTargetTotalsSelector(source))); } /** @@ -93,8 +95,8 @@ export class SuggestionTargetsStateService { * @return Observable * The number of the current Reciter Suggestion Targets page. */ - public getReciterSuggestionTargetsCurrentPage(): Observable { - return this.store.pipe(select(getreciterSuggestionTargetCurrentPageSelector)); + public getReciterSuggestionTargetsCurrentPage(source: string): Observable { + return this.store.pipe(select(getReciterSuggestionTargetCurrentPageSelector(source))); } /** @@ -103,8 +105,8 @@ export class SuggestionTargetsStateService { * @return Observable * The number of the Reciter Suggestion Targets. */ - public getReciterSuggestionTargetsTotals(): Observable { - return this.store.pipe(select(getreciterSuggestionTargetTotalsSelector)); + public getReciterSuggestionTargetsTotals(source: string): Observable { + return this.store.pipe(select(getReciterSuggestionTargetTotalsSelector(source))); } /** @@ -128,7 +130,7 @@ export class SuggestionTargetsStateService { * The Reciter Suggestion Targets object. */ public getCurrentUserSuggestionTargets(): Observable { - return this.store.pipe(select(getCurrentUserSuggestionTargetsSelector)); + return this.store.pipe(select(getCurrentUserSuggestionTargetsSelector())); } /** @@ -138,7 +140,7 @@ export class SuggestionTargetsStateService { * True if user already visited, false otherwise. */ public hasUserVisitedSuggestions(): Observable { - return this.store.pipe(select(getCurrentUserSuggestionTargetsVisitedSelector)); + return this.store.pipe(select(getCurrentUserSuggestionTargetsVisitedSelector())); } /** @@ -150,9 +152,12 @@ export class SuggestionTargetsStateService { /** * Dispatch an action to clear the Reciter Suggestion Targets state. + * + * @param source + * the source of suggestion targets */ - public dispatchClearSuggestionTargetsAction(): void { - this.store.dispatch(new ClearSuggestionTargetsAction()); + public dispatchClearSuggestionTargetsAction(source: string): void { + this.store.dispatch(new ClearSuggestionTargetsAction(source)); } /** diff --git a/src/app/openaire/reciter-suggestions/suggestions-notification/suggestions-notification.component.ts b/src/app/openaire/reciter-suggestions/suggestions-notification/suggestions-notification.component.ts index 094dfab0174..923c25ecf41 100644 --- a/src/app/openaire/reciter-suggestions/suggestions-notification/suggestions-notification.component.ts +++ b/src/app/openaire/reciter-suggestions/suggestions-notification/suggestions-notification.component.ts @@ -29,6 +29,7 @@ export class SuggestionsNotificationComponent implements OnInit { ngOnInit() { this.suggestionsRD$ = this.reciterSuggestionStateService.getCurrentUserSuggestionTargets(); + this.reciterSuggestionStateService.dispatchMarkUserSuggestionsAsVisitedAction(); } /** diff --git a/src/app/process-page/detail/process-detail.component.html b/src/app/process-page/detail/process-detail.component.html index 29cbfc113ff..5f905cbfff3 100644 --- a/src/app/process-page/detail/process-detail.component.html +++ b/src/app/process-page/detail/process-detail.component.html @@ -1,10 +1,15 @@ -
    -
    -

    {{'process.detail.title' | translate:{ - id: process?.processId, - name: process?.scriptName - } }}

    +
    +
    +
    +

    + {{ 'process.detail.title' | translate:{ id: process?.processId, name: process?.scriptName } }} +

    +
    +
    + Refreshing in {{ seconds }}s +
    +
    {{ process?.scriptName }}
    @@ -17,10 +22,12 @@

    {{'process.detail.title' | translate:{
    +
    - {{getFileName(file)}} - ({{(file?.sizeBytes) | dsFileSize }}) + {{getFileName(file)}} + ({{(file?.sizeBytes) | dsFileSize }}) +
    @@ -70,7 +77,7 @@

    {{'process.detail.title' | translate:{ -
    +
    - {{group.name}} + {{ dsoNameService.getName(group) }}
    diff --git a/src/app/shared/comcol/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.spec.ts b/src/app/shared/comcol/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.spec.ts index 59fc95b67f8..00efc3ccaad 100644 --- a/src/app/shared/comcol/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.spec.ts +++ b/src/app/shared/comcol/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.spec.ts @@ -12,6 +12,8 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { ComcolModule } from '../../../comcol.module'; import { NotificationsService } from '../../../../notifications/notifications.service'; import { NotificationsServiceStub } from '../../../../testing/notifications-service.stub'; +import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service'; +import { DSONameServiceMock } from '../../../../mocks/dso-name.service.mock'; describe('ComcolRoleComponent', () => { @@ -41,6 +43,7 @@ describe('ComcolRoleComponent', () => { NoopAnimationsModule ], providers: [ + { provide: DSONameService, useValue: new DSONameServiceMock() }, { provide: GroupDataService, useValue: groupService }, { provide: RequestService, useValue: requestService }, { provide: NotificationsService, useClass: NotificationsServiceStub } diff --git a/src/app/shared/comcol/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.ts b/src/app/shared/comcol/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.ts index 3091dd0cf01..5ae22d754ee 100644 --- a/src/app/shared/comcol/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.ts +++ b/src/app/shared/comcol/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.ts @@ -14,6 +14,7 @@ import { hasNoValue, hasValue } from '../../../../empty.util'; import { NoContent } from '../../../../../core/shared/NoContent.model'; import { NotificationsService } from '../../../../notifications/notifications.service'; import { TranslateService } from '@ngx-translate/core'; +import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service'; /** * Component for managing a community or collection role. @@ -76,6 +77,7 @@ export class ComcolRoleComponent implements OnInit { protected groupService: GroupDataService, protected notificationsService: NotificationsService, protected translateService: TranslateService, + public dsoNameService: DSONameService, ) { } diff --git a/src/app/shared/comcol/comcol-forms/edit-comcol-page/edit-comcol-page.component.ts b/src/app/shared/comcol/comcol-forms/edit-comcol-page/edit-comcol-page.component.ts index 48eb9aec968..e4d6c9c8a74 100644 --- a/src/app/shared/comcol/comcol-forms/edit-comcol-page/edit-comcol-page.component.ts +++ b/src/app/shared/comcol/comcol-forms/edit-comcol-page/edit-comcol-page.component.ts @@ -1,7 +1,7 @@ import { Component, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; -import { first, map } from 'rxjs/operators'; +import { map } from 'rxjs/operators'; import { ActivatedRoute, Router } from '@angular/router'; import { RemoteData } from '../../../../core/data/remote-data'; @@ -53,7 +53,7 @@ export class EditComColPageComponent implements On this.pages = this.route.routeConfig.children .map((child: any) => child.path) .filter((path: string) => isNotEmpty(path)); // ignore reroutes - this.dsoRD$ = this.route.data.pipe(first(), map((data) => data.dso)); + this.dsoRD$ = this.route.data.pipe(map((data) => data.dso)); } /** diff --git a/src/app/shared/comcol/comcol-page-logo/comcol-page-logo.component.html b/src/app/shared/comcol/comcol-page-logo/comcol-page-logo.component.html index 057c3582239..cb3feecbe59 100644 --- a/src/app/shared/comcol/comcol-page-logo/comcol-page-logo.component.html +++ b/src/app/shared/comcol/comcol-page-logo/comcol-page-logo.component.html @@ -1,3 +1,3 @@ diff --git a/src/app/shared/comcol/comcol-page-logo/comcol-page-logo.component.scss b/src/app/shared/comcol/comcol-page-logo/comcol-page-logo.component.scss index e69de29bb2d..db6e7daecfd 100644 --- a/src/app/shared/comcol/comcol-page-logo/comcol-page-logo.component.scss +++ b/src/app/shared/comcol/comcol-page-logo/comcol-page-logo.component.scss @@ -0,0 +1,4 @@ +img { + max-width: var(--ds-comcol-logo-max-width); + max-height: var(--ds-comcol-logo-max-height); +} diff --git a/src/app/shared/confirmation-modal/confirmation-modal.component.html b/src/app/shared/confirmation-modal/confirmation-modal.component.html index 82c70b662bf..02434b1fa1e 100644 --- a/src/app/shared/confirmation-modal/confirmation-modal.component.html +++ b/src/app/shared/confirmation-modal/confirmation-modal.component.html @@ -1,18 +1,18 @@
    - diff --git a/src/app/shared/confirmation-modal/confirmation-modal.component.ts b/src/app/shared/confirmation-modal/confirmation-modal.component.ts index 4fa48586007..46eb4cedc5a 100644 --- a/src/app/shared/confirmation-modal/confirmation-modal.component.ts +++ b/src/app/shared/confirmation-modal/confirmation-modal.component.ts @@ -1,6 +1,7 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; +import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; @Component({ selector: 'ds-confirmation-modal', @@ -25,7 +26,10 @@ export class ConfirmationModalComponent { @Output() response = new EventEmitter(); - constructor(protected activeModal: NgbActiveModal) { + constructor( + protected activeModal: NgbActiveModal, + public dsoNameService: DSONameService, + ) { } /** diff --git a/src/app/shared/context-help-wrapper/context-help-wrapper.component.html b/src/app/shared/context-help-wrapper/context-help-wrapper.component.html index b031d0f42d7..083b8163ff5 100644 --- a/src/app/shared/context-help-wrapper/context-help-wrapper.component.html +++ b/src/app/shared/context-help-wrapper/context-help-wrapper.component.html @@ -2,7 +2,7 @@
    - {{elem.text}} + {{elem.text}} {{ elem }} diff --git a/src/app/shared/context-menu/context-menu.component.html b/src/app/shared/context-menu/context-menu.component.html index b4c140c6dba..f68bbc697ee 100644 --- a/src/app/shared/context-menu/context-menu.component.html +++ b/src/app/shared/context-menu/context-menu.component.html @@ -1,4 +1,4 @@ -
    +
    diff --git a/src/app/shared/context-menu/context-menu.component.spec.ts b/src/app/shared/context-menu/context-menu.component.spec.ts index df63e67392c..6f1669d403a 100644 --- a/src/app/shared/context-menu/context-menu.component.spec.ts +++ b/src/app/shared/context-menu/context-menu.component.spec.ts @@ -4,7 +4,6 @@ import { BrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/t import { By } from '@angular/platform-browser'; import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; -import { cold } from 'jasmine-marbles'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { Store } from '@ngrx/store'; import { of } from 'rxjs'; @@ -155,13 +154,13 @@ describe('ContextMenuComponent', () => { done(); }); - it('should display d-none', (done) => { + it('should use d-none', (done) => { const menu = fixture.debugElement.query(By.css('div.d-none')); expect(menu).not.toBeNull(); done(); }); - it('should not display d-inline-block', (done) => { + it('should not use d-inline-block', (done) => { const menu = fixture.debugElement.query(By.css('div.d-inline-block')); expect(menu).toBeNull(); done(); @@ -173,11 +172,6 @@ describe('ContextMenuComponent', () => { done(); }); - it('should check the authorization of the current user', (done) => { - expect(component.isAuthenticated).toBeObservable(cold('a', { a: true })); - done(); - }); - it('should not have menu entries when are disabled on rest side', (done) => { component.contextMenuObjectType = DSpaceObjectType.COMMUNITY; configurationDataService.findByPropertyName.and.returnValues( @@ -218,34 +212,29 @@ describe('ContextMenuComponent', () => { done(); }); - it('should not display context menu', (done) => { + it('should display context menu', (done) => { const menu = fixture.debugElement.query(By.css('button#context-menu')); expect(menu).not.toBeNull(); done(); }); - it('should display d-none', (done) => { + it('should use d-none', (done) => { const menu = fixture.debugElement.query(By.css('div.d-none')); expect(menu).not.toBeNull(); done(); }); - it('should not display d-inline-block', (done) => { + it('should not use d-inline-block', (done) => { const menu = fixture.debugElement.query(By.css('div.d-inline-block')); expect(menu).toBeNull(); done(); }); - it('should not display stand alone buttons', (done) => { + it('should display stand alone buttons', (done) => { const menu = fixture.debugElement.query(By.css('button.btn-primary')); expect(menu).not.toBeNull(); done(); }); - - it('should check the authorization of the current user', (done) => { - expect(component.isAuthenticated).toBeObservable(cold('a', { a: false })); - done(); - }); }); describe('and the object type is not ITEM', () => { @@ -259,9 +248,9 @@ describe('ContextMenuComponent', () => { done(); }); - it('should not display context menu', (done) => { + it('should display context menu', (done) => { const menu = fixture.debugElement.query(By.css('button#context-menu')); - expect(menu).toBeNull(); + expect(menu).not.toBeNull(); done(); }); @@ -271,14 +260,9 @@ describe('ContextMenuComponent', () => { done(); }); - it('should not display d-none', (done) => { + it('should display d-none', (done) => { const menu = fixture.debugElement.query(By.css('div.d-none')); - expect(menu).toBeNull(); - done(); - }); - - it('should check the authorization of the current user', (done) => { - expect(component.isAuthenticated).toBeObservable(cold('a', { a: false })); + expect(menu).not.toBeNull(); done(); }); }); diff --git a/src/app/shared/context-menu/context-menu.component.ts b/src/app/shared/context-menu/context-menu.component.ts index 9c047f23b32..25f19813093 100644 --- a/src/app/shared/context-menu/context-menu.component.ts +++ b/src/app/shared/context-menu/context-menu.component.ts @@ -1,12 +1,9 @@ import { ChangeDetectorRef, Component, Inject, Injector, Input, OnInit } from '@angular/core'; import { DOCUMENT } from '@angular/common'; -import { select, Store } from '@ngrx/store'; import { from, Observable } from 'rxjs'; import { concatMap, filter, map, reduce, take } from 'rxjs/operators'; -import { CoreState } from '../../core/core-state.model'; -import { isAuthenticated } from '../../core/auth/selectors'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { DSpaceObjectType } from '../../core/shared/dspace-object-type.model'; import { ContextMenuEntryRenderOptions, getContextMenuEntriesForDSOType } from './context-menu.decorator'; @@ -39,12 +36,6 @@ export class ContextMenuComponent implements OnInit { */ @Input() contextMenuObjectType: DSpaceObjectType; - /** - * Whether user is authenticated. - * @type {Observable} - */ - public isAuthenticated: Observable; - /** * Injector to inject a menu entry component with the @Input parameters * @type {Injector} @@ -61,16 +52,15 @@ export class ContextMenuComponent implements OnInit { * Initialize instance variables * * @param {Document} _document + * @param {ChangeDetectorRef} cdr * @param {ConfigurationDataService} configurationService * @param {Injector} injector - * @param {Store} store */ constructor( @Inject(DOCUMENT) private _document: Document, private cdr: ChangeDetectorRef, private configurationService: ConfigurationDataService, - private injector: Injector, - private store: Store + private injector: Injector ) { } @@ -82,8 +72,6 @@ export class ContextMenuComponent implements OnInit { ], parent: this.injector }); - // set isAuthenticated - this.isAuthenticated = this.store.pipe(select(isAuthenticated)); } /** diff --git a/src/app/shared/context-menu/item-version/item-version-menu.component.ts b/src/app/shared/context-menu/item-version/item-version-menu.component.ts index 22e7aa42ddc..d5b91fac1bc 100644 --- a/src/app/shared/context-menu/item-version/item-version-menu.component.ts +++ b/src/app/shared/context-menu/item-version/item-version-menu.component.ts @@ -1,6 +1,6 @@ import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; -import { BehaviorSubject, combineLatest, distinctUntilChanged, map, shareReplay, Subscription } from 'rxjs'; +import { BehaviorSubject, combineLatest, distinctUntilChanged, map, Subscription } from 'rxjs'; import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; @@ -61,8 +61,7 @@ export class ItemVersionMenuComponent extends ContextMenuEntryComponent implemen this.sub = combineLatest([isAuthorized$, isDisabled$]).pipe( map(([isAuthorized, isDisabled]) => isAuthorized && !isDisabled), - distinctUntilChanged(), - shareReplay(1) + distinctUntilChanged() ).subscribe((canShow) => { this.canShow$.next(canShow); }); diff --git a/src/app/shared/cookies/browser-klaro.service.ts b/src/app/shared/cookies/browser-klaro.service.ts index 2b09c0bf155..0db3d5464ef 100644 --- a/src/app/shared/cookies/browser-klaro.service.ts +++ b/src/app/shared/cookies/browser-klaro.service.ts @@ -1,21 +1,22 @@ import { Inject, Injectable, InjectionToken } from '@angular/core'; -import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs'; +import { BehaviorSubject, combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs'; import { AuthService } from '../../core/auth/auth.service'; import { TranslateService } from '@ngx-translate/core'; import { environment } from '../../../environments/environment'; import { map, switchMap, take } from 'rxjs/operators'; import { EPerson } from '../../core/eperson/models/eperson.model'; -import { KlaroService } from './klaro.service'; +import { CookieConsents, KlaroService } from './klaro.service'; import { hasValue, isEmpty, isNotEmpty } from '../empty.util'; import { CookieService } from '../../core/services/cookie.service'; import { EPersonDataService } from '../../core/eperson/eperson-data.service'; import cloneDeep from 'lodash/cloneDeep'; import debounce from 'lodash/debounce'; import { ANONYMOUS_STORAGE_NAME_KLARO, klaroConfiguration } from './klaro-configuration'; -import { Operation } from 'fast-json-patch'; +import { deepClone, Operation } from 'fast-json-patch'; import { getFirstCompletedRemoteData } from '../../core/shared/operators'; import { ConfigurationDataService } from '../../core/data/configuration-data.service'; import { CAPTCHA_NAME } from '../../core/google-recaptcha/google-recaptcha.service'; +import isEqual from 'lodash/isEqual'; /** * Metadata field to store a user's cookie consent preferences in @@ -65,11 +66,19 @@ export class BrowserKlaroService extends KlaroService { private readonly GOOGLE_ANALYTICS_SERVICE_NAME = 'google-analytics'; + private lastCookiesConsents: CookieConsents; + /** * Initial Klaro configuration */ klaroConfig = cloneDeep(klaroConfiguration); + /** + * Subject to emit updates in the consents + */ + consentsUpdates$: BehaviorSubject = new BehaviorSubject(null); + + constructor( private translateService: TranslateService, private authService: AuthService, @@ -94,6 +103,20 @@ export class BrowserKlaroService extends KlaroService { this.klaroConfig.translations.zz.consentNotice.description = 'cookies.consent.content-notice.description.no-privacy'; } + if (hasValue(environment.info.metricsConsents)) { + environment.info.metricsConsents.forEach((metric) => { + if (metric.enabled) { + this.klaroConfig.services.push( + { + name: metric.key, + purposes: ['thirdPartyJs'], + required: false, + } + ); + } + }); + } + const hideGoogleAnalytics$ = this.configService.findByPropertyName(this.GOOGLE_ANALYTICS_KEY).pipe( getFirstCompletedRemoteData(), map(remoteData => !remoteData.hasSucceeded || !remoteData.payload || isEmpty(remoteData.payload.values)), @@ -331,6 +354,25 @@ export class BrowserKlaroService extends KlaroService { return 'klaro-' + identifier; } + watchConsentUpdates(): void { + this.lazyKlaro.then(({getManager}) => { + const manager = getManager(this.klaroConfig); + const consentsSubject$ = this.consentsUpdates$; + let lastCookiesConsents = this.lastCookiesConsents; + + consentsSubject$.next(manager.consents); + manager.watch({ + update(_, eventName, consents) { + + if (eventName === 'consents' && !isEqual(consents, lastCookiesConsents)) { + lastCookiesConsents = deepClone(consents); + consentsSubject$.next(consents); + } + } + }); + }); + } + /** * remove the google analytics from the services */ diff --git a/src/app/shared/cookies/klaro-configuration.ts b/src/app/shared/cookies/klaro-configuration.ts index a41b641dec1..c818ddc19cb 100644 --- a/src/app/shared/cookies/klaro-configuration.ts +++ b/src/app/shared/cookies/klaro-configuration.ts @@ -22,7 +22,7 @@ export const GOOGLE_ANALYTICS_KLARO_KEY = 'google-analytics'; export const klaroConfiguration: any = { storageName: ANONYMOUS_STORAGE_NAME_KLARO, - privacyPolicy: '/info/privacy', + privacyPolicy: './info/privacy', /* Setting 'hideLearnMore' to 'true' will hide the "learn more / customize" link in diff --git a/src/app/shared/cookies/klaro.service.ts b/src/app/shared/cookies/klaro.service.ts index d54fed8b30a..fb34b773755 100644 --- a/src/app/shared/cookies/klaro.service.ts +++ b/src/app/shared/cookies/klaro.service.ts @@ -1,7 +1,9 @@ import { Injectable } from '@angular/core'; -import { Observable } from 'rxjs'; - +import { BehaviorSubject, Observable } from 'rxjs'; +export interface CookieConsents { + [key: string]: boolean; +} /** * Abstract class representing a service for handling Klaro consent preferences and UI */ @@ -10,15 +12,25 @@ export abstract class KlaroService { /** * Initializes the service */ - abstract initialize(); + abstract initialize(): void; /** * Shows a dialog with the current consent preferences */ - abstract showSettings(); + abstract showSettings(): void; /** * Return saved preferences stored in the klaro cookie */ abstract getSavedPreferences(): Observable; + + /** + * Watch for changes in consents + */ + abstract watchConsentUpdates(): void; + + /** + * Subject to emit updates in the consents + */ + abstract consentsUpdates$: BehaviorSubject; } diff --git a/src/app/shared/dso-page/dso-edit-menu.resolver.spec.ts b/src/app/shared/dso-page/dso-edit-menu.resolver.spec.ts index abfe618174b..e28a416f230 100644 --- a/src/app/shared/dso-page/dso-edit-menu.resolver.spec.ts +++ b/src/app/shared/dso-page/dso-edit-menu.resolver.spec.ts @@ -1,6 +1,6 @@ import { TestBed, waitForAsync } from '@angular/core/testing'; import { MenuServiceStub } from '../testing/menu-service.stub'; -import { of as observableOf } from 'rxjs'; +import { combineLatest, map, of as observableOf } from 'rxjs'; import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { RouterTestingModule } from '@angular/router/testing'; @@ -16,10 +16,13 @@ import { Item } from '../../core/shared/item.model'; import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../remote-data.utils'; import { MenuID } from '../menu/menu-id.model'; import { MenuItemType } from '../menu/menu-item-type.model'; -import { TextMenuItemModel } from '../menu/menu-item/models/text.model'; import { LinkMenuItemModel } from '../menu/menu-item/models/link.model'; import { ResearcherProfileDataService } from '../../core/profile/researcher-profile-data.service'; import { NotificationsService } from '../notifications/notifications.service'; +import { DSpaceObject } from '../../core/shared/dspace-object.model'; +import { Community } from '../../core/shared/community.model'; +import { Collection } from '../../core/shared/collection.model'; +import flatten from 'lodash/flatten'; describe('DSOEditMenuResolver', () => { @@ -37,25 +40,44 @@ describe('DSOEditMenuResolver', () => { let notificationsService; let translate; - const route = { - data: { - menu: { - 'statistics': [{ - id: 'statistics-dummy-1', - active: false, - visible: true, - model: null - }] - } - }, - params: {id: 'test-uuid'}, + const dsoRoute = (dso: DSpaceObject) => { + return { + data: { + menu: { + 'statistics': [{ + id: 'statistics-dummy-1', + active: false, + visible: true, + model: null + }] + } + }, + params: {id: dso.uuid}, + }; }; const state = { url: 'test-url' }; - const testObject = Object.assign(new Item(), {uuid: 'test-uuid', type: 'item', _links: {self: {href: 'self-link'}}}); + const testCommunity: Community = Object.assign(new Community(), { + uuid: 'test-community-uuid', + type: 'community', + _links: {self: {href: 'self-link'}}, + }); + const testCollection: Collection = Object.assign(new Collection(), { + uuid: 'test-collection-uuid', + type: 'collection', + _links: {self: {href: 'self-link'}}, + }); + const testItem: Item = Object.assign(new Item(), { + uuid: 'test-item-uuid', + type: 'item', + _links: {self: {href: 'self-link'}}, + }); + + let testObject: DSpaceObject; + let route; const dummySections1 = [{ id: 'dummy-1', @@ -90,6 +112,10 @@ describe('DSOEditMenuResolver', () => { }]; beforeEach(waitForAsync(() => { + // test with Items unless specified otherwise + testObject = testItem; + route = dsoRoute(testItem); + menuService = new MenuServiceStub(); spyOn(menuService, 'getMenu').and.returnValue(observableOf(MENU_STATE)); @@ -154,16 +180,17 @@ describe('DSOEditMenuResolver', () => { { ...route.data.menu, [MenuID.DSO_EDIT]: [ - ...dummySections1.map((menu) => Object.assign(menu, {id: menu.id + '-test-uuid'})), - ...dummySections2.map((menu) => Object.assign(menu, {id: menu.id + '-test-uuid'})) + ...dummySections1.map((menu) => Object.assign(menu, {id: menu.id + '-test-item-uuid'})), + ...dummySections2.map((menu) => Object.assign(menu, {id: menu.id + '-test-item-uuid'})) ] } ); - expect(dSpaceObjectDataService.findById).toHaveBeenCalledWith('test-uuid', true, false); + expect(dSpaceObjectDataService.findById).toHaveBeenCalledWith('test-item-uuid', true, false); expect(resolver.getDsoMenus).toHaveBeenCalled(); done(); }); }); + it('should create all menus when a dso is found based on the route scope query param when no id param is present', (done) => { spyOn(resolver, 'getDsoMenus').and.returnValue( [observableOf(dummySections1), observableOf(dummySections2)] @@ -198,6 +225,7 @@ describe('DSOEditMenuResolver', () => { done(); }); }); + it('should return the statistics menu when no dso is found', (done) => { (dSpaceObjectDataService.findById as jasmine.Spy).and.returnValue(createFailedRemoteDataObject$()); @@ -211,49 +239,165 @@ describe('DSOEditMenuResolver', () => { }); }); }); + describe('getDsoMenus', () => { - it('should return as first part the item version, orcid and claim list ', (done) => { - const result = resolver.getDsoMenus(testObject, route, state); - result[0].subscribe((menuList) => { - expect(menuList.length).toEqual(3); - expect(menuList[0].id).toEqual('orcid-dso'); - expect(menuList[0].active).toEqual(false); - // Visible should be false due to the item not being of type person - expect(menuList[0].visible).toEqual(false); - expect(menuList[0].model.type).toEqual(MenuItemType.LINK); - - expect(menuList[1].id).toEqual('version-dso'); - expect(menuList[1].active).toEqual(false); - expect(menuList[1].visible).toEqual(true); - expect(menuList[1].model.type).toEqual(MenuItemType.ONCLICK); - expect((menuList[1].model as TextMenuItemModel).text).toEqual('message'); - expect(menuList[1].model.disabled).toEqual(false); - expect(menuList[1].icon).toEqual('code-branch'); - - expect(menuList[2].id).toEqual('claim-dso'); - expect(menuList[2].active).toEqual(false); - // Visible should be false due to the item not being of type person - expect(menuList[2].visible).toEqual(false); - expect(menuList[2].model.type).toEqual(MenuItemType.ONCLICK); - expect((menuList[2].model as TextMenuItemModel).text).toEqual('item.page.claim.button'); - done(); + describe('for Communities', () => { + beforeEach(() => { + testObject = testCommunity; + dSpaceObjectDataService.findById.and.returnValue(createSuccessfulRemoteDataObject$(testCommunity)); + route = dsoRoute(testCommunity); + }); + + it('should not return Item-specific entries', (done) => { + const result = resolver.getDsoMenus(testObject, route, state); + combineLatest(result).pipe(map(flatten)).subscribe((menu) => { + const orcidEntry = menu.find(entry => entry.id === 'orcid-dso'); + expect(orcidEntry).toBeFalsy(); + + const versionEntry = menu.find(entry => entry.id === 'version-dso'); + expect(versionEntry).toBeFalsy(); + + const claimEntry = menu.find(entry => entry.id === 'claim-dso'); + expect(claimEntry).toBeFalsy(); + + done(); + }); + }); + + it('should return Community/Collection-specific entries', (done) => { + const result = resolver.getDsoMenus(testObject, route, state); + combineLatest(result).pipe(map(flatten)).subscribe((menu) => { + const subscribeEntry = menu.find(entry => entry.id === 'subscribe'); + expect(subscribeEntry).toBeTruthy(); + expect(subscribeEntry.active).toBeFalse(); + expect(subscribeEntry.visible).toBeTrue(); + expect(subscribeEntry.model.type).toEqual(MenuItemType.ONCLICK); + done(); + }); }); + it('should return as third part the common list ', (done) => { + const result = resolver.getDsoMenus(testObject, route, state); + combineLatest(result).pipe(map(flatten)).subscribe((menu) => { + const editEntry = menu.find(entry => entry.id === 'edit-dso'); + expect(editEntry).toBeTruthy(); + expect(editEntry.active).toBeFalse(); + expect(editEntry.visible).toBeTrue(); + expect(editEntry.model.type).toEqual(MenuItemType.LINK); + expect((editEntry.model as LinkMenuItemModel).link).toEqual( + '/communities/test-community-uuid/edit/metadata' + ); + done(); + }); + }); }); - it('should return as second part the common list ', (done) => { - const result = resolver.getDsoMenus(testObject, route, state); - result[1].subscribe((menuList) => { - expect(menuList.length).toEqual(1); - expect(menuList[0].id).toEqual('edit-dso'); - expect(menuList[0].active).toEqual(false); - expect(menuList[0].visible).toEqual(true); - expect(menuList[0].model.type).toEqual(MenuItemType.LINK); - expect((menuList[0].model as LinkMenuItemModel).text).toEqual('item.page.edit'); - expect((menuList[0].model as LinkMenuItemModel).link).toEqual('/items/test-uuid/edit/metadata'); - expect(menuList[0].icon).toEqual('pencil-alt'); - done(); + + describe('for Collections', () => { + beforeEach(() => { + testObject = testCollection; + dSpaceObjectDataService.findById.and.returnValue(createSuccessfulRemoteDataObject$(testCollection)); + route = dsoRoute(testCollection); + }); + + it('should not return Item-specific entries', (done) => { + const result = resolver.getDsoMenus(testObject, route, state); + combineLatest(result).pipe(map(flatten)).subscribe((menu) => { + const orcidEntry = menu.find(entry => entry.id === 'orcid-dso'); + expect(orcidEntry).toBeFalsy(); + + const versionEntry = menu.find(entry => entry.id === 'version-dso'); + expect(versionEntry).toBeFalsy(); + + const claimEntry = menu.find(entry => entry.id === 'claim-dso'); + expect(claimEntry).toBeFalsy(); + + done(); + }); + }); + + it('should return Community/Collection-specific entries', (done) => { + const result = resolver.getDsoMenus(testObject, route, state); + combineLatest(result).pipe(map(flatten)).subscribe((menu) => { + const subscribeEntry = menu.find(entry => entry.id === 'subscribe'); + expect(subscribeEntry).toBeTruthy(); + expect(subscribeEntry.active).toBeFalse(); + expect(subscribeEntry.visible).toBeTrue(); + expect(subscribeEntry.model.type).toEqual(MenuItemType.ONCLICK); + done(); + }); + }); + + it('should return as third part the common list ', (done) => { + const result = resolver.getDsoMenus(testObject, route, state); + combineLatest(result).pipe(map(flatten)).subscribe((menu) => { + const editEntry = menu.find(entry => entry.id === 'edit-dso'); + expect(editEntry).toBeTruthy(); + expect(editEntry.active).toBeFalse(); + expect(editEntry.visible).toBeTrue(); + expect(editEntry.model.type).toEqual(MenuItemType.LINK); + expect((editEntry.model as LinkMenuItemModel).link).toEqual( + '/collections/test-collection-uuid/edit/metadata' + ); + done(); + }); }); + }); + + describe('for Items', () => { + beforeEach(() => { + testObject = testItem; + dSpaceObjectDataService.findById.and.returnValue(createSuccessfulRemoteDataObject$(testItem)); + route = dsoRoute(testItem); + }); + + it('should return Item-specific entries', (done) => { + const result = resolver.getDsoMenus(testObject, route, state); + combineLatest(result).pipe(map(flatten)).subscribe((menu) => { + const orcidEntry = menu.find(entry => entry.id === 'orcid-dso'); + expect(orcidEntry).toBeTruthy(); + expect(orcidEntry.active).toBeFalse(); + expect(orcidEntry.visible).toBeFalse(); + expect(orcidEntry.model.type).toEqual(MenuItemType.LINK); + + const versionEntry = menu.find(entry => entry.id === 'version-dso'); + expect(versionEntry).toBeTruthy(); + expect(versionEntry.active).toBeFalse(); + expect(versionEntry.visible).toBeTrue(); + expect(versionEntry.model.type).toEqual(MenuItemType.ONCLICK); + expect(versionEntry.model.disabled).toBeFalse(); + const claimEntry = menu.find(entry => entry.id === 'claim-dso'); + expect(claimEntry).toBeTruthy(); + expect(claimEntry.active).toBeFalse(); + expect(claimEntry.visible).toBeFalse(); + expect(claimEntry.model.type).toEqual(MenuItemType.ONCLICK); + done(); + }); + }); + + it('should not return Community/Collection-specific entries', (done) => { + const result = resolver.getDsoMenus(testObject, route, state); + combineLatest(result).pipe(map(flatten)).subscribe((menu) => { + const subscribeEntry = menu.find(entry => entry.id === 'subscribe'); + expect(subscribeEntry).toBeFalsy(); + done(); + }); + }); + + it('should return as third part the common list ', (done) => { + const result = resolver.getDsoMenus(testObject, route, state); + combineLatest(result).pipe(map(flatten)).subscribe((menu) => { + const editEntry = menu.find(entry => entry.id === 'edit-dso'); + expect(editEntry).toBeTruthy(); + expect(editEntry.active).toBeFalse(); + expect(editEntry.visible).toBeTrue(); + expect(editEntry.model.type).toEqual(MenuItemType.LINK); + expect((editEntry.model as LinkMenuItemModel).link).toEqual( + '/items/test-item-uuid/edit/metadata' + ); + done(); + }); + }); }); }); }); diff --git a/src/app/shared/dso-page/dso-edit-menu.resolver.ts b/src/app/shared/dso-page/dso-edit-menu.resolver.ts index cd2aa27fca5..d30ee5e3e94 100644 --- a/src/app/shared/dso-page/dso-edit-menu.resolver.ts +++ b/src/app/shared/dso-page/dso-edit-menu.resolver.ts @@ -1,26 +1,29 @@ -import {ActivatedRouteSnapshot, Resolve, RouterStateSnapshot} from '@angular/router'; -import {combineLatest, Observable, of as observableOf} from 'rxjs'; -import {FeatureID} from '../../core/data/feature-authorization/feature-id'; -import {MenuService} from '../menu/menu.service'; -import {AuthorizationDataService} from '../../core/data/feature-authorization/authorization-data.service'; -import {Injectable} from '@angular/core'; -import {LinkMenuItemModel} from '../menu/menu-item/models/link.model'; -import {Item} from '../../core/shared/item.model'; -import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; -import {OnClickMenuItemModel} from '../menu/menu-item/models/onclick.model'; -import {getFirstCompletedRemoteData} from '../../core/shared/operators'; -import {map, switchMap} from 'rxjs/operators'; -import {DSpaceObjectDataService} from '../../core/data/dspace-object-data.service'; -import {URLCombiner} from '../../core/url-combiner/url-combiner'; -import {DsoVersioningModalService} from './dso-versioning-modal-service/dso-versioning-modal.service'; -import {hasNoValue, hasValue, isNotEmpty} from '../empty.util'; -import {MenuID} from '../menu/menu-id.model'; -import {MenuItemType} from '../menu/menu-item-type.model'; -import {MenuSection} from '../menu/menu-section.model'; -import {getDSORoute} from '../../app-routing-paths'; -import {ResearcherProfileDataService} from '../../core/profile/researcher-profile-data.service'; -import {NotificationsService} from '../notifications/notifications.service'; -import {TranslateService} from '@ngx-translate/core'; +import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router'; +import { combineLatest, Observable, of as observableOf } from 'rxjs'; +import { FeatureID } from '../../core/data/feature-authorization/feature-id'; +import { MenuService } from '../menu/menu.service'; +import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; +import { Injectable } from '@angular/core'; +import { LinkMenuItemModel } from '../menu/menu-item/models/link.model'; +import { Item } from '../../core/shared/item.model'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { OnClickMenuItemModel } from '../menu/menu-item/models/onclick.model'; +import { getFirstCompletedRemoteData } from '../../core/shared/operators'; +import { map, switchMap } from 'rxjs/operators'; +import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; +import { URLCombiner } from '../../core/url-combiner/url-combiner'; +import { DsoVersioningModalService } from './dso-versioning-modal-service/dso-versioning-modal.service'; +import { hasNoValue, hasValue, isNotEmpty } from '../empty.util'; +import { MenuID } from '../menu/menu-id.model'; +import { MenuItemType } from '../menu/menu-item-type.model'; +import { MenuSection } from '../menu/menu-section.model'; +import { getDSORoute } from '../../app-routing-paths'; +import { ResearcherProfileDataService } from '../../core/profile/researcher-profile-data.service'; +import { NotificationsService } from '../notifications/notifications.service'; +import { TranslateService } from '@ngx-translate/core'; +import { SubscriptionModalComponent } from '../subscriptions/subscription-modal/subscription-modal.component'; +import { Community } from '../../core/shared/community.model'; +import { Collection } from '../../core/shared/collection.model'; /** * Creates the menus for the dspace object pages @@ -52,27 +55,32 @@ export class DSOEditMenuResolver implements Resolve<{ [key: string]: MenuSection } else if (hasNoValue(id) && hasNoValue(route.queryParams.scope)) { return observableOf({}); } - return this.dSpaceObjectDataService.findById(id, true, false).pipe( - getFirstCompletedRemoteData(), - switchMap((dsoRD) => { + if (hasNoValue(id)) { + // If there's no ID, we're not on a DSO homepage, so pass on any pre-existing menu route data + return observableOf({ ...route.data?.menu }); + } else { + return this.dSpaceObjectDataService.findById(id, true, false).pipe( + getFirstCompletedRemoteData(), + switchMap((dsoRD) => { if (dsoRD.hasSucceeded && dsoRD.payload != null) { - const dso = dsoRD.payload; - return combineLatest(this.getDsoMenus(dso, route, state)).pipe( - // Menu sections are retrieved as an array of arrays and flattened into a single array - map((combinedMenus) => [].concat.apply([], combinedMenus)), - map((menus) => this.addDsoUuidToMenuIDs(menus, dso)), - map((menus) => { - return { - ...route.data?.menu, - [MenuID.DSO_EDIT]: menus - }; - }) - ); - } else { - return observableOf({...route.data?.menu}); - } - }) - ); + const dso = dsoRD.payload; + return combineLatest(this.getDsoMenus(dso, route, state)).pipe( + // Menu sections are retrieved as an array of arrays and flattened into a single array + map((combinedMenus) => [].concat.apply([], combinedMenus)), + map((menus) => this.addDsoUuidToMenuIDs(menus, dso)), + map((menus) => { + return { + ...route.data?.menu, + [MenuID.DSO_EDIT]: menus + }; + }) + ); + } else { + return observableOf({...route.data?.menu}); + } + }) + ); + } } /** @@ -81,6 +89,7 @@ export class DSOEditMenuResolver implements Resolve<{ [key: string]: MenuSection getDsoMenus(dso, route, state): Observable[] { return [ this.getItemMenu(dso), + this.getComColMenu(dso), this.getCommonMenu(dso, state) ]; } @@ -175,6 +184,39 @@ export class DSOEditMenuResolver implements Resolve<{ [key: string]: MenuSection } } + /** + * Get Community/Collection-specific menus + */ + protected getComColMenu(dso): Observable { + if (dso instanceof Community || dso instanceof Collection) { + return combineLatest([ + this.authorizationService.isAuthorized(FeatureID.CanSubscribe, dso.self), + ]).pipe( + map(([canSubscribe]) => { + return [ + { + id: 'subscribe', + active: false, + visible: canSubscribe, + model: { + type: MenuItemType.ONCLICK, + text: 'subscriptions.tooltip', + function: () => { + const modalRef = this.modalService.open(SubscriptionModalComponent); + modalRef.componentInstance.dso = dso; + } + } as OnClickMenuItemModel, + icon: 'bell', + index: 4 + }, + ]; + }) + ); + } else { + return observableOf([]); + } + } + /** * Claim a researcher by creating a profile * Shows notifications and/or hides the menu section on success/error diff --git a/src/app/shared/dso-page/dso-edit-menu/dso-edit-expandable-menu-section/dso-edit-menu-expandable-section.component.ts b/src/app/shared/dso-page/dso-edit-menu/dso-edit-expandable-menu-section/dso-edit-menu-expandable-section.component.ts index 8e4a7008afe..1925099418a 100644 --- a/src/app/shared/dso-page/dso-edit-menu/dso-edit-expandable-menu-section/dso-edit-menu-expandable-section.component.ts +++ b/src/app/shared/dso-page/dso-edit-menu/dso-edit-expandable-menu-section/dso-edit-menu-expandable-section.component.ts @@ -13,7 +13,6 @@ import { hasValue } from '../../../empty.util'; * Represents an expandable section in the dso edit menus */ @Component({ - /* tslint:disable:component-selector */ selector: 'ds-dso-edit-menu-expandable-section', templateUrl: './dso-edit-menu-expandable-section.component.html', styleUrls: ['./dso-edit-menu-expandable-section.component.scss'], diff --git a/src/app/shared/dso-page/dso-edit-menu/dso-edit-menu-section/dso-edit-menu-section.component.ts b/src/app/shared/dso-page/dso-edit-menu/dso-edit-menu-section/dso-edit-menu-section.component.ts index af3381ef716..060049ef5fc 100644 --- a/src/app/shared/dso-page/dso-edit-menu/dso-edit-menu-section/dso-edit-menu-section.component.ts +++ b/src/app/shared/dso-page/dso-edit-menu/dso-edit-menu-section/dso-edit-menu-section.component.ts @@ -10,7 +10,6 @@ import { MenuSection } from '../../../menu/menu-section.model'; * Represents a non-expandable section in the dso edit menus */ @Component({ - /* tslint:disable:component-selector */ selector: 'ds-dso-edit-menu-section', templateUrl: './dso-edit-menu-section.component.html', styleUrls: ['./dso-edit-menu-section.component.scss'] diff --git a/src/app/shared/dso-page/dso-page-subscription-button/dso-page-subscription-button.component.html b/src/app/shared/dso-page/dso-page-subscription-button/dso-page-subscription-button.component.html deleted file mode 100644 index 15135009fcd..00000000000 --- a/src/app/shared/dso-page/dso-page-subscription-button/dso-page-subscription-button.component.html +++ /dev/null @@ -1,8 +0,0 @@ - diff --git a/src/app/shared/dso-page/dso-page-subscription-button/dso-page-subscription-button.component.spec.ts b/src/app/shared/dso-page/dso-page-subscription-button/dso-page-subscription-button.component.spec.ts deleted file mode 100644 index 726854778dc..00000000000 --- a/src/app/shared/dso-page/dso-page-subscription-button/dso-page-subscription-button.component.spec.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { DsoPageSubscriptionButtonComponent } from './dso-page-subscription-button.component'; -import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; -import { of as observableOf } from 'rxjs'; -import { NgbModalModule } from '@ng-bootstrap/ng-bootstrap'; -import { By } from '@angular/platform-browser'; -import { DebugElement } from '@angular/core'; -import { Item } from '../../../core/shared/item.model'; -import { ITEM } from '../../../core/shared/item.resource-type'; -import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; -import { TranslateLoaderMock } from '../../mocks/translate-loader.mock'; - -describe('DsoPageSubscriptionButtonComponent', () => { - let component: DsoPageSubscriptionButtonComponent; - let fixture: ComponentFixture; - let de: DebugElement; - - const authorizationService = jasmine.createSpyObj('authorizationService', { - isAuthorized: jasmine.createSpy('isAuthorized') // observableOf(true) - }); - - const mockItem = Object.assign(new Item(), { - id: 'fake-id', - uuid: 'fake-id', - handle: 'fake/handle', - lastModified: '2018', - type: ITEM, - _links: { - self: { - href: 'https://localhost:8000/items/fake-id' - } - } - }); - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [ - NgbModalModule, - TranslateModule.forRoot({ - loader: { - provide: TranslateLoader, - useClass: TranslateLoaderMock - } - }) - ], - declarations: [ DsoPageSubscriptionButtonComponent ], - providers: [ - { provide: AuthorizationDataService, useValue: authorizationService }, - ] - }) - .compileComponents(); - }); - - beforeEach(() => { - fixture = TestBed.createComponent(DsoPageSubscriptionButtonComponent); - component = fixture.componentInstance; - de = fixture.debugElement; - component.dso = mockItem; - }); - - describe('when is authorized', () => { - beforeEach(() => { - authorizationService.isAuthorized.and.returnValue(observableOf(true)); - fixture.detectChanges(); - }); - - it('should display subscription button', () => { - expect(de.query(By.css(' [data-test="subscription-button"]'))).toBeTruthy(); - }); - }); - - describe('when is not authorized', () => { - beforeEach(() => { - authorizationService.isAuthorized.and.returnValue(observableOf(false)); - fixture.detectChanges(); - }); - - it('should not display subscription button', () => { - expect(de.query(By.css(' [data-test="subscription-button"]'))).toBeNull(); - }); - }); -}); diff --git a/src/app/shared/dso-page/dso-page-subscription-button/dso-page-subscription-button.component.ts b/src/app/shared/dso-page/dso-page-subscription-button/dso-page-subscription-button.component.ts deleted file mode 100644 index 54cd9e6bb0d..00000000000 --- a/src/app/shared/dso-page/dso-page-subscription-button/dso-page-subscription-button.component.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { Component, Input, OnInit } from '@angular/core'; - -import { Observable, of } from 'rxjs'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; - -import { DSpaceObject } from '../../../core/shared/dspace-object.model'; -import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; -import { SubscriptionModalComponent } from '../../subscriptions/subscription-modal/subscription-modal.component'; -import { FeatureID } from '../../../core/data/feature-authorization/feature-id'; - -@Component({ - selector: 'ds-dso-page-subscription-button', - templateUrl: './dso-page-subscription-button.component.html', - styleUrls: ['./dso-page-subscription-button.component.scss'] -}) -/** - * Display a button that opens the modal to manage subscriptions - */ -export class DsoPageSubscriptionButtonComponent implements OnInit { - - /** - * Whether the current user is authorized to edit the DSpaceObject - */ - isAuthorized$: Observable = of(false); - - /** - * Reference to NgbModal - */ - public modalRef: NgbModalRef; - - /** - * DSpaceObject that is being viewed - */ - @Input() dso: DSpaceObject; - - constructor( - protected authorizationService: AuthorizationDataService, - private modalService: NgbModal, - ) { - } - - /** - * check if the current DSpaceObject can be subscribed by the user - */ - ngOnInit(): void { - this.isAuthorized$ = this.authorizationService.isAuthorized(FeatureID.CanSubscribe, this.dso.self); - } - - /** - * Open the modal to subscribe to the related DSpaceObject - */ - public openSubscriptionModal() { - this.modalRef = this.modalService.open(SubscriptionModalComponent); - this.modalRef.componentInstance.dso = this.dso; - } - -} diff --git a/src/app/shared/dso-page/dso-page.module.ts b/src/app/shared/dso-page/dso-page.module.ts index 383ff04c532..ef970fa8ea7 100644 --- a/src/app/shared/dso-page/dso-page.module.ts +++ b/src/app/shared/dso-page/dso-page.module.ts @@ -9,7 +9,7 @@ import { import { DsoEditMenuExpandableSectionComponent } from './dso-edit-menu/dso-edit-expandable-menu-section/dso-edit-menu-expandable-section.component'; -import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; +import { NgbDropdownModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; const COMPONENTS = [ DsoEditMenuComponent, @@ -25,6 +25,7 @@ const MODULES = [ RouterModule, CommonModule, NgbTooltipModule, + NgbDropdownModule, ]; const PROVIDERS = [ diff --git a/src/app/shared/dso-page/dso-versioning-modal-service/dso-versioning-modal.service.spec.ts b/src/app/shared/dso-page/dso-versioning-modal-service/dso-versioning-modal.service.spec.ts index fc5c1dafc97..0e6cc177c20 100644 --- a/src/app/shared/dso-page/dso-versioning-modal-service/dso-versioning-modal.service.spec.ts +++ b/src/app/shared/dso-page/dso-versioning-modal-service/dso-versioning-modal.service.spec.ts @@ -18,6 +18,8 @@ describe('DsoVersioningModalService', () => { let router; let workspaceItemDataService; let itemService; + let editItemService; + let authorizationService; const mockItem: Item = Object.assign(new Item(), { bundles: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])), @@ -48,6 +50,8 @@ describe('DsoVersioningModalService', () => { router = jasmine.createSpyObj('router', ['navigateByUrl']); workspaceItemDataService = jasmine.createSpyObj('workspaceItemDataService', ['findByItem']); itemService = jasmine.createSpyObj('itemService', ['findByHref']); + editItemService = jasmine.createSpyObj('editItemService', ['invalidateItemCache']); + authorizationService = jasmine.createSpyObj('authorizationService', ['invalidateAuthorization']); service = new DsoVersioningModalService( modalService, @@ -56,7 +60,9 @@ describe('DsoVersioningModalService', () => { itemVersionShared, router, workspaceItemDataService, - itemService + itemService, + editItemService, + authorizationService, ); })); describe('when onCreateNewVersion() is called', () => { diff --git a/src/app/shared/dso-page/dso-versioning-modal-service/dso-versioning-modal.service.ts b/src/app/shared/dso-page/dso-versioning-modal-service/dso-versioning-modal.service.ts index 46792294dd8..d48ee93380c 100644 --- a/src/app/shared/dso-page/dso-versioning-modal-service/dso-versioning-modal.service.ts +++ b/src/app/shared/dso-page/dso-versioning-modal-service/dso-versioning-modal.service.ts @@ -16,6 +16,10 @@ import { ItemVersionsSharedService } from '../../../item-page/versions/item-vers import { ItemVersionsSummaryModalComponent } from '../../../item-page/versions/item-versions-summary-modal/item-versions-summary-modal.component'; +import { EditItemDataService } from '../../../core/submission/edititem-data.service'; +import { fromPromise } from 'rxjs/internal/observable/innerFrom'; +import { FeatureID } from '../../../core/data/feature-authorization/feature-id'; +import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; /** * Service to take care of all the functionality related to the version creation modal @@ -34,6 +38,8 @@ export class DsoVersioningModalService { protected router: Router, protected workspaceItemDataService: WorkspaceitemDataService, protected itemService: ItemDataService, + protected editItemService: EditItemDataService, + protected authorizationService: AuthorizationDataService, ) { } @@ -71,11 +77,15 @@ export class DsoVersioningModalService { getFirstSucceededRemoteDataPayload(), switchMap((newVersionItem: Item) => this.workspaceItemDataService.findByItem(newVersionItem.uuid, true, false)), getFirstSucceededRemoteDataPayload(), - ).subscribe((wsItem) => { - const wsiId = wsItem.id; - const route = 'workspaceitems/' + wsiId + '/edit'; - this.router.navigateByUrl(route); - }); + map((wsItem: WorkspaceItem) => `workspaceitems/${wsItem?.id}/edit`), + switchMap((route: string) => fromPromise(this.router.navigateByUrl(route))), + ).subscribe(() => this.invalidateCacheFor(item)); + } + + private invalidateCacheFor(previousItem: Item) { + this.versionService.invalidateVersionHrefCache(previousItem); + this.authorizationService.invalidateAuthorization(FeatureID.CanCreateVersion, previousItem.self); + this.editItemService.invalidateItemCache(previousItem.uuid); } /** diff --git a/src/app/shared/dso-selector/dso-selector/dso-selector.component.html b/src/app/shared/dso-selector/dso-selector/dso-selector.component.html index c4f5dbc4cd6..19ce1dea05b 100644 --- a/src/app/shared/dso-selector/dso-selector/dso-selector.component.html +++ b/src/app/shared/dso-selector/dso-selector/dso-selector.component.html @@ -6,12 +6,12 @@ [formControl]="input" ngbAutofocus (keyup.enter)="selectSingleResult()">
    -
    +
    diff --git a/src/app/shared/dso-selector/dso-selector/dso-selector.component.spec.ts b/src/app/shared/dso-selector/dso-selector/dso-selector.component.spec.ts index 7c28859388a..e2acd17bc05 100644 --- a/src/app/shared/dso-selector/dso-selector/dso-selector.component.spec.ts +++ b/src/app/shared/dso-selector/dso-selector/dso-selector.component.spec.ts @@ -11,6 +11,7 @@ import { PaginatedSearchOptions } from '../../search/models/paginated-search-opt import { hasValue } from '../../empty.util'; import { createPaginatedList } from '../../testing/utils.test'; import { NotificationsService } from '../../notifications/notifications.service'; +import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model'; describe('DSOSelectorComponent', () => { let component: DSOSelectorComponent; @@ -34,7 +35,7 @@ describe('DSOSelectorComponent', () => { ]; const searchService = { - search: (options: PaginatedSearchOptions) => { + search: (options: PaginatedSearchOptions, responseMsToLive?: number, useCachedVersionIfAvailable = true) => { if (hasValue(options.query) && options.query.startsWith('search.resourceid')) { return createSuccessfulRemoteDataObject$(createPaginatedList([searchResult])); } else if (options.pagination.currentPage === 1) { @@ -120,6 +121,43 @@ describe('DSOSelectorComponent', () => { }); }); + describe('search', () => { + beforeEach(() => { + spyOn(searchService, 'search').and.callThrough(); + }); + + it('should specify how to sort if no query is given', () => { + component.sort = new SortOptions('dc.title', SortDirection.ASC); + component.search(undefined, 0); + + expect(searchService.search).toHaveBeenCalledWith( + jasmine.objectContaining({ + query: undefined, + sort: jasmine.objectContaining({ + field: 'dc.title', + direction: SortDirection.ASC, + }), + }), + null, + true + ); + }); + + it('should not specify how to sort if a query is given', () => { + component.sort = new SortOptions('dc.title', SortDirection.ASC); + component.search('testQuery', 0); + + expect(searchService.search).toHaveBeenCalledWith( + jasmine.objectContaining({ + query: 'testQuery', + sort: null, + }), + null, + true + ); + }); + }); + describe('when search returns an error', () => { beforeEach(() => { spyOn(searchService, 'search').and.returnValue(createFailedRemoteDataObject$()); diff --git a/src/app/shared/dso-selector/dso-selector/dso-selector.component.ts b/src/app/shared/dso-selector/dso-selector/dso-selector.component.ts index 174614c79ac..b8880320ea5 100644 --- a/src/app/shared/dso-selector/dso-selector/dso-selector.component.ts +++ b/src/app/shared/dso-selector/dso-selector/dso-selector.component.ts @@ -9,7 +9,7 @@ import { QueryList, ViewChildren } from '@angular/core'; -import { FormControl } from '@angular/forms'; +import { UntypedFormControl } from '@angular/forms'; import { BehaviorSubject, @@ -31,6 +31,7 @@ import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../empty.util'; import { buildPaginatedList, PaginatedList } from '../../../core/data/paginated-list.model'; import { SearchResult } from '../../search/models/search-result.model'; +import { SortOptions } from '../../../core/cache/models/sort-options.model'; import { RemoteData } from '../../../core/data/remote-data'; import { NotificationsService } from '../../notifications/notifications.service'; import { TranslateService } from '@ngx-translate/core'; @@ -59,6 +60,12 @@ export class DSOSelectorComponent implements OnInit, OnDestroy { * The view mode of the listed objects */ viewMode = ViewMode.ListElement; + + /** + * The configuration. + */ + @Input() configuration = 'default'; + /** * The initially selected DSO's uuid */ @@ -69,10 +76,10 @@ export class DSOSelectorComponent implements OnInit, OnDestroy { */ @Input() types: DSpaceObjectType[]; - /** - * The configuration. + /** + * The sorting options */ - @Input() configuration = 'default'; + @Input() sort: SortOptions; // list of allowed selectable dsoTypes typesString: string; @@ -85,7 +92,7 @@ export class DSOSelectorComponent implements OnInit, OnDestroy { /** * Input form control to query the list */ - public input: FormControl = new FormControl(); + public input: UntypedFormControl = new UntypedFormControl(); /** * Default pagination for this feature @@ -135,6 +142,11 @@ export class DSOSelectorComponent implements OnInit, OnDestroy { */ public subs: Subscription[] = []; + /** + * Random seed of 4 characters to avoid duplicate ids + */ + randomSeed: string = Math.random().toString(36).substring(2, 6); + constructor( protected searchService: SearchService, protected notifcationsService: NotificationsService, @@ -226,6 +238,8 @@ export class DSOSelectorComponent implements OnInit, OnDestroy { * @param useCache Whether or not to use the cache */ search(query: string, page: number, useCache: boolean = true): Observable>>> { + // default sort is only used when there is not query + let efectiveSort = query ? null : this.sort; return this.searchService.search( new PaginatedSearchOptions({ query: query, @@ -233,7 +247,8 @@ export class DSOSelectorComponent implements OnInit, OnDestroy { configuration: this.configuration, pagination: Object.assign({}, this.defaultPagination, { currentPage: page - }) + }), + sort: efectiveSort }), null, useCache, diff --git a/src/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component.ts index d33f070a873..20084cc20f8 100644 --- a/src/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component.ts @@ -8,7 +8,8 @@ import { getCollectionCreateRoute, COLLECTION_PARENT_PARAMETER } from '../../../../collection-page/collection-page-routing-paths'; - +import { SortDirection, SortOptions } from '../../../../core/cache/models/sort-options.model'; +import { environment } from '../../../../../environments/environment'; /** * Component to wrap a list of existing communities inside a modal * Used to choose a community from to create a new collection in @@ -23,6 +24,7 @@ export class CreateCollectionParentSelectorComponent extends DSOSelectorModalWra selectorTypes = [DSpaceObjectType.COMMUNITY]; action = SelectorActionType.CREATE; header = 'dso-selector.create.collection.sub-level'; + defaultSort = new SortOptions(environment.comcolSelectionSort.sortField, environment.comcolSelectionSort.sortDirection as SortDirection); configuration = 'editCommunity'; constructor(protected activeModal: NgbActiveModal, protected route: ActivatedRoute, private router: Router) { diff --git a/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.html b/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.html index 04f5b1ecc4b..02098404405 100644 --- a/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.html +++ b/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.html @@ -14,6 +14,6 @@

    {{'dso-selector.create.community.sub-level' | translate}}
    - +
    diff --git a/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.ts index 809542e18be..40bac3eb9a0 100644 --- a/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.ts @@ -12,6 +12,8 @@ import { getCommunityCreateRoute, COMMUNITY_PARENT_PARAMETER } from '../../../../community-page/community-page-routing-paths'; +import { SortDirection, SortOptions } from '../../../../core/cache/models/sort-options.model'; +import { environment } from '../../../../../environments/environment'; /** * Component to wrap a button - for top communities - @@ -29,6 +31,7 @@ export class CreateCommunityParentSelectorComponent extends DSOSelectorModalWrap objectType = DSpaceObjectType.COMMUNITY; selectorTypes = [DSpaceObjectType.COMMUNITY]; action = SelectorActionType.CREATE; + defaultSort = new SortOptions(environment.comcolSelectionSort.sortField, environment.comcolSelectionSort.sortDirection as SortDirection); configuration = 'editCommunity'; constructor(protected activeModal: NgbActiveModal, protected route: ActivatedRoute, private router: Router) { diff --git a/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.ts index b109be0af2f..ed8a7b0780e 100644 --- a/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.ts @@ -4,6 +4,8 @@ import { DSpaceObjectType } from '../../../../core/shared/dspace-object-type.mod import { DSpaceObject } from '../../../../core/shared/dspace-object.model'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { DSOSelectorModalWrapperComponent, SelectorActionType } from '../dso-selector-modal-wrapper.component'; +import { SortDirection, SortOptions } from '../../../../core/cache/models/sort-options.model'; +import { environment } from '../../../../../environments/environment'; /** * Component to wrap a list of existing collections inside a modal @@ -21,6 +23,7 @@ export class CreateItemParentSelectorComponent extends DSOSelectorModalWrapperCo selectorTypes = [DSpaceObjectType.COLLECTION]; action = SelectorActionType.CREATE; header = 'dso-selector.create.item.sub-level'; + defaultSort = new SortOptions(environment.comcolSelectionSort.sortField, environment.comcolSelectionSort.sortDirection as SortDirection); /** * If present this value is used to filter collection list by entity type diff --git a/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.html b/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.html index 40579c2f3df..151ed5ef0c7 100644 --- a/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.html +++ b/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.html @@ -6,6 +6,6 @@
    diff --git a/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.ts b/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.ts index 16291054bfd..0853ad07ccc 100644 --- a/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/dso-selector-modal-wrapper.component.ts @@ -5,6 +5,7 @@ import { RemoteData } from '../../../core/data/remote-data'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model'; import { hasValue, isNotEmpty } from '../../empty.util'; +import { SortOptions } from '../../../core/cache/models/sort-options.model'; export enum SelectorActionType { CREATE = 'create', @@ -63,6 +64,11 @@ export abstract class DSOSelectorModalWrapperComponent implements OnInit { */ action: SelectorActionType; + /** + * Default DSO ordering + */ + defaultSort: SortOptions; + /** * Event emitted when a DSO entry is selected if emitOnly is set to true */ diff --git a/src/app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component.ts index 4a7130a99e6..04a20cffc8c 100644 --- a/src/app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component.ts @@ -8,6 +8,8 @@ import { SelectorActionType } from '../dso-selector-modal-wrapper.component'; import { getCollectionEditRoute } from '../../../../collection-page/collection-page-routing-paths'; +import { SortDirection, SortOptions } from '../../../../core/cache/models/sort-options.model'; +import { environment } from '../../../../../environments/environment'; /** * Component to wrap a list of existing collections inside a modal @@ -22,6 +24,7 @@ export class EditCollectionSelectorComponent extends DSOSelectorModalWrapperComp objectType = DSpaceObjectType.COLLECTION; selectorTypes = [DSpaceObjectType.COLLECTION]; action = SelectorActionType.EDIT; + defaultSort = new SortOptions(environment.comcolSelectionSort.sortField, environment.comcolSelectionSort.sortDirection as SortDirection); configuration = 'editCollection'; constructor(protected activeModal: NgbActiveModal, protected route: ActivatedRoute, private router: Router) { diff --git a/src/app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component.ts index 69f5b61f034..40d6e8d41ab 100644 --- a/src/app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component.ts @@ -8,6 +8,8 @@ import { SelectorActionType } from '../dso-selector-modal-wrapper.component'; import { getCommunityEditRoute } from '../../../../community-page/community-page-routing-paths'; +import { SortDirection, SortOptions } from '../../../../core/cache/models/sort-options.model'; +import { environment } from '../../../../../environments/environment'; /** * Component to wrap a list of existing communities inside a modal @@ -23,6 +25,7 @@ export class EditCommunitySelectorComponent extends DSOSelectorModalWrapperCompo objectType = DSpaceObjectType.COMMUNITY; selectorTypes = [DSpaceObjectType.COMMUNITY]; action = SelectorActionType.EDIT; + defaultSort = new SortOptions(environment.comcolSelectionSort.sortField, environment.comcolSelectionSort.sortDirection as SortDirection); configuration = 'editCommunity'; constructor(protected activeModal: NgbActiveModal, protected route: ActivatedRoute, private router: Router) { diff --git a/src/app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component.html b/src/app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component.html new file mode 100644 index 00000000000..85d8797e660 --- /dev/null +++ b/src/app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component.html @@ -0,0 +1,11 @@ +
    + + +
    diff --git a/src/app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component.ts index 4822849e4cc..c1ae5839081 100644 --- a/src/app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component.ts @@ -14,7 +14,7 @@ import { Item } from '../../../../core/shared/item.model'; @Component({ selector: 'ds-edit-item-selector', - templateUrl: '../dso-selector-modal-wrapper.component.html', + templateUrl: 'edit-item-selector.component.html', }) export class EditItemSelectorComponent extends DSOSelectorModalWrapperComponent implements OnInit { objectType = DSpaceObjectType.ITEM; diff --git a/src/app/shared/dso-selector/modal-wrappers/export-batch-selector/export-batch-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/export-batch-selector/export-batch-selector.component.ts index 47c38b6f748..9c6320a38b8 100644 --- a/src/app/shared/dso-selector/modal-wrappers/export-batch-selector/export-batch-selector.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/export-batch-selector/export-batch-selector.component.ts @@ -30,7 +30,7 @@ import { FeatureID } from '../../../../core/data/feature-authorization/feature-i templateUrl: '../dso-selector-modal-wrapper.component.html', }) export class ExportBatchSelectorComponent extends DSOSelectorModalWrapperComponent implements OnInit { - configuration = 'backend'; + configuration = 'communityOrCollection'; objectType = DSpaceObjectType.DSPACEOBJECT; selectorTypes = [DSpaceObjectType.COLLECTION]; action = SelectorActionType.EXPORT_BATCH; diff --git a/src/app/shared/dso-selector/modal-wrappers/export-metadata-csv-selector/export-metadata-csv-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/export-metadata-csv-selector/export-metadata-csv-selector.component.ts index 630a554141b..2ea439512ee 100644 --- a/src/app/shared/dso-selector/modal-wrappers/export-metadata-csv-selector/export-metadata-csv-selector.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/export-metadata-csv-selector/export-metadata-csv-selector.component.ts @@ -31,7 +31,7 @@ import { FeatureID } from '../../../../core/data/feature-authorization/feature-i templateUrl: '../dso-selector-modal-wrapper.component.html', }) export class ExportMetadataCsvSelectorComponent extends DSOSelectorModalWrapperComponent implements OnInit { - configuration = 'backend'; + configuration = 'communityOrCollection'; objectType = DSpaceObjectType.DSPACEOBJECT; selectorTypes = [DSpaceObjectType.COLLECTION, DSpaceObjectType.COMMUNITY]; action = SelectorActionType.EXPORT_METADATA_CSV; diff --git a/src/app/shared/dso-selector/modal-wrappers/export-metadata-xls-selector/export-metadata-xls-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/export-metadata-xls-selector/export-metadata-xls-selector.component.ts index 9f1a4dfc9ee..68c9349b6d8 100644 --- a/src/app/shared/dso-selector/modal-wrappers/export-metadata-xls-selector/export-metadata-xls-selector.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/export-metadata-xls-selector/export-metadata-xls-selector.component.ts @@ -28,7 +28,7 @@ import { getProcessDetailRoute } from '../../../../process-page/process-page-rou templateUrl: '../dso-selector-modal-wrapper.component.html', }) export class ExportMetadataXlsSelectorComponent extends DSOSelectorModalWrapperComponent implements OnInit { - configuration = 'backend'; + configuration = 'communityOrCollection'; objectType = DSpaceObjectType.DSPACEOBJECT; selectorTypes = [DSpaceObjectType.COLLECTION]; action = SelectorActionType.EXPORT_METADATA_XLS; diff --git a/src/app/shared/dso-selector/modal-wrappers/import-batch-selector/import-batch-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/import-batch-selector/import-batch-selector.component.ts index 4d0e2926c47..42eb8e9325f 100644 --- a/src/app/shared/dso-selector/modal-wrappers/import-batch-selector/import-batch-selector.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/import-batch-selector/import-batch-selector.component.ts @@ -16,7 +16,7 @@ import { Observable, of } from 'rxjs'; templateUrl: '../dso-selector-modal-wrapper.component.html', }) export class ImportBatchSelectorComponent extends DSOSelectorModalWrapperComponent implements OnInit { - configuration = 'backend'; + configuration = 'communityOrCollection'; objectType = DSpaceObjectType.DSPACEOBJECT; selectorTypes = [DSpaceObjectType.COLLECTION]; action = SelectorActionType.IMPORT_BATCH; diff --git a/src/app/shared/eperson-group-list/eperson-search-box/eperson-search-box.component.spec.ts b/src/app/shared/eperson-group-list/eperson-search-box/eperson-search-box.component.spec.ts index 5a9e74055aa..e88f08a1312 100644 --- a/src/app/shared/eperson-group-list/eperson-search-box/eperson-search-box.component.spec.ts +++ b/src/app/shared/eperson-group-list/eperson-search-box/eperson-search-box.component.spec.ts @@ -1,6 +1,6 @@ import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing'; import { Component, NO_ERRORS_SCHEMA } from '@angular/core'; -import { FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { UntypedFormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { TranslateModule } from '@ngx-translate/core'; @@ -13,7 +13,7 @@ describe('EpersonSearchBoxComponent test suite', () => { let compAsAny: any; let fixture: ComponentFixture; let de; - let formBuilder: FormBuilder; + let formBuilder: UntypedFormBuilder; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ @@ -27,7 +27,7 @@ describe('EpersonSearchBoxComponent test suite', () => { TestComponent ], providers: [ - FormBuilder, + UntypedFormBuilder, EpersonSearchBoxComponent ], schemas: [ @@ -64,7 +64,7 @@ describe('EpersonSearchBoxComponent test suite', () => { beforeEach(() => { // initTestScheduler(); fixture = TestBed.createComponent(EpersonSearchBoxComponent); - formBuilder = TestBed.inject(FormBuilder); + formBuilder = TestBed.inject(UntypedFormBuilder); comp = fixture.componentInstance; compAsAny = fixture.componentInstance; }); diff --git a/src/app/shared/eperson-group-list/eperson-search-box/eperson-search-box.component.ts b/src/app/shared/eperson-group-list/eperson-search-box/eperson-search-box.component.ts index 2aa4891c035..4689d29a8c8 100644 --- a/src/app/shared/eperson-group-list/eperson-search-box/eperson-search-box.component.ts +++ b/src/app/shared/eperson-group-list/eperson-search-box/eperson-search-box.component.ts @@ -1,5 +1,5 @@ import { Component, EventEmitter, Output } from '@angular/core'; -import { FormBuilder } from '@angular/forms'; +import { UntypedFormBuilder } from '@angular/forms'; import { Subscription } from 'rxjs'; @@ -33,7 +33,7 @@ export class EpersonSearchBoxComponent { */ @Output() search: EventEmitter = new EventEmitter(); - constructor(private formBuilder: FormBuilder) { + constructor(private formBuilder: UntypedFormBuilder) { this.searchForm = this.formBuilder.group(({ scope: 'metadata', query: '', diff --git a/src/app/shared/eperson-group-list/group-search-box/group-search-box.component.spec.ts b/src/app/shared/eperson-group-list/group-search-box/group-search-box.component.spec.ts index d28a144245a..b4c663902d8 100644 --- a/src/app/shared/eperson-group-list/group-search-box/group-search-box.component.spec.ts +++ b/src/app/shared/eperson-group-list/group-search-box/group-search-box.component.spec.ts @@ -1,6 +1,6 @@ import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing'; import { Component, NO_ERRORS_SCHEMA } from '@angular/core'; -import { FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { UntypedFormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { TranslateModule } from '@ngx-translate/core'; @@ -13,7 +13,7 @@ describe('GroupSearchBoxComponent test suite', () => { let compAsAny: any; let fixture: ComponentFixture; let de; - let formBuilder: FormBuilder; + let formBuilder: UntypedFormBuilder; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ @@ -27,7 +27,7 @@ describe('GroupSearchBoxComponent test suite', () => { TestComponent ], providers: [ - FormBuilder, + UntypedFormBuilder, GroupSearchBoxComponent ], schemas: [ @@ -64,7 +64,7 @@ describe('GroupSearchBoxComponent test suite', () => { beforeEach(() => { // initTestScheduler(); fixture = TestBed.createComponent(GroupSearchBoxComponent); - formBuilder = TestBed.inject(FormBuilder); + formBuilder = TestBed.inject(UntypedFormBuilder); comp = fixture.componentInstance; compAsAny = fixture.componentInstance; }); diff --git a/src/app/shared/eperson-group-list/group-search-box/group-search-box.component.ts b/src/app/shared/eperson-group-list/group-search-box/group-search-box.component.ts index 3e45bb0336e..154bee2d078 100644 --- a/src/app/shared/eperson-group-list/group-search-box/group-search-box.component.ts +++ b/src/app/shared/eperson-group-list/group-search-box/group-search-box.component.ts @@ -1,5 +1,5 @@ import { Component, EventEmitter, Output } from '@angular/core'; -import { FormBuilder } from '@angular/forms'; +import { UntypedFormBuilder } from '@angular/forms'; import { Subscription } from 'rxjs'; @@ -33,7 +33,7 @@ export class GroupSearchBoxComponent { */ @Output() search: EventEmitter = new EventEmitter(); - constructor(private formBuilder: FormBuilder) { + constructor(private formBuilder: UntypedFormBuilder) { this.searchForm = this.formBuilder.group(({ query: '', })); diff --git a/src/app/shared/error/error.component.ts b/src/app/shared/error/error.component.ts index 9a6b0660bb3..6572598c8b1 100644 --- a/src/app/shared/error/error.component.ts +++ b/src/app/shared/error/error.component.ts @@ -3,7 +3,7 @@ import { Component, Input } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { Subscription } from 'rxjs'; -import { AlertType } from '../alert/aletr-type'; +import { AlertType } from '../alert/alert-type'; @Component({ selector: 'ds-error', diff --git a/src/app/shared/explore/section-component/counters-section/counters-section.component.html b/src/app/shared/explore/section-component/counters-section/counters-section.component.html index ac433476d86..5db1d8b1a43 100644 --- a/src/app/shared/explore/section-component/counters-section/counters-section.component.html +++ b/src/app/shared/explore/section-component/counters-section/counters-section.component.html @@ -1,27 +1,30 @@
    -
    -
    +
    +
    -
    -
    - + + + + + + + + + + +
    + +
    + {{ 'explore.counters-section.' + counter.label | translate }} +
    +
    + {{ counter.count }} +
    -
    - {{'explore.counters-section.' + counter.label | translate}} -
    -
    - {{counter.count}} -
    -
    + +
    diff --git a/src/app/shared/explore/section-component/counters-section/counters-section.component.scss b/src/app/shared/explore/section-component/counters-section/counters-section.component.scss new file mode 100644 index 00000000000..c08f994bbe7 --- /dev/null +++ b/src/app/shared/explore/section-component/counters-section/counters-section.component.scss @@ -0,0 +1,13 @@ +.counters-section { + min-width: 120px; + max-width: 140px; + color: var(--bs-gray-800); + + &:hover { + color: #{darken($gray-800, 20%)}; + } +} + +.counters-label { + line-height: 1.25; +} diff --git a/src/app/shared/explore/section-component/counters-section/counters-section.component.ts b/src/app/shared/explore/section-component/counters-section/counters-section.component.ts index db50de91833..19273bbb627 100644 --- a/src/app/shared/explore/section-component/counters-section/counters-section.component.ts +++ b/src/app/shared/explore/section-component/counters-section/counters-section.component.ts @@ -1,9 +1,9 @@ -import { Component, Inject, Input, OnInit } from '@angular/core'; +import { Component, Inject, Input, OnInit, PLATFORM_ID } from '@angular/core'; +import { isPlatformServer } from '@angular/common'; import { BehaviorSubject, forkJoin, Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { NativeWindowRef, NativeWindowService } from '../../../../core/services/window.service'; import { DSpaceObject } from '../../../../core/shared/dspace-object.model'; import { SearchObjects } from '../../../search/models/search-objects.model'; import { getFirstSucceededRemoteDataPayload } from '../../../../core/shared/operators'; @@ -11,11 +11,12 @@ import { PaginationComponentOptions } from '../../../pagination/pagination-compo import { SectionComponent } from '../../../../core/layout/models/section.model'; import { SearchService } from '../../../../core/shared/search/search.service'; import { PaginatedSearchOptions } from '../../../search/models/paginated-search-options.model'; -import { hasValue } from '../../../empty.util'; import { UUIDService } from '../../../../core/shared/uuid.service'; +import { InternalLinkService } from 'src/app/core/services/internal-link.service'; @Component({ selector: 'ds-counters-section', + styleUrls: ['./counters-section.component.scss'], templateUrl: './counters-section.component.html' }) export class CountersSectionComponent implements OnInit { @@ -37,13 +38,20 @@ export class CountersSectionComponent implements OnInit { }); - constructor(private searchService: SearchService, + constructor( + public internalLinkService: InternalLinkService, + private searchService: SearchService, private uuidService: UUIDService, - @Inject(NativeWindowService) protected _window: NativeWindowRef) { + @Inject(PLATFORM_ID) private platformId: Object, + ) { } ngOnInit() { + if (isPlatformServer(this.platformId)) { + return; + } + this.counterData$ = forkJoin( this.countersSection.counterSettingsList.map((counterSettings: CountersSettings) => this.searchService.search(new PaginatedSearchOptions({ @@ -63,12 +71,6 @@ export class CountersSectionComponent implements OnInit { ))); this.counterData$.subscribe(() => this.isLoading$.next(false)); } - - goToLink(link: string) { - if (hasValue(link)) { - this._window.nativeWindow.location.href = link; - } - } } diff --git a/src/app/shared/explore/section-component/top-section/top-section.component.html b/src/app/shared/explore/section-component/top-section/top-section.component.html index 6aeff2e88f4..6702d9dbb7b 100644 --- a/src/app/shared/explore/section-component/top-section/top-section.component.html +++ b/src/app/shared/explore/section-component/top-section/top-section.component.html @@ -3,7 +3,7 @@
    {{ 'explore.index.' + topSection.titleKey | translate }}
    {{ 'explore.index.' + topSection.sortField | translate }}
    - +
    diff --git a/src/app/shared/explore/section-component/top-section/top-section.component.spec.ts b/src/app/shared/explore/section-component/top-section/top-section.component.spec.ts index 50d31e5089d..a77ceb6bb07 100644 --- a/src/app/shared/explore/section-component/top-section/top-section.component.spec.ts +++ b/src/app/shared/explore/section-component/top-section/top-section.component.spec.ts @@ -82,7 +82,8 @@ describe('TopSectionComponent', () => { order: 'desc', sortField: 'dc.date.accessioned', numberOfItems: 5, - titleKey: undefined + titleKey: undefined, + showThumbnails: false, }; fixture.detectChanges(); @@ -121,7 +122,8 @@ describe('TopSectionComponent', () => { order: 'desc', sortField: 'dc.date.foo', numberOfItems: 5, - titleKey: 'lastPublications' + titleKey: 'lastPublications', + showThumbnails: false, }; fixture.detectChanges(); diff --git a/src/app/shared/explore/section-component/top-section/top-section.component.ts b/src/app/shared/explore/section-component/top-section/top-section.component.ts index 34011929573..5e312ca2075 100644 --- a/src/app/shared/explore/section-component/top-section/top-section.component.ts +++ b/src/app/shared/explore/section-component/top-section/top-section.component.ts @@ -26,6 +26,8 @@ export class TopSectionComponent implements OnInit { paginatedSearchOptions: PaginatedSearchOptions; + showThumbnails: boolean; + ngOnInit() { const order = this.topSection.order; const numberOfItems = this.topSection.numberOfItems; @@ -41,6 +43,8 @@ export class TopSectionComponent implements OnInit { pagination: pagination, sort: new SortOptions(this.topSection.sortField, sortDirection) }); + + this.showThumbnails = this.topSection.showThumbnails; } } diff --git a/src/app/shared/file-download-link/file-download-link.component.html b/src/app/shared/file-download-link/file-download-link.component.html index d08fb50f9a4..e799e1341c2 100644 --- a/src/app/shared/file-download-link/file-download-link.component.html +++ b/src/app/shared/file-download-link/file-download-link.component.html @@ -1,6 +1,6 @@ -  -  - + + + diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html index 385104a727e..67e40a0bce1 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html @@ -21,7 +21,7 @@
    {{lang.display}}
    -
    +
    diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.scss b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.scss index 1f87fa19083..0ddc0a9908c 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.scss +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.scss @@ -16,4 +16,8 @@ appearance: none; } +.date-field-security { + padding-top: 2rem; +} + diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts index dceb8d2de63..68e5f5ca6c0 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts @@ -1,6 +1,6 @@ import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing'; import { CUSTOM_ELEMENTS_SCHEMA, DebugElement, NgZone, SimpleChange } from '@angular/core'; -import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { UntypedFormControl, UntypedFormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { BrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/testing'; import { By } from '@angular/platform-browser'; @@ -152,13 +152,14 @@ describe('DsDynamicFormControlContainerComponent test suite', () => { id: 'checkboxList', vocabularyOptions: vocabularyOptions, repeatable: true, - hint: 'test hint', - required: false + required: false, + hint: 'test hint' }), new DynamicListRadioGroupModel({ id: 'radioList', vocabularyOptions: vocabularyOptions, - repeatable: false + repeatable: false, + required: false, }), new DynamicRelationGroupModel({ submissionId: '1234', @@ -188,7 +189,7 @@ describe('DsDynamicFormControlContainerComponent test suite', () => { metadataFields: [], hasSelectableMetadata: false }), - new DynamicDsDatePickerModel({ id: 'datepicker' }), + new DynamicDsDatePickerModel({ id: 'datepicker', repeatable: false }), new DynamicLookupModel({ id: 'lookup', metadataFields: [], @@ -206,7 +207,7 @@ describe('DsDynamicFormControlContainerComponent test suite', () => { new DynamicQualdropModel({ id: 'combobox', readOnly: false, required: false }) ]; const testModel = formModel[8]; - let formGroup: FormGroup; + let formGroup: UntypedFormGroup; let fixture: ComponentFixture; let component: DsDynamicFormControlContainerComponent; let debugElement: DebugElement; @@ -289,8 +290,8 @@ describe('DsDynamicFormControlContainerComponent test suite', () => { it('should initialize correctly', () => { expect(component.context).toBeNull(); - expect(component.control instanceof FormControl).toBe(true); - expect(component.group instanceof FormGroup).toBe(true); + expect(component.control instanceof UntypedFormControl).toBe(true); + expect(component.group instanceof UntypedFormGroup).toBe(true); expect(component.model instanceof DynamicFormControlModel).toBe(true); expect(component.hasErrorMessaging).toBe(false); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts index 01cc2434fe3..c98d327c8ab 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts @@ -16,7 +16,7 @@ import { ViewChild, ViewContainerRef } from '@angular/core'; -import { FormArray, FormGroup } from '@angular/forms'; +import { UntypedFormArray, UntypedFormGroup } from '@angular/forms'; import { DYNAMIC_FORM_CONTROL_TYPE_ARRAY, @@ -122,6 +122,7 @@ import { FormFieldMetadataValueObject } from '../models/form-field-metadata-valu import { APP_CONFIG, AppConfig } from '../../../../../config/app-config.interface'; import { itemLinksToFollow } from '../../../utils/relation-query.utils'; import { DynamicConcatModel } from './models/ds-dynamic-concat.model'; +import { Metadata } from '../../../../core/shared/metadata.utils'; export function dsDynamicFormControlMapFn(model: DynamicFormControlModel): Type | null { switch (model.type) { @@ -200,12 +201,12 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo @Input('templates') inputTemplateList: QueryList; @Input() hasMetadataModel: any; @Input() formId: string; - @Input() formGroup: FormGroup; + @Input() formGroup: UntypedFormGroup; @Input() formModel: DynamicFormControlModel[]; @Input() asBootstrapFormGroup = false; @Input() bindId = true; @Input() context: any | null = null; - @Input() group: FormGroup; + @Input() group: UntypedFormGroup; @Input() hostClass: string[]; @Input() hasErrorMessaging = false; @Input() layout = null as DynamicFormLayout; @@ -263,7 +264,7 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo private submissionObjectService: SubmissionObjectDataService, private ref: ChangeDetectorRef, private formService: FormService, - private formBuilderService: FormBuilderService, + public formBuilderService: FormBuilderService, private submissionService: SubmissionService, @Inject(APP_CONFIG) protected appConfig: AppConfig, ) { @@ -347,9 +348,15 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo ); } } - if (this.model && this.model.value && this.model.value.securityLevel !== undefined) { + + if (isNotEmpty(this.model?.value?.securityLevel)) { this.securityLevel = this.model.value.securityLevel; + } else if (isNotEmpty(this.model?.metadataValue?.securityLevel)) { + this.securityLevel = this.model.metadataValue.securityLevel; + } else { + this.securityLevel = this.model.securityLevel; } + } get isCheckbox(): boolean { @@ -479,7 +486,7 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo onRemove(): void { const arrayContext: DynamicFormArrayModel = (this.context as DynamicFormArrayGroupModel).context; const path = this.formBuilderService.getPath(arrayContext); - const formArrayControl = this.group.root.get(path) as FormArray; + const formArrayControl = this.group.root.get(path) as UntypedFormArray; this.formBuilderService.removeFormArrayGroup(this.context.index, formArrayControl, arrayContext); if (this.model.parent.context.groups.length === 0) { this.formBuilderService.addFormArrayGroup(formArrayControl, arrayContext); @@ -499,6 +506,14 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo return isNotEmpty(this.model.hint) && this.model.hint !== ' '; } + get hasValue(): boolean { + if (hasValue(this.model.metadataValue)) { + return Metadata.hasValue(this.model?.metadataValue); + } else { + return Metadata.hasValue(this.model?.value); + } + } + /** * Check if the current field has a hint and is repeatable * for specific input types such as CHECKBOX_GROUP (input-type: LIST) and RELATION (input-type: GROUP) diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form.component.ts index ca52cc47c78..fb8fd2da2db 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form.component.ts @@ -8,7 +8,7 @@ import { QueryList, ViewChildren } from '@angular/core'; -import { FormGroup } from '@angular/forms'; +import { UntypedFormGroup } from '@angular/forms'; import { DynamicFormComponent, DynamicFormComponentService, @@ -27,7 +27,7 @@ import { DsDynamicFormControlContainerComponent } from './ds-dynamic-form-contro export class DsDynamicFormComponent extends DynamicFormComponent { @Input() formId: string; - @Input() formGroup: FormGroup; + @Input() formGroup: UntypedFormGroup; @Input() formModel: DynamicFormControlModel[]; @Input() formLayout: DynamicFormLayout; @Input() entityType: string; diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service.spec.ts index bd2806c59f5..fb4e752866e 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service.spec.ts @@ -16,7 +16,7 @@ import { } from '../../../mocks/form-models.mock'; import { DsDynamicTypeBindRelationService } from './ds-dynamic-type-bind-relation.service'; import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model'; -import { FormControl, ReactiveFormsModule } from '@angular/forms'; +import {UntypedFormControl, ReactiveFormsModule} from '@angular/forms'; import { FormBuilderService } from '../form-builder.service'; import { getMockFormBuilderService } from '../../../mocks/form-builder-service.mock'; import { Injector } from '@angular/core'; @@ -89,7 +89,7 @@ describe('DSDynamicTypeBindRelationService test suite', () => { it('Should receive one subscription to dc.type type binding"', () => { const testModel = mockInputWithTypeBindModel; testModel.typeBindRelations = getTypeBindRelations(['boundType']); - const dcTypeControl = new FormControl(); + const dcTypeControl = new UntypedFormControl(); dcTypeControl.setValue('boundType'); let subscriptions = service.subscribeRelations(testModel, dcTypeControl); expect(subscriptions).toHaveSize(1); @@ -98,7 +98,7 @@ describe('DSDynamicTypeBindRelationService test suite', () => { it('Expect hasMatch to be true (ie. this should be hidden)', () => { const testModel = mockInputWithTypeBindModel; testModel.typeBindRelations = getTypeBindRelations(['boundType']); - const dcTypeControl = new FormControl(); + const dcTypeControl = new UntypedFormControl(); dcTypeControl.setValue('boundType'); testModel.typeBindRelations[0].when[0].value = 'anotherType'; const relation = dynamicFormRelationService.findRelationByMatcher((testModel as any).typeBindRelations, HIDDEN_MATCHER); @@ -113,7 +113,7 @@ describe('DSDynamicTypeBindRelationService test suite', () => { it('Expect hasMatch to be false (ie. this should NOT be hidden)', () => { const testModel = mockInputWithTypeBindModel; testModel.typeBindRelations = getTypeBindRelations(['boundType']); - const dcTypeControl = new FormControl(); + const dcTypeControl = new UntypedFormControl(); dcTypeControl.setValue('boundType'); testModel.typeBindRelations[0].when[0].value = 'boundType'; const relation = dynamicFormRelationService.findRelationByMatcher((testModel as any).typeBindRelations, HIDDEN_MATCHER); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service.ts index 5dd4a6627d0..5f7e2e3e228 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service.ts @@ -1,5 +1,5 @@ import { Inject, Injectable, Injector, Optional } from '@angular/core'; -import { FormControl } from '@angular/forms'; +import { UntypedFormControl } from '@angular/forms'; import { Subscription } from 'rxjs'; import { startWith } from 'rxjs/operators'; @@ -172,7 +172,7 @@ export class DsDynamicTypeBindRelationService { * @param model * @param control */ - subscribeRelations(model: DynamicFormControlModel, control: FormControl): Subscription[] { + subscribeRelations(model: DynamicFormControlModel, control: UntypedFormControl): Subscription[] { const relatedModels = this.getRelatedFormModel(model); const subscriptions: Subscription[] = []; @@ -183,7 +183,8 @@ export class DsDynamicTypeBindRelationService { const initValue = (hasNoValue(relatedModel.value) || typeof relatedModel.value === 'string') ? relatedModel.value : (Array.isArray(relatedModel.value) ? relatedModel.value : relatedModel.value.value); - const valueChanges = relatedModel.valueChanges.pipe( + const updateSubject = (relatedModel.type === 'CHECKBOX_GROUP' ? relatedModel.valueUpdates : relatedModel.valueChanges); + const valueChanges = updateSubject.pipe( startWith(initValue) ); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.ts index 3160bccb41d..aa50133e8a2 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.ts @@ -1,6 +1,6 @@ /* eslint-disable max-classes-per-file */ import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output } from '@angular/core'; -import { FormControl } from '@angular/forms'; +import { UntypedFormControl } from '@angular/forms'; import { DynamicFormArrayGroupModel } from '@ng-dynamic-forms/core'; import { Store } from '@ngrx/store'; import { BehaviorSubject, Subscription } from 'rxjs'; @@ -64,7 +64,7 @@ export class ReorderableFormFieldMetadataValue extends Reorderable { constructor( public metadataValue: FormFieldMetadataValueObject, public model: DynamicConcatModel, - public control: FormControl, + public control: UntypedFormControl, public group: DynamicFormArrayGroupModel, oldIndex?: number, newIndex?: number diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.html index d518d59da25..dd19e6158df 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.html @@ -12,12 +12,11 @@ [formGroupName]="idx" [ngClass]="[getClass('element', 'group'), getClass('grid', 'group')]" cdkDrag - cdkDragHandle [cdkDragDisabled]="dragDisabled" [cdkDragPreviewClass]="'ds-submission-reorder-dragging'" [class.grey-background]="model.isInlineGroupArray"> -
    +
    diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.ts index 9802724dc99..fc22dd0a4ad 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/array-group/dynamic-form-array.component.ts @@ -1,6 +1,6 @@ import { CdkDragDrop } from '@angular/cdk/drag-drop'; import { Component, EventEmitter, Input, Output, QueryList } from '@angular/core'; -import { FormGroup } from '@angular/forms'; +import { UntypedFormGroup } from '@angular/forms'; import { DynamicFormArrayComponent, DynamicFormControlCustomEvent, @@ -26,7 +26,7 @@ export class DsDynamicFormArrayComponent extends DynamicFormArrayComponent { @Input() bindId = true; @Input() formModel: DynamicFormControlModel[]; @Input() formLayout: DynamicFormLayout; - @Input() group: FormGroup; + @Input() group: UntypedFormGroup; @Input() layout: DynamicFormControlLayout; @Input() model: DynamicRowArrayModel;// DynamicRow? @Input() templates: QueryList | undefined; diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component.spec.ts index 9f85ccc013f..ceb498fe567 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component.spec.ts @@ -1,5 +1,5 @@ import { DynamicFormsCoreModule, DynamicFormService } from '@ng-dynamic-forms/core'; -import { FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { UntypedFormGroup, ReactiveFormsModule } from '@angular/forms'; import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing'; import { DebugElement } from '@angular/core'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; @@ -11,7 +11,7 @@ describe('CustomSwitchComponent', () => { const testModel = new DynamicCustomSwitchModel({ id: 'switch' }); const formModel = [testModel]; - let formGroup: FormGroup; + let formGroup: UntypedFormGroup; let fixture: ComponentFixture; let component: CustomSwitchComponent; let debugElement: DebugElement; @@ -47,7 +47,7 @@ describe('CustomSwitchComponent', () => { it('should initialize correctly', () => { expect(component.bindId).toBe(true); - expect(component.group instanceof FormGroup).toBe(true); + expect(component.group instanceof UntypedFormGroup).toBe(true); expect(component.model instanceof DynamicCustomSwitchModel).toBe(true); expect(component.blur).toBeDefined(); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component.ts index 5b3f1e89e4c..47780e66f6a 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.component.ts @@ -1,5 +1,5 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { FormGroup } from '@angular/forms'; +import { UntypedFormGroup } from '@angular/forms'; import { DynamicNGBootstrapCheckboxComponent } from '@ng-dynamic-forms/ui-ng-bootstrap'; import { DynamicFormLayoutService, DynamicFormValidationService } from '@ng-dynamic-forms/core'; @@ -24,7 +24,7 @@ export class CustomSwitchComponent extends DynamicNGBootstrapCheckboxComponent { /** * The formgroup containing this component */ - @Input() group: FormGroup; + @Input() group: UntypedFormGroup; /** * The model used for displaying the switch diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker-inline/dynamic-date-picker-inline.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker-inline/dynamic-date-picker-inline.component.spec.ts index 0756e48a8da..c0d1c83bf9f 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker-inline/dynamic-date-picker-inline.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker-inline/dynamic-date-picker-inline.component.spec.ts @@ -1,6 +1,6 @@ import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing'; import { DebugElement } from '@angular/core'; -import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { UntypedFormControl, UntypedFormGroup, ReactiveFormsModule } from '@angular/forms'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { By } from '@angular/platform-browser'; import { NgbDatepickerModule } from '@ng-bootstrap/ng-bootstrap'; @@ -11,7 +11,7 @@ describe('DsDatePickerInlineComponent test suite', () => { const testModel = new DynamicDatePickerModel({ id: 'datepicker' }); const formModel = [testModel]; - let formGroup: FormGroup; + let formGroup: UntypedFormGroup; let fixture: ComponentFixture; let component: DsDatePickerInlineComponent; let debugElement: DebugElement; @@ -53,8 +53,8 @@ describe('DsDatePickerInlineComponent test suite', () => { it('should initialize correctly', () => { expect(component.bindId).toBe(true); - expect(component.control instanceof FormControl).toBe(true); - expect(component.group instanceof FormGroup).toBe(true); + expect(component.control instanceof UntypedFormControl).toBe(true); + expect(component.group instanceof UntypedFormGroup).toBe(true); expect(component.model instanceof DynamicDatePickerModel).toBe(true); expect(component.blur).toBeDefined(); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker-inline/dynamic-date-picker-inline.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker-inline/dynamic-date-picker-inline.component.ts index 4ca7492f4be..d151a490845 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker-inline/dynamic-date-picker-inline.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker-inline/dynamic-date-picker-inline.component.ts @@ -1,5 +1,5 @@ import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core'; -import { FormGroup } from '@angular/forms'; +import { UntypedFormGroup } from '@angular/forms'; import { NgbDateParserFormatter, NgbDatepicker, NgbDatepickerConfig, NgbDateStruct } from '@ng-bootstrap/ng-bootstrap'; import { DynamicDatePickerModel, @@ -17,7 +17,7 @@ import { export class DsDatePickerInlineComponent extends DynamicFormControlComponent { @Input() bindId = true; - @Input() group: FormGroup; + @Input() group: UntypedFormGroup; @Input() layout: DynamicFormControlLayout; @Input() model: DynamicDatePickerModel; diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.spec.ts index f3ca741475c..5033242a7e6 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.spec.ts @@ -1,7 +1,7 @@ // Load the implementations that should be tested import { ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA, Renderer2 } from '@angular/core'; import { ComponentFixture, fakeAsync, inject, TestBed, tick, waitForAsync, } from '@angular/core/testing'; -import { FormControl, FormGroup } from '@angular/forms'; +import { UntypedFormControl, UntypedFormGroup } from '@angular/forms'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { DynamicFormLayoutService, DynamicFormValidationService } from '@ng-dynamic-forms/core'; @@ -16,8 +16,8 @@ import { import { By } from '@angular/platform-browser'; -export const DATE_TEST_GROUP = new FormGroup({ - date: new FormControl() +export const DATE_TEST_GROUP = new UntypedFormGroup({ + date: new UntypedFormControl() }); export const DATE_TEST_MODEL_CONFIG = { @@ -29,6 +29,7 @@ export const DATE_TEST_MODEL_CONFIG = { placeholder: 'Date', readOnly: false, required: true, + repeatable: false, toggleIcon: 'fas fa-calendar' }; diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.ts index 2fafdc0ae3d..faeed8f44a5 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.ts @@ -1,5 +1,5 @@ import { Component, EventEmitter, HostListener, Inject, Input, OnInit, Output, Renderer2 } from '@angular/core'; -import { FormGroup } from '@angular/forms'; +import { UntypedFormGroup } from '@angular/forms'; import { DynamicDsDatePickerModel } from './date-picker.model'; import { hasValue } from '../../../../../empty.util'; import { @@ -23,7 +23,7 @@ export const DS_DATE_PICKER_SEPARATOR = '-'; export class DsDatePickerComponent extends DynamicFormControlComponent implements OnInit { @Input() bindId = true; - @Input() group: FormGroup; + @Input() group: UntypedFormGroup; @Input() model: DynamicDsDatePickerModel; @Input() legend: string; @@ -88,8 +88,9 @@ export class DsDatePickerComponent extends DynamicFormControlComponent implement this.day = this.initialDay; } } - this.maxYear = this.initialYear + 100; - } + + this.maxYear = now.getUTCFullYear() + 100; + } onBlur(event) { this.blur.emit(); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.model.ts index 91a7a56a52a..31c4e575b35 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.model.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.model.ts @@ -15,6 +15,7 @@ export const DYNAMIC_FORM_CONTROL_TYPE_DSDATEPICKER = 'DATE'; export interface DynamicDsDatePickerModelConfig extends DynamicDatePickerModelConfig { legend?: string; typeBindRelations?: DynamicFormControlRelation[]; + repeatable: boolean; securityLevel?: number; securityConfigLevel?: number[]; toggleSecurityVisibility?: boolean; @@ -47,6 +48,7 @@ export class DynamicDsDatePickerModel extends DynamicDateControlModel { this.toggleSecurityVisibility = config.toggleSecurityVisibility; } this.typeBindRelations = config.typeBindRelations ? config.typeBindRelations : []; + this.repeatable = config.repeatable; this.hiddenUpdates = new BehaviorSubject(this.hidden); this.hiddenUpdates.subscribe((hidden: boolean) => { this.hidden = hidden; diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/disabled/dynamic-disabled.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/disabled/dynamic-disabled.component.spec.ts index 8cfa5c818a5..a25ad4d2314 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/disabled/dynamic-disabled.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/disabled/dynamic-disabled.component.spec.ts @@ -1,6 +1,6 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core'; -import { FormControl, FormGroup, FormsModule } from '@angular/forms'; +import { UntypedFormControl, UntypedFormGroup, FormsModule } from '@angular/forms'; import { By } from '@angular/platform-browser'; import { TranslateModule } from '@ngx-translate/core'; @@ -31,8 +31,8 @@ describe('DsDynamicDisabledComponent', () => { name: 'disabledInput', hasSelectableMetadata: false }); - group = new FormGroup({ - disabledInput: new FormControl(), + group = new UntypedFormGroup({ + disabledInput: new UntypedFormControl(), }); } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/disabled/dynamic-disabled.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/disabled/dynamic-disabled.component.ts index 974858b1cc0..222ad510496 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/disabled/dynamic-disabled.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/disabled/dynamic-disabled.component.ts @@ -1,5 +1,5 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { FormGroup } from '@angular/forms'; +import { UntypedFormGroup } from '@angular/forms'; import { DynamicFormControlComponent, DynamicFormLayoutService, DynamicFormValidationService } from '@ng-dynamic-forms/core'; @@ -18,7 +18,7 @@ import { DynamicDisabledModel } from './dynamic-disabled.model'; export class DsDynamicDisabledComponent extends DynamicFormControlComponent { @Input() formId: string; - @Input() group: FormGroup; + @Input() group: UntypedFormGroup; @Input() model: DynamicDisabledModel; modelValuesString = ''; diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-concat.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-concat.model.ts index edaf48816e3..1b6a7ec3100 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-concat.model.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-concat.model.ts @@ -49,6 +49,7 @@ export class DynamicConcatModel extends DynamicFormGroupModel { @serializable() submissionId: string; @serializable() hasSelectableMetadata: boolean; @serializable() metadataValue: MetadataValue; + @serializable() readOnly?: boolean; @serializable() securityLevel?: number; @serializable() securityConfigLevel?: number[]; @serializable() toggleSecurityVisibility = true; @@ -70,6 +71,7 @@ export class DynamicConcatModel extends DynamicFormGroupModel { this.valueUpdates = new Subject(); this.valueUpdates.subscribe((value: string) => this.value = value); this.typeBindRelations = config.typeBindRelations ? config.typeBindRelations : []; + this.readOnly = config.disabled; this.securityLevel = config.securityLevel; this.securityConfigLevel = config.securityConfigLevel; if (isNotUndefined(config.toggleSecurityVisibility)) { diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model.ts index 3fdf35de36b..9546316b8a3 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model.ts @@ -65,6 +65,7 @@ export class DsDynamicInputModel extends DynamicInputModel { this.metadataFields = config.metadataFields; this.hint = config.hint; this.readOnly = config.readOnly; + this.disabled = config.readOnly; this.value = config.value; this.relationship = config.relationship; this.submissionId = config.submissionId; diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-vocabulary.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-vocabulary.component.ts index 6d3d145db19..2b0d815dd8b 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-vocabulary.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-vocabulary.component.ts @@ -1,5 +1,5 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { FormGroup } from '@angular/forms'; +import { UntypedFormGroup } from '@angular/forms'; import { DynamicFormControlComponent, @@ -37,7 +37,7 @@ import { Metadata } from '../../../../../core/shared/metadata.utils'; }) export abstract class DsDynamicVocabularyComponent extends DynamicFormControlComponent { - @Input() abstract group: FormGroup; + @Input() abstract group: UntypedFormGroup; @Input() abstract model: DsDynamicInputModel; @Output() abstract blur: EventEmitter; @@ -77,7 +77,7 @@ export abstract class DsDynamicVocabularyComponent extends DynamicFormControlCom */ getInitValueFromModel(): Observable { let initValue$: Observable; - if (isNotEmpty(this.model.value) && (this.model.value instanceof FormFieldMetadataValueObject)) { + if (isNotEmpty(this.model.value) && (this.model.value instanceof FormFieldMetadataValueObject) && !this.model.value.hasAuthorityToGenerate()) { let initEntry$: Observable; if (this.hasValidAuthority(this.model.value)) { initEntry$ = this.vocabularyService.getVocabularyEntryByID(this.model.value.authority, this.model.vocabularyOptions); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/form-group/dynamic-form-group.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/form-group/dynamic-form-group.component.ts index 77bcbf671a9..06874f41fc1 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/form-group/dynamic-form-group.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/form-group/dynamic-form-group.component.ts @@ -1,6 +1,5 @@ import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, QueryList } from '@angular/core'; -import { FormGroup } from '@angular/forms'; - +import { UntypedFormGroup } from '@angular/forms'; import { DynamicFormControlComponent, DynamicFormControlCustomEvent, @@ -23,7 +22,7 @@ export class DsDynamicFormGroupComponent extends DynamicFormControlComponent { @Input() formModel: DynamicFormControlModel[]; @Input() formLayout: DynamicFormLayout; - @Input() group: FormGroup; + @Input() group: UntypedFormGroup; @Input() layout: DynamicFormControlLayout; @Input() model: DynamicFormGroupModel; @Input() templates: QueryList | DynamicTemplateDirective[] | undefined; diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list-checkbox-group.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list-checkbox-group.model.ts index 85673fb9240..2420818acdf 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list-checkbox-group.model.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list-checkbox-group.model.ts @@ -15,10 +15,10 @@ export interface DynamicListCheckboxGroupModelConfig extends DynamicFormGroupMod vocabularyOptions: VocabularyOptions; groupLength?: number; repeatable: boolean; - value?: any; + value?: VocabularyEntry[]; typeBindRelations?: DynamicFormControlRelation[]; - hint: string; required: boolean; + hint?: string; } export class DynamicListCheckboxGroupModel extends DynamicCheckboxGroupModel { @@ -28,9 +28,9 @@ export class DynamicListCheckboxGroupModel extends DynamicCheckboxGroupModel { @serializable() groupLength: number; @serializable() _value: VocabularyEntry[]; @serializable() typeBindRelations: DynamicFormControlRelation[]; - @serializable() toggleSecurityVisibility = false; - @serializable() hint: string; @serializable() required: boolean; + @serializable() hint: string; + @serializable() toggleSecurityVisibility = false; isListGroup = true; valueChanges: Subject; @@ -42,8 +42,8 @@ export class DynamicListCheckboxGroupModel extends DynamicCheckboxGroupModel { this.groupLength = config.groupLength || 5; this._value = []; this.repeatable = config.repeatable; - this.hint = config.hint; this.required = config.required; + this.hint = config.hint; this.valueChanges = new Subject(); this.valueChanges.subscribe((value: VocabularyEntry | VocabularyEntry[]) => this.value = value); @@ -64,9 +64,8 @@ export class DynamicListCheckboxGroupModel extends DynamicCheckboxGroupModel { if (Array.isArray(value)) { this._value = value; } else { - // _value is non extendible so assign it a new array - const newValue = (this.value as VocabularyEntry[]).concat([value]); - this._value = newValue; + // _value is non-extendable so assign it a new array + this._value = (this.value as VocabularyEntry[]).concat([value]); } } } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list-radio-group.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list-radio-group.model.ts index 6f51eed2ac2..0a32498173e 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list-radio-group.model.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list-radio-group.model.ts @@ -6,12 +6,15 @@ import { } from '@ng-dynamic-forms/core'; import { VocabularyOptions } from '../../../../../../core/submission/vocabularies/models/vocabulary-options.model'; import { hasValue } from '../../../../../empty.util'; +import { VocabularyEntry } from '../../../../../../core/submission/vocabularies/models/vocabulary-entry.model'; export interface DynamicListModelConfig extends DynamicRadioGroupModelConfig { vocabularyOptions: VocabularyOptions; groupLength?: number; repeatable: boolean; - value?: any; + value?: VocabularyEntry[]; + required: boolean; + hint?: string; } export class DynamicListRadioGroupModel extends DynamicRadioGroupModel { @@ -19,6 +22,8 @@ export class DynamicListRadioGroupModel extends DynamicRadioGroupModel { @serializable() vocabularyOptions: VocabularyOptions; @serializable() repeatable: boolean; @serializable() groupLength: number; + @serializable() required: boolean; + @serializable() hint: string; isListGroup = true; constructor(config: DynamicListModelConfig, layout?: DynamicFormControlLayout) { @@ -27,6 +32,8 @@ export class DynamicListRadioGroupModel extends DynamicRadioGroupModel { this.vocabularyOptions = config.vocabularyOptions; this.groupLength = config.groupLength || 5; this.repeatable = config.repeatable; + this.required = config.required; + this.hint = config.hint; this.value = config.value; } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.html index b445ab50ae4..5c88af76bcc 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.html @@ -17,7 +17,6 @@ [id]="model.id + item.id" [formControlName]="item.id" [name]="model.name" - [required]="model.required" [value]="item.value" (blur)="onBlur($event)" (change)="onChange($event)" diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.spec.ts index bcf4e233dbf..3fb443192f9 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.spec.ts @@ -1,6 +1,6 @@ // Load the implementations that should be tested import { ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; -import { FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { UntypedFormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { ComponentFixture, inject, TestBed, waitForAsync, } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; @@ -137,9 +137,9 @@ describe('DsDynamicListComponent test suite', () => { listFixture = TestBed.createComponent(DsDynamicListComponent); listComp = listFixture.componentInstance; // FormComponent test instance - listComp.group = new FormGroup({ - listCheckbox: new FormGroup({}), - listRadio: new FormGroup({}) + listComp.group = new UntypedFormGroup({ + listCheckbox: new UntypedFormGroup({}), + listRadio: new UntypedFormGroup({}) }); listComp.model = new DynamicListCheckboxGroupModel(LIST_CHECKBOX_TEST_MODEL_CONFIG, LAYOUT_TEST); listFixture.detectChanges(); @@ -185,9 +185,9 @@ describe('DsDynamicListComponent test suite', () => { listFixture = TestBed.createComponent(DsDynamicListComponent); listComp = listFixture.componentInstance; // FormComponent test instance - listComp.group = new FormGroup({ - listCheckbox: new FormGroup({}), - listRadio: new FormGroup({}) + listComp.group = new UntypedFormGroup({ + listCheckbox: new UntypedFormGroup({}), + listRadio: new UntypedFormGroup({}) }); listComp.model = new DynamicListCheckboxGroupModel(LIST_CHECKBOX_TEST_MODEL_CONFIG, LAYOUT_TEST); modelValue = [Object.assign(new VocabularyEntry(), { authority: 1, display: 'one', value: 1 })]; @@ -225,9 +225,9 @@ describe('DsDynamicListComponent test suite', () => { listFixture = TestBed.createComponent(DsDynamicListComponent); listComp = listFixture.componentInstance; // FormComponent test instance - listComp.group = new FormGroup({ - listCheckbox: new FormGroup({}), - listRadio: new FormGroup({}) + listComp.group = new UntypedFormGroup({ + listCheckbox: new UntypedFormGroup({}), + listRadio: new UntypedFormGroup({}) }); listComp.model = new DynamicListRadioGroupModel(LIST_RADIO_TEST_MODEL_CONFIG, LAYOUT_TEST); listFixture.detectChanges(); @@ -261,9 +261,9 @@ describe('DsDynamicListComponent test suite', () => { listFixture = TestBed.createComponent(DsDynamicListComponent); listComp = listFixture.componentInstance; // FormComponent test instance - listComp.group = new FormGroup({ - listCheckbox: new FormGroup({}), - listRadio: new FormGroup({}) + listComp.group = new UntypedFormGroup({ + listCheckbox: new UntypedFormGroup({}), + listRadio: new UntypedFormGroup({}) }); listComp.model = new DynamicListRadioGroupModel(LIST_RADIO_TEST_MODEL_CONFIG, LAYOUT_TEST); modelValue = Object.assign(new VocabularyEntry(), { authority: 1, display: 'one', value: 1 }); @@ -292,9 +292,9 @@ describe('DsDynamicListComponent test suite', () => { }) class TestComponent { - group: FormGroup = new FormGroup({ - listCheckbox: new FormGroup({}), - listRadio: new FormGroup({}) + group: UntypedFormGroup = new UntypedFormGroup({ + listCheckbox: new UntypedFormGroup({}), + listRadio: new UntypedFormGroup({}) }); model = new DynamicListCheckboxGroupModel(LIST_CHECKBOX_TEST_MODEL_CONFIG, LAYOUT_TEST); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.ts index 7d245b90658..3ac9d0f4792 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.ts @@ -1,5 +1,5 @@ import { ChangeDetectorRef, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; -import { FormGroup } from '@angular/forms'; +import { UntypedFormGroup, ValidatorFn, ValidationErrors, AbstractControl } from '@angular/forms'; import { Subscription } from 'rxjs'; import { filter } from 'rxjs/operators'; @@ -38,7 +38,7 @@ export interface ListItem { }) export class DsDynamicListComponent extends DynamicFormControlComponent implements OnInit, OnDestroy { - @Input() group: FormGroup; + @Input() group: UntypedFormGroup; @Input() model: any; @Output() blur: EventEmitter = new EventEmitter(); @@ -132,7 +132,10 @@ export class DsDynamicListComponent extends DynamicFormControlComponent implemen */ protected setOptionsFromVocabulary() { if (this.model.vocabularyOptions.name && this.model.vocabularyOptions.name.length > 0) { - const listGroup = this.group.controls[this.model.id] as FormGroup; + const listGroup = this.group.controls[this.model.id] as UntypedFormGroup; + if (this.model.repeatable && this.model.required) { + listGroup.addValidators(this.hasAtLeastOneVocabularyEntry()); + } const pageInfo: PageInfo = new PageInfo({ elementsPerPage: 9999, currentPage: 1 } as PageInfo); @@ -144,7 +147,7 @@ export class DsDynamicListComponent extends DynamicFormControlComponent implemen let tempList: ListItem[] = []; this.optionsList = entries.page; // Make a list of available options (checkbox/radio) and split in groups of 'model.groupLength' - entries.page.forEach((option, key) => { + entries.page.forEach((option: VocabularyEntry, key: number) => { const value = option.authority || option.value; let checked: boolean; if (this.model.repeatable) { @@ -156,7 +159,7 @@ export class DsDynamicListComponent extends DynamicFormControlComponent implemen } const item: ListItem = { - id: value, + id: `${this.model.id}_${value}`, label: option.display, value: checked, index: key @@ -184,6 +187,15 @@ export class DsDynamicListComponent extends DynamicFormControlComponent implemen } } + /** + * Checks if at least one {@link VocabularyEntry} has been selected. + */ + hasAtLeastOneVocabularyEntry(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + return control && control.value && Object.values(control.value).find((checked: boolean) => checked === true) ? null : this.model.errorMessages; + }; + } + ngOnDestroy(): void { if (hasValue(this.subscription)) { this.subscription.unsubscribe(); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.spec.ts index 18da6c7d448..aecd022bf1b 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.spec.ts @@ -1,6 +1,6 @@ // Load the implementations that should be tested import { ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA, DebugElement } from '@angular/core'; -import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { UntypedFormControl, UntypedFormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { ComponentFixture, fakeAsync, inject, TestBed, tick, waitForAsync, } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; @@ -78,9 +78,9 @@ let LOOKUP_NAME_TEST_MODEL_CONFIG = { hasSelectableMetadata: false }; -let LOOKUP_TEST_GROUP = new FormGroup({ - lookup: new FormControl(), - lookupName: new FormControl() +let LOOKUP_TEST_GROUP = new UntypedFormGroup({ + lookup: new UntypedFormControl(), + lookupName: new UntypedFormControl() }); const vocabulary = Object.assign(new Vocabulary(), { id: 'vocabulary', @@ -167,9 +167,9 @@ describe('Dynamic Lookup component', () => { hasSelectableMetadata: false }; - LOOKUP_TEST_GROUP = new FormGroup({ - lookup: new FormControl(), - lookupName: new FormControl() + LOOKUP_TEST_GROUP = new UntypedFormGroup({ + lookup: new UntypedFormControl(), + lookupName: new UntypedFormControl() }); } @@ -696,7 +696,7 @@ describe('Dynamic Lookup component', () => { }) class TestComponent { - group: FormGroup = LOOKUP_TEST_GROUP; + group: UntypedFormGroup = LOOKUP_TEST_GROUP; inputLookupModelConfig = LOOKUP_TEST_MODEL_CONFIG; diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.ts index f2898d063b7..673251755f3 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.ts @@ -1,5 +1,5 @@ import { ChangeDetectorRef, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; -import { FormGroup } from '@angular/forms'; +import { UntypedFormGroup } from '@angular/forms'; import { of as observableOf, Subscription } from 'rxjs'; import { catchError, distinctUntilChanged } from 'rxjs/operators'; @@ -33,7 +33,7 @@ import { SubmissionService } from '../../../../../../submission/submission.servi }) export class DsDynamicLookupComponent extends DsDynamicVocabularyComponent implements OnDestroy, OnInit { - @Input() group: FormGroup; + @Input() group: UntypedFormGroup; @Input() model: any; @Output() blur: EventEmitter = new EventEmitter(); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.html index db105328b61..aa5eca1da4f 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.html @@ -76,6 +76,7 @@ [name]="model.name" [placeholder]="model.placeholder" [readonly]="true" + [disabled]="model.readOnly" [type]="model.inputType" [value]="currentValue?.display" [disabled]="model.readOnly" diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.spec.ts index 62bc3403641..0264a3a9780 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.spec.ts @@ -1,7 +1,7 @@ /* eslint-disable max-classes-per-file */ // Load the implementations that should be tested import { ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA, DebugElement } from '@angular/core'; -import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { UntypedFormControl, UntypedFormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { ComponentFixture, fakeAsync, inject, TestBed, tick, } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { CdkTreeModule } from '@angular/cdk/tree'; @@ -55,8 +55,8 @@ export class MockNgbModalRef { } function init() { - ONEBOX_TEST_GROUP = new FormGroup({ - onebox: new FormControl(), + ONEBOX_TEST_GROUP = new UntypedFormGroup({ + onebox: new UntypedFormControl(), }); ONEBOX_TEST_MODEL_CONFIG = { @@ -527,7 +527,7 @@ describe('DsDynamicOneboxComponent test suite', () => { }) class TestComponent { - group: FormGroup = ONEBOX_TEST_GROUP; + group: UntypedFormGroup = ONEBOX_TEST_GROUP; model = new DynamicOneboxModel(ONEBOX_TEST_MODEL_CONFIG); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.ts index f23ea98ed3e..1e7e0e14290 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.ts @@ -1,5 +1,5 @@ import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'; -import { FormGroup } from '@angular/forms'; +import { UntypedFormGroup } from '@angular/forms'; import { DynamicFormControlCustomEvent, @@ -31,7 +31,9 @@ import { VocabularyEntry } from '../../../../../../core/submission/vocabularies/ import { PageInfo } from '../../../../../../core/shared/page-info.model'; import { DsDynamicVocabularyComponent } from '../dynamic-vocabulary.component'; import { Vocabulary } from '../../../../../../core/submission/vocabularies/models/vocabulary.model'; -import { VocabularyTreeviewComponent } from '../../../../vocabulary-treeview/vocabulary-treeview.component'; +import { + VocabularyTreeviewModalComponent +} from '../../../../vocabulary-treeview-modal/vocabulary-treeview-modal.component'; import { FormBuilderService } from '../../../form-builder.service'; import { SubmissionService } from '../../../../../../submission/submission.service'; @@ -46,7 +48,7 @@ import { SubmissionService } from '../../../../../../submission/submission.servi }) export class DsDynamicOneboxComponent extends DsDynamicVocabularyComponent implements OnInit { - @Input() group: FormGroup; + @Input() group: UntypedFormGroup; @Input() model: DynamicOneboxModel; @Output() blur: EventEmitter = new EventEmitter(); @@ -237,13 +239,13 @@ export class DsDynamicOneboxComponent extends DsDynamicVocabularyComponent imple map((vocabulary: Vocabulary) => vocabulary.preloadLevel), take(1) ).subscribe((preloadLevel) => { - const modalRef: NgbModalRef = this.modalService.open(VocabularyTreeviewComponent, { + const modalRef: NgbModalRef = this.modalService.open(VocabularyTreeviewModalComponent, { size: 'lg', windowClass: 'treeview' }); modalRef.componentInstance.vocabularyOptions = this.model.vocabularyOptions; modalRef.componentInstance.preloadLevel = preloadLevel; - modalRef.componentInstance.selectedItem = this.currentValue ? this.currentValue : ''; + modalRef.componentInstance.selectedItems = this.currentValue ? [this.currentValue] : []; modalRef.result.then((result: FormFieldMetadataValueObject) => { if (result) { this.currentValue = result; diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.component.spec.ts index eacdafae370..e99c3cc8f0a 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.component.spec.ts @@ -1,7 +1,7 @@ // Load the implementations that should be tested import { ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA, DebugElement } from '@angular/core'; import { ComponentFixture, inject, TestBed, waitForAsync, } from '@angular/core/testing'; -import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { UntypedFormControl, UntypedFormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { Store, StoreModule } from '@ngrx/store'; @@ -130,8 +130,8 @@ function init() { hasSelectableMetadata: false } as DynamicRelationGroupModelConfig; - FORM_GROUP_TEST_GROUP = new FormGroup({ - dc_contributor_author: new FormControl(), + FORM_GROUP_TEST_GROUP = new UntypedFormGroup({ + dc_contributor_author: new UntypedFormControl(), }); } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.components.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.components.ts index 5a2941dff0d..7d02412082a 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.components.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.components.ts @@ -1,5 +1,5 @@ import { ChangeDetectorRef, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; -import { FormGroup } from '@angular/forms'; +import { UntypedFormGroup } from '@angular/forms'; import { combineLatest, Observable, of as observableOf, Subscription } from 'rxjs'; import { filter, map, mergeMap, scan, take } from 'rxjs/operators'; @@ -41,7 +41,8 @@ import { Metadata } from '../../../../../../core/shared/metadata.utils'; }) export class DsDynamicRelationGroupComponent extends DynamicFormControlComponent implements OnDestroy, OnInit { - @Input() group: FormGroup; + @Input() formId: string; + @Input() group: UntypedFormGroup; @Input() model: DynamicRelationGroupModel; @Output() blur: EventEmitter = new EventEmitter(); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/modal/dynamic-relation-group-modal.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/modal/dynamic-relation-group-modal.component.spec.ts index 414cca05813..3ffff15129b 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/modal/dynamic-relation-group-modal.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/modal/dynamic-relation-group-modal.component.spec.ts @@ -1,7 +1,12 @@ // Load the implementations that should be tested import { ChangeDetectorRef, CUSTOM_ELEMENTS_SCHEMA, DebugElement } from '@angular/core'; import { ComponentFixture, inject, TestBed, waitForAsync, } from '@angular/core/testing'; -import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { + FormsModule, + ReactiveFormsModule, + UntypedFormControl, + UntypedFormGroup +} from '@angular/forms'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { Store, StoreModule } from '@ngrx/store'; @@ -30,7 +35,9 @@ import { SubmissionService } from '../../../../../../../submission/submission.se import { SubmissionServiceStub } from '../../../../../../testing/submission-service.stub'; import { SubmissionScopeType } from '../../../../../../../core/submission/submission-scope-type'; import { Vocabulary } from '../../../../../../../core/submission/vocabularies/models/vocabulary.model'; -import { VocabularyEntryDetail } from '../../../../../../../core/submission/vocabularies/models/vocabulary-entry-detail.model'; +import { + VocabularyEntryDetail +} from '../../../../../../../core/submission/vocabularies/models/vocabulary-entry-detail.model'; import { DsDynamicRelationGroupModalComponent } from './dynamic-relation-group-modal.components'; export let FORM_GROUP_TEST_MODEL_CONFIG; @@ -133,8 +140,8 @@ function init() { hasSelectableMetadata: false } as DynamicRelationGroupModelConfig; - FORM_GROUP_TEST_GROUP = new FormGroup({ - dc_contributor_author: new FormControl(), + FORM_GROUP_TEST_GROUP = new UntypedFormGroup({ + dc_contributor_author: new UntypedFormControl(), }); } @@ -147,9 +154,9 @@ describe('DsDynamicRelationGroupModelComponent test suite', () => { let groupFixture: ComponentFixture; let debugElement: DebugElement; let modelValue: any; - let control1: FormControl; + let control1: UntypedFormControl; let model1: DsDynamicInputModel; - let control2: FormControl; + let control2: UntypedFormControl; let model2: DsDynamicInputModel; const modal = jasmine.createSpyObj('modal', ['close', 'dismiss']); @@ -229,9 +236,9 @@ describe('DsDynamicRelationGroupModelComponent test suite', () => { testComponent.group = FORM_GROUP_TEST_GROUP; testComponent.model = new DynamicRelationGroupModel(FORM_GROUP_TEST_MODEL_CONFIG); componentFixture.detectChanges(); - control1 = service.getFormControlById('dc_contributor_author', (testComponent as any).formRef.formGroup, testComponent.formModel) as FormControl; + control1 = service.getFormControlById('dc_contributor_author', (testComponent as any).formRef.formGroup, testComponent.formModel) as UntypedFormControl; model1 = service.findById('dc_contributor_author', testComponent.formModel) as DsDynamicInputModel; - control2 = service.getFormControlById('local_contributor_affiliation', (testComponent as any).formRef.formGroup, testComponent.formModel) as FormControl; + control2 = service.getFormControlById('local_contributor_affiliation', (testComponent as any).formRef.formGroup, testComponent.formModel) as UntypedFormControl; model2 = service.findById('local_contributor_affiliation', testComponent.formModel) as DsDynamicInputModel; // spyOn(store, 'dispatch'); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/modal/dynamic-relation-group-modal.components.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/modal/dynamic-relation-group-modal.components.ts index 3fbacece512..4ffb8ec03a6 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/modal/dynamic-relation-group-modal.components.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/modal/dynamic-relation-group-modal.components.ts @@ -65,29 +65,30 @@ export class DsDynamicRelationGroupModalComponent extends DynamicFormControlComp @Output() edit: EventEmitter = new EventEmitter(); @Output() add: EventEmitter = new EventEmitter(); - @ViewChild('formRef', {static: false}) private formRef: FormComponent; + @ViewChild('formRef', { static: false }) private formRef: FormComponent; public formModel: DynamicFormControlModel[]; public vocabulary$: Observable; + public securityLevelParent: number; private subs: Subscription[] = []; constructor(private vocabularyService: VocabularyService, - private formBuilderService: FormBuilderService, - private formService: FormService, - private cdr: ChangeDetectorRef, - protected layoutService: DynamicFormLayoutService, - protected validationService: DynamicFormValidationService, - protected modalService: NgbModal, - protected submissionService: SubmissionService, - private activeModal: NgbActiveModal + private formBuilderService: FormBuilderService, + private formService: FormService, + private cdr: ChangeDetectorRef, + protected layoutService: DynamicFormLayoutService, + protected validationService: DynamicFormValidationService, + protected modalService: NgbModal, + protected submissionService: SubmissionService, + private activeModal: NgbActiveModal ) { super(layoutService, validationService); } ngOnInit() { - const config = {rows: this.model.formConfiguration} as SubmissionFormsModel; + const config = { rows: this.model.formConfiguration } as SubmissionFormsModel; this.formId = this.formService.getUniqueId(this.model.id); this.formModel = this.formBuilderService.modelFromConfiguration( this.model.submissionId, @@ -113,10 +114,8 @@ export class DsDynamicRelationGroupModalComponent extends DynamicFormControlComp if (isNotEmpty(nextValue)) { model.value = nextValue; } - // as the value doesn't support the security level, add into the big model - if (value && typeof value !== 'string') { - (model as any).securityLevel = value.securityLevel; - } + + this.initSecurityLevelConfig(model, modelRow); }); }); } @@ -262,6 +261,7 @@ export class DsDynamicRelationGroupModalComponent extends DynamicFormControlComp modelRow.group.forEach((model: DynamicInputModel) => { if (model.name === this.model.mandatoryField) { mandatoryFieldModel = model; + this.initSecurityLevelConfig(model, modelRow); return; } }); @@ -291,14 +291,25 @@ export class DsDynamicRelationGroupModalComponent extends DynamicFormControlComp private buildChipItem() { const item = Object.create({}); + let mainModel; + this.formModel.some((modelRow: DynamicFormGroupModel) => { + const findIndex = modelRow.group.findIndex(model => model.name === this.model.name); + if (findIndex !== -1) { + mainModel = modelRow.group[findIndex]; + return true; + } + }); this.formModel.forEach((row) => { const modelRow = row as DynamicFormGroupModel; modelRow.group.forEach((control: DynamicInputModel) => { const controlValue: any = (control?.value as any)?.value || control?.value || PLACEHOLDER_PARENT_METADATA; const controlAuthority: any = (control?.value as any)?.authority || null; + item[control.name] = new FormFieldMetadataValueObject( - controlValue, (control as any)?.language, (control as any)?.securityLevel, controlAuthority, + controlValue, (control as any)?.language, + controlValue === PLACEHOLDER_PARENT_METADATA ? null : mainModel.securityLevel, + controlAuthority, null, 0, null, (control?.value as any)?.otherInformation || null ); @@ -307,6 +318,33 @@ export class DsDynamicRelationGroupModalComponent extends DynamicFormControlComp return item; } + private initSecurityLevelConfig(chipModel: DynamicInputModel, modelGroup: DynamicFormGroupModel) { + if (this.model.name === chipModel.name && this.model.securityConfigLevel.length > 1) { + (chipModel as any).securityConfigLevel = this.model.securityConfigLevel; + (chipModel as any).toggleSecurityVisibility = true; + + const mainRow = modelGroup.group.find(itemModel => itemModel.name === this.model.name); + + (chipModel as any).securityLevel = (mainRow as any).securityLevel || 0; + this.securityLevelParent = (mainRow as any).securityLevel; + + modelGroup.group.forEach((item: any) => { + if (item.name !== this.model.name) { + item.securityConfigLevel = this.model.securityConfigLevel; + item.toggleSecurityVisibility = false; + item.securityLevel = this.securityLevelParent; + } + }); + } + if (this.model.securityConfigLevel.length === 1) { + modelGroup.group.forEach((item: any) => { + item.securityConfigLevel = this.model.securityConfigLevel; + item.toggleSecurityVisibility = false; + item.securityLevel = this.model.securityLevel; + }); + } + } + private retrieveVocabulary(vocabularyOptions: VocabularyOptions): void { this.vocabulary$ = this.vocabularyService.findVocabularyById(vocabularyOptions.name).pipe( getFirstSucceededRemoteDataPayload(), diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-inline-group/dynamic-relation-inline-group.components.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-inline-group/dynamic-relation-inline-group.components.ts index 8882d3aca28..cc59eade714 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-inline-group/dynamic-relation-inline-group.components.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-inline-group/dynamic-relation-inline-group.components.ts @@ -10,7 +10,8 @@ import { DynamicFormControlModel, DynamicFormGroupModel, DynamicFormLayoutService, - DynamicFormValidationService + DynamicFormValidationService, + DynamicInputModel } from '@ng-dynamic-forms/core'; import { DynamicRelationGroupModel } from '../relation-group/dynamic-relation-group.model'; @@ -49,14 +50,14 @@ export class DsDynamicRelationInlineGroupComponent extends DynamicFormControlCom public formGroup: FormGroup; public formModel: DynamicFormControlModel[]; - @ViewChild('formRef', {static: false}) private formRef: FormComponent; + @ViewChild('formRef', { static: false }) private formRef: FormComponent; protected metadataSecurityConfiguration: MetadataSecurityConfiguration; constructor(private formBuilderService: FormBuilderService, - private formService: FormService, - protected layoutService: DynamicFormLayoutService, - protected submissionService: SubmissionService, - protected validationService: DynamicFormValidationService + private formService: FormService, + protected layoutService: DynamicFormLayoutService, + protected submissionService: SubmissionService, + protected validationService: DynamicFormValidationService ) { super(layoutService, validationService); } @@ -64,9 +65,9 @@ export class DsDynamicRelationInlineGroupComponent extends DynamicFormControlCom ngOnInit() { this.submissionService.getSubmissionSecurityConfiguration(this.model.submissionId).pipe( take(1)).subscribe(security => { - this.metadataSecurityConfiguration = security; - }); - const config = {rows: this.model.formConfiguration} as SubmissionFormsModel; + this.metadataSecurityConfiguration = security; + }); + const config = { rows: this.model.formConfiguration } as SubmissionFormsModel; this.formId = this.formService.getUniqueId(this.model.id); this.formModel = this.initArrayModel(config); @@ -76,7 +77,6 @@ export class DsDynamicRelationInlineGroupComponent extends DynamicFormControlCom initArrayModel(formConfig): DynamicRowArrayModel[] { let arrayCounter = 0; - const config = { id: this.model.id + '_array', initialCount: isNotEmpty(this.model.value) ? (this.model.value as any[]).length : 1, @@ -114,8 +114,12 @@ export class DsDynamicRelationInlineGroupComponent extends DynamicFormControlCom this.model.readOnly, this.formBuilderService.getTypeBindModel(), true, - this.metadataSecurityConfiguration); - return formModel[0]; + this.metadataSecurityConfiguration)[0]; + + (formModel as any).group?.forEach((modelItem: DynamicInputModel) => { + this.initSecurityLevelConfig(modelItem, (formModel as any)); + }); + return formModel; } onBlur(event: DynamicFormControlEvent) { @@ -124,7 +128,11 @@ export class DsDynamicRelationInlineGroupComponent extends DynamicFormControlCom onChange(event: DynamicFormControlEvent) { const index = event.model.parent.parent.index; - const groupValue = this.getRowValue(event.model.parent as DynamicFormGroupModel); + let parentSecurityLevel; + if (event.type === 'change') { + parentSecurityLevel = this.model.securityLevel; + } + const groupValue = this.getRowValue(event.model.parent as DynamicFormGroupModel, parentSecurityLevel); if (this.hasEmptyGroupValue(groupValue)) { this.removeItemFromArray(event); @@ -155,7 +163,59 @@ export class DsDynamicRelationInlineGroupComponent extends DynamicFormControlCom } } - private getRowValue(formGroup: DynamicFormGroupModel) { + private findModelGroups() { + this.formModel.forEach((row: any) => { + row.groups.forEach((groupArray) => { + groupArray.group.forEach((groupRow) => { + const modelRow = groupRow as DynamicFormGroupModel; + modelRow.group.forEach((model: DynamicInputModel) => { + this.initSecurityLevelConfig(model, modelRow); + }); + }); + }); + }); + } + + private initSecurityLevelConfig(model: DynamicInputModel | any, modelGroup: DynamicFormGroupModel) { + if (this.model.name === model.name && this.model.securityConfigLevel?.length > 1) { + model.securityConfigLevel = this.model.securityConfigLevel; + model.toggleSecurityVisibility = true; + + let mainSecurityLevel; + const mainRow = modelGroup.group.find(itemModel => itemModel.name === this.model.name); + if (isNotEmpty(this.model.securityLevel)) { + mainSecurityLevel = this.model.securityLevel; + } else { + mainSecurityLevel = (mainRow as any).securityLevel; + } + + model.securityLevel = mainSecurityLevel; + + modelGroup.group.forEach((item: any) => { + if (item.name !== this.model.name) { + item.securityConfigLevel = this.model.securityConfigLevel; + item.toggleSecurityVisibility = false; + item.securityLevel = mainSecurityLevel; + } + }); + } + if (this.model.securityConfigLevel?.length === 1) { + modelGroup.group.forEach((item: any) => { + item.securityConfigLevel = this.model.securityConfigLevel; + item.toggleSecurityVisibility = false; + item.securityLevel = this.model.securityLevel; + }); + } + } + + private getRowValue(formGroup: DynamicFormGroupModel, securityLevel?: number) { + let mainSecurityLevel; + if (isNotEmpty(securityLevel)) { + mainSecurityLevel = securityLevel; + } else { + const mainRow = formGroup.group.find(itemModel => itemModel.name === this.model.name); + mainSecurityLevel = (mainRow as any).securityLevel; + } const groupValue = Object.create({}); formGroup.group.forEach((model: any) => { if (model.name !== this.model.mandatoryField) { @@ -163,16 +223,16 @@ export class DsDynamicRelationInlineGroupComponent extends DynamicFormControlCom groupValue[model.name] = PLACEHOLDER_PARENT_METADATA; } else { if (typeof model.value === 'string') { - groupValue[model.name] = new FormFieldMetadataValueObject(model.value, null, model.securityLevel); + groupValue[model.name] = new FormFieldMetadataValueObject(model.value, null, mainSecurityLevel); } else { - groupValue[model.name] = model.value; + groupValue[model.name] = Object.assign(new FormFieldMetadataValueObject(), model.value, { securityLevel: mainSecurityLevel || null }); } } } else { if (typeof model.value === 'string') { - groupValue[model.name] = new FormFieldMetadataValueObject(model.value, null, model.securityLevel); + groupValue[model.name] = new FormFieldMetadataValueObject(model.value, null, mainSecurityLevel); } else { - groupValue[model.name] = model.value; + groupValue[model.name] = Object.assign(new FormFieldMetadataValueObject(), model.value, { securityLevel: mainSecurityLevel || null }); } } }); @@ -192,7 +252,7 @@ export class DsDynamicRelationInlineGroupComponent extends DynamicFormControlCom return normValue; } - private hasPlaceholder(value: string|FormFieldMetadataValueObject): boolean { + private hasPlaceholder(value: string | FormFieldMetadataValueObject): boolean { return (value instanceof FormFieldMetadataValueObject) ? value.hasPlaceholder() : (isNotEmpty(value) && value === PLACEHOLDER_PARENT_METADATA); } @@ -216,15 +276,29 @@ export class DsDynamicRelationInlineGroupComponent extends DynamicFormControlCom } private updateArrayModelValue(groupValue, index) { - let modelValue = this.model.value; + let parentSecurityLevel = this.model.securityLevel || this.model.securityConfigLevel?.length > 0 ? this.model.securityConfigLevel[0] : null; + for (const name of Object.keys(groupValue)) { + if (name === this.model.name && isNotEmpty(groupValue[name].securityLevel)) { + parentSecurityLevel = groupValue[name].securityLevel; + break; + } + } + if (isNotEmpty(parentSecurityLevel)) { + Object.keys(groupValue).forEach(model => { + if (groupValue[model] instanceof Object) { + groupValue[model].securityLevel = parentSecurityLevel; + } + }); + this.model.securityLevel = parentSecurityLevel; + } + let modelValue = this.model.value; if (isEmpty(modelValue)) { modelValue = [groupValue]; } else { modelValue[index] = groupValue; } this.model.value = modelValue; - this.change.emit(); } onCustomEvent(event) { @@ -246,7 +320,7 @@ export class DsDynamicRelationInlineGroupComponent extends DynamicFormControlCom if (arrayOfValue[index] === undefined || arrayOfValue[previousIndex] === undefined) { return; - } else if ( arrayOfValue.length > 0 ) { + } else if (arrayOfValue.length > 0) { arrayOfValue = arrayOfValue.filter((el) => el !== undefined); } else { return; @@ -260,9 +334,11 @@ export class DsDynamicRelationInlineGroupComponent extends DynamicFormControlCom } private copyArrayItem(event) { - const index = event.model.parent.index; - const groupValue = this.getRowValue(event.model as DynamicFormGroupModel); + const index = Array.isArray(this.model.value) ? this.model.value.length : event.model.parent.index; + const mainRow = event.model.group.find(itemModel => itemModel.name === this.model.name); + const groupValue = this.getRowValue(event.model as DynamicFormGroupModel, mainRow.securityLevel); this.updateArrayModelValue(groupValue, index); + this.findModelGroups(); this.change.emit(); } } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.html index 9d75445da16..186f5fdb95d 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.html @@ -6,7 +6,7 @@ + aria-hidden="true"> + (keydown)="selectOnKeyDown($event, sdRef)">
    - - +

    {{ 'submission.sections.describe.relationship-lookup.selection-tab.title.' + externalSource.id | translate}}

    diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.ts index 22fcc4e8bb6..08ff5378ab8 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.ts @@ -75,6 +75,12 @@ export class DsDynamicLookupRelationExternalSourceTabComponent implements OnInit * The context to displaying lists for */ @Input() context: Context; + + /** + * The search query + */ + @Input() query: string; + @Input() repeatable: boolean; /** * Emit an event when an object has been imported (or selected from similar local entries) @@ -124,12 +130,13 @@ export class DsDynamicLookupRelationExternalSourceTabComponent implements OnInit */ relatedEntityType: ItemType; - constructor(private router: Router, - public searchConfigService: SearchConfigurationService, - private externalSourceService: ExternalSourceDataService, - private modalService: NgbModal, - private selectableListService: SelectableListService, - private paginationService: PaginationService + constructor( + protected router: Router, + public searchConfigService: SearchConfigurationService, + protected externalSourceService: ExternalSourceDataService, + protected modalService: NgbModal, + protected selectableListService: SelectableListService, + protected paginationService: PaginationService, ) { } @@ -148,8 +155,12 @@ export class DsDynamicLookupRelationExternalSourceTabComponent implements OnInit this.resetRoute(); this.entriesRD$ = this.searchConfigService.paginatedSearchOptions.pipe( - switchMap((searchOptions: PaginatedSearchOptions) => - this.externalSourceService.getExternalSourceEntries(this.externalSource.id, searchOptions).pipe(startWith(undefined))) + switchMap((searchOptions: PaginatedSearchOptions) => { + if (searchOptions.query === '') { + searchOptions.query = this.query; + } + return this.externalSourceService.getExternalSourceEntries(this.externalSource.id, searchOptions).pipe(startWith(undefined)); + }) ); this.currentPagination$ = this.paginationService.getCurrentPagination(this.searchConfigService.paginationID, this.initialPagination); this.importConfig = { diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/themed-dynamic-lookup-relation-external-source-tab.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/themed-dynamic-lookup-relation-external-source-tab.component.ts new file mode 100644 index 00000000000..113d902c3d8 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/themed-dynamic-lookup-relation-external-source-tab.component.ts @@ -0,0 +1,51 @@ +import { ThemedComponent } from '../../../../../theme-support/themed.component'; +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { RelationshipOptions } from '../../../models/relationship-options.model'; +import { ListableObject } from '../../../../../object-collection/shared/listable-object.model'; +import { Context } from '../../../../../../core/shared/context.model'; +import { Item } from '../../../../../../core/shared/item.model'; +import { Collection } from '../../../../../../core/shared/collection.model'; +import { ExternalSource } from '../../../../../../core/shared/external-source.model'; +import { DsDynamicLookupRelationExternalSourceTabComponent } from './dynamic-lookup-relation-external-source-tab.component'; + +@Component({ + selector: 'ds-themed-dynamic-lookup-relation-external-source-tab', + styleUrls: [], + templateUrl: '../../../../../theme-support/themed.component.html', +}) +export class ThemedDynamicLookupRelationExternalSourceTabComponent extends ThemedComponent { + protected inAndOutputNames: (keyof DsDynamicLookupRelationExternalSourceTabComponent & keyof this)[] = ['label', 'listId', + 'item', 'collection', 'relationship', 'context', 'query', 'repeatable', 'importedObject', 'externalSource']; + + @Input() label: string; + + @Input() listId: string; + + @Input() item: Item; + + @Input() collection: Collection; + + @Input() relationship: RelationshipOptions; + + @Input() context: Context; + + @Input() query: string; + + @Input() repeatable: boolean; + + @Output() importedObject: EventEmitter = new EventEmitter(); + + @Input() externalSource: ExternalSource; + + protected getComponentName(): string { + return 'DsDynamicLookupRelationExternalSourceTabComponent'; + } + + protected importThemedComponent(themeName: string): Promise { + return import(`../../../../../../../themes/${themeName}/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component`); + } + + protected importUnthemedComponent(): Promise { + return import(`./dynamic-lookup-relation-external-source-tab.component`); + } +} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.html index 376900609e7..17aafae5eb9 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.html @@ -9,6 +9,7 @@ [selectionConfig]="{ repeatable: repeatable, listId: listId }" [showScopeSelector]="false" [showViewModes]="false" + [query]="query" (resultFound)="onResultFound($event)" (deselectObject)="deselectObject.emit($event)" (selectObject)="selectObject.emit($event)"> diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.ts index cd4a8f7690b..9452918a978 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.ts @@ -147,12 +147,12 @@ export class DsDynamicLookupRelationSearchTabComponent implements OnInit, OnDest @Output() resultFound: EventEmitter> = new EventEmitter>(); constructor( - private searchService: SearchService, - private selectableListService: SelectableListService, + protected searchService: SearchService, + protected selectableListService: SelectableListService, public searchConfigService: SearchConfigurationService, public lookupRelationService: LookupRelationService, - private relationshipService: RelationshipDataService, - private paginationService: PaginationService + protected relationshipService: RelationshipDataService, + protected paginationService: PaginationService, ) { } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/themed-dynamic-lookup-relation-search-tab.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/themed-dynamic-lookup-relation-search-tab.component.ts new file mode 100644 index 00000000000..d44f8f84a02 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/themed-dynamic-lookup-relation-search-tab.component.ts @@ -0,0 +1,63 @@ +import { ThemedComponent } from '../../../../../theme-support/themed.component'; +import { DsDynamicLookupRelationSearchTabComponent } from './dynamic-lookup-relation-search-tab.component'; +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { RelationshipOptions } from '../../../models/relationship-options.model'; +import { Observable } from 'rxjs'; +import { ListableObject } from '../../../../../object-collection/shared/listable-object.model'; +import { Context } from '../../../../../../core/shared/context.model'; +import { RelationshipType } from '../../../../../../core/shared/item-relationships/relationship-type.model'; +import { Item } from '../../../../../../core/shared/item.model'; +import { SearchResult } from '../../../../../search/models/search-result.model'; +import { SearchObjects } from '../../../../../search/models/search-objects.model'; +import { DSpaceObject } from '../../../../../../core/shared/dspace-object.model'; + +@Component({ + selector: 'ds-themed-dynamic-lookup-relation-search-tab', + styleUrls: [], + templateUrl: '../../../../../theme-support/themed.component.html', +}) +export class ThemedDynamicLookupRelationSearchTabComponent extends ThemedComponent { + protected inAndOutputNames: (keyof DsDynamicLookupRelationSearchTabComponent & keyof this)[] = ['relationship', 'listId', + 'query', 'repeatable', 'selection$', 'context', 'relationshipType', 'item', 'isLeft', 'toRemove', 'isEditRelationship', + 'deselectObject', 'selectObject', 'resultFound']; + + @Input() relationship: RelationshipOptions; + + @Input() listId: string; + + @Input() query: string; + + @Input() repeatable: boolean; + + @Input() selection$: Observable; + + @Input() context: Context; + + @Input() relationshipType: RelationshipType; + + @Input() item: Item; + + @Input() isLeft: boolean; + + @Input() toRemove: SearchResult[]; + + @Input() isEditRelationship: boolean; + + @Output() deselectObject: EventEmitter = new EventEmitter(); + + @Output() selectObject: EventEmitter = new EventEmitter(); + + @Output() resultFound: EventEmitter> = new EventEmitter(); + + protected getComponentName(): string { + return 'DsDynamicLookupRelationSearchTabComponent'; + } + + protected importThemedComponent(themeName: string): Promise { + return import(`../../../../../../../themes/${themeName}/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component`); + } + + protected importUnthemedComponent(): Promise { + return import(`./dynamic-lookup-relation-search-tab.component`); + } +} diff --git a/src/app/shared/form/builder/form-builder.service.spec.ts b/src/app/shared/form/builder/form-builder.service.spec.ts index 83513a6e68c..2192b588cf2 100644 --- a/src/app/shared/form/builder/form-builder.service.spec.ts +++ b/src/app/shared/form/builder/form-builder.service.spec.ts @@ -1,8 +1,8 @@ import { inject, TestBed } from '@angular/core/testing'; import { - FormArray, - FormControl, - FormGroup, + UntypedFormArray, + UntypedFormControl, + UntypedFormGroup, NG_ASYNC_VALIDATORS, NG_VALIDATORS, ReactiveFormsModule @@ -240,11 +240,17 @@ describe('FormBuilderService test suite', () => { id: 'testCheckboxList', vocabularyOptions: vocabularyOptions, repeatable: true, - hint: 'test hint', required: false, + hint: 'test hint', }), - new DynamicListRadioGroupModel({ id: 'testRadioList', vocabularyOptions: vocabularyOptions, repeatable: false }), + new DynamicListRadioGroupModel({ + id: 'testRadioList', + vocabularyOptions: vocabularyOptions, + repeatable: false, + required: false, + hint: 'test hint', + }), new DynamicRelationGroupModel({ submissionId, @@ -291,7 +297,7 @@ describe('FormBuilderService test suite', () => { hasSelectableMetadata: true }), - new DynamicDsDatePickerModel({ id: 'testDate' }), + new DynamicDsDatePickerModel({ id: 'testDate', repeatable: false}), new DynamicLookupModel({ id: 'testLookup', @@ -687,21 +693,21 @@ describe('FormBuilderService test suite', () => { const formGroup = service.createFormGroup(testModel); - expect(formGroup instanceof FormGroup).toBe(true); + expect(formGroup instanceof UntypedFormGroup).toBe(true); - expect(formGroup.get('testCheckbox') instanceof FormControl).toBe(true); - expect(formGroup.get('testCheckboxGroup') instanceof FormGroup).toBe(true); - expect(formGroup.get('testDatepicker') instanceof FormControl).toBe(true); - expect(formGroup.get('testFormArray') instanceof FormArray).toBe(true); - expect(formGroup.get('testInput') instanceof FormControl).toBe(true); - expect(formGroup.get('testRadioGroup') instanceof FormControl).toBe(true); - expect(formGroup.get('testSelect') instanceof FormControl).toBe(true); - expect(formGroup.get('testTextArea') instanceof FormControl).toBe(true); - expect(formGroup.get('testFileUpload') instanceof FormControl).toBe(true); - expect(formGroup.get('testEditor') instanceof FormControl).toBe(true); - expect(formGroup.get('testTimePicker') instanceof FormControl).toBe(true); - expect(formGroup.get('testRating') instanceof FormControl).toBe(true); - expect(formGroup.get('testColorPicker') instanceof FormControl).toBe(true); + expect(formGroup.get('testCheckbox') instanceof UntypedFormControl).toBe(true); + expect(formGroup.get('testCheckboxGroup') instanceof UntypedFormGroup).toBe(true); + expect(formGroup.get('testDatepicker') instanceof UntypedFormControl).toBe(true); + expect(formGroup.get('testFormArray') instanceof UntypedFormArray).toBe(true); + expect(formGroup.get('testInput') instanceof UntypedFormControl).toBe(true); + expect(formGroup.get('testRadioGroup') instanceof UntypedFormControl).toBe(true); + expect(formGroup.get('testSelect') instanceof UntypedFormControl).toBe(true); + expect(formGroup.get('testTextArea') instanceof UntypedFormControl).toBe(true); + expect(formGroup.get('testFileUpload') instanceof UntypedFormControl).toBe(true); + expect(formGroup.get('testEditor') instanceof UntypedFormControl).toBe(true); + expect(formGroup.get('testTimePicker') instanceof UntypedFormControl).toBe(true); + expect(formGroup.get('testRating') instanceof UntypedFormControl).toBe(true); + expect(formGroup.get('testColorPicker') instanceof UntypedFormControl).toBe(true); }); it('should throw when unknown DynamicFormControlModel id is specified in JSON', () => { @@ -724,7 +730,7 @@ describe('FormBuilderService test suite', () => { it('should add a form control to an existing form group', () => { const formGroup = service.createFormGroup(testModel); - const nestedFormGroup = formGroup.controls.testFormGroup as FormGroup; + const nestedFormGroup = formGroup.controls.testFormGroup as UntypedFormGroup; const nestedFormGroupModel = testModel[7] as DynamicFormGroupModel; const newModel1 = new DynamicInputModel({ id: 'newInput1' }); const newModel2 = new DynamicInputModel({ id: 'newInput2' }); @@ -735,14 +741,14 @@ describe('FormBuilderService test suite', () => { expect(formGroup.controls[newModel1.id]).toBeTruthy(); expect(testModel[testModel.length - 1] === newModel1).toBe(true); - expect((formGroup.controls.testFormGroup as FormGroup).controls[newModel2.id]).toBeTruthy(); + expect((formGroup.controls.testFormGroup as UntypedFormGroup).controls[newModel2.id]).toBeTruthy(); expect(nestedFormGroupModel.get(nestedFormGroupModel.group.length - 1) === newModel2).toBe(true); }); it('should insert a form control to an existing form group', () => { const formGroup = service.createFormGroup(testModel); - const nestedFormGroup = formGroup.controls.testFormGroup as FormGroup; + const nestedFormGroup = formGroup.controls.testFormGroup as UntypedFormGroup; const nestedFormGroupModel = testModel[7] as DynamicFormGroupModel; const newModel1 = new DynamicInputModel({ id: 'newInput1' }); const newModel2 = new DynamicInputModel({ id: 'newInput2' }); @@ -754,7 +760,7 @@ describe('FormBuilderService test suite', () => { expect(testModel[4] === newModel1).toBe(true); expect(service.getPath(testModel[4])).toEqual(['newInput1']); - expect((formGroup.controls.testFormGroup as FormGroup).controls[newModel2.id]).toBeTruthy(); + expect((formGroup.controls.testFormGroup as UntypedFormGroup).controls[newModel2.id]).toBeTruthy(); expect(nestedFormGroupModel.get(0) === newModel2).toBe(true); expect(service.getPath(nestedFormGroupModel.get(0))).toEqual(['testFormGroup', 'newInput2']); }); @@ -773,14 +779,14 @@ describe('FormBuilderService test suite', () => { service.moveFormGroupControl(0, 1, nestedFormGroupModel); - expect((formGroup.controls.testFormGroup as FormGroup).controls[model2.id]).toBeTruthy(); + expect((formGroup.controls.testFormGroup as UntypedFormGroup).controls[model2.id]).toBeTruthy(); expect(nestedFormGroupModel.get(1) === model2).toBe(true); }); it('should remove a form control from an existing form group', () => { const formGroup = service.createFormGroup(testModel); - const nestedFormGroup = formGroup.controls.testFormGroup as FormGroup; + const nestedFormGroup = formGroup.controls.testFormGroup as UntypedFormGroup; const nestedFormGroupModel = testModel[7] as DynamicFormGroupModel; const length = testModel.length; const size = nestedFormGroupModel.size(); @@ -814,7 +820,7 @@ describe('FormBuilderService test suite', () => { formArray = service.createFormArray(model); - expect(formArray instanceof FormArray).toBe(true); + expect(formArray instanceof UntypedFormArray).toBe(true); expect(formArray.length).toBe(model.initialCount); }); @@ -855,8 +861,8 @@ describe('FormBuilderService test suite', () => { const index = 3; const step = 1; - (formArray.at(index) as FormGroup).controls.testFormArrayGroupInput.setValue('next test value 1'); - (formArray.at(index + step) as FormGroup).controls.testFormArrayGroupInput.setValue('next test value 2'); + (formArray.at(index) as UntypedFormGroup).controls.testFormArrayGroupInput.setValue('next test value 1'); + (formArray.at(index + step) as UntypedFormGroup).controls.testFormArrayGroupInput.setValue('next test value 2'); (model.get(index).get(0) as DynamicFormValueControlModel).value = 'next test value 1'; (model.get(index + step).get(0) as DynamicFormValueControlModel).value = 'next test value 2'; @@ -865,8 +871,8 @@ describe('FormBuilderService test suite', () => { expect(formArray.length).toBe(model.initialCount); - expect((formArray.at(index) as FormGroup).controls.testFormArrayGroupInput.value).toEqual('next test value 2'); - expect((formArray.at(index + step) as FormGroup).controls.testFormArrayGroupInput.value).toEqual('next test value 1'); + expect((formArray.at(index) as UntypedFormGroup).controls.testFormArrayGroupInput.value).toEqual('next test value 2'); + expect((formArray.at(index + step) as UntypedFormGroup).controls.testFormArrayGroupInput.value).toEqual('next test value 1'); expect((model.get(index).get(0) as DynamicFormValueControlModel).value).toEqual('next test value 2'); expect((model.get(index + step).get(0) as DynamicFormValueControlModel).value).toEqual('next test value 1'); @@ -879,8 +885,8 @@ describe('FormBuilderService test suite', () => { const index = 3; const step = -1; - (formArray.at(index) as FormGroup).controls.testFormArrayGroupInput.setValue('next test value 1'); - (formArray.at(index + step) as FormGroup).controls.testFormArrayGroupInput.setValue('next test value 2'); + (formArray.at(index) as UntypedFormGroup).controls.testFormArrayGroupInput.setValue('next test value 1'); + (formArray.at(index + step) as UntypedFormGroup).controls.testFormArrayGroupInput.setValue('next test value 2'); (model.get(index).get(0) as DynamicFormValueControlModel).value = 'next test value 1'; (model.get(index + step).get(0) as DynamicFormValueControlModel).value = 'next test value 2'; @@ -889,8 +895,8 @@ describe('FormBuilderService test suite', () => { expect(formArray.length).toBe(model.initialCount); - expect((formArray.at(index) as FormGroup).controls.testFormArrayGroupInput.value).toEqual('next test value 2'); - expect((formArray.at(index + step) as FormGroup).controls.testFormArrayGroupInput.value).toEqual('next test value 1'); + expect((formArray.at(index) as UntypedFormGroup).controls.testFormArrayGroupInput.value).toEqual('next test value 2'); + expect((formArray.at(index + step) as UntypedFormGroup).controls.testFormArrayGroupInput.value).toEqual('next test value 1'); expect((model.get(index).get(0) as DynamicFormValueControlModel).value).toEqual('next test value 2'); expect((model.get(index + step).get(0) as DynamicFormValueControlModel).value).toEqual('next test value 1'); diff --git a/src/app/shared/form/builder/form-builder.service.ts b/src/app/shared/form/builder/form-builder.service.ts index 47c71ebb543..16d280a94db 100644 --- a/src/app/shared/form/builder/form-builder.service.ts +++ b/src/app/shared/form/builder/form-builder.service.ts @@ -1,5 +1,5 @@ import { Injectable, Optional } from '@angular/core'; -import { AbstractControl, FormArray, FormControl, FormGroup } from '@angular/forms'; +import { AbstractControl, FormArray, UntypedFormControl, UntypedFormGroup } from '@angular/forms'; import { DYNAMIC_FORM_CONTROL_TYPE_ARRAY, @@ -60,7 +60,7 @@ export class FormBuilderService extends DynamicFormService { /** * This map contains the active forms control groups */ - private formGroups: Map; + private formGroups: Map; /** * This is the field to use for type binding @@ -82,7 +82,7 @@ export class FormBuilderService extends DynamicFormService { } } - createDynamicFormControlEvent(control: FormControl, group: FormGroup, model: DynamicFormControlModel, type: string): DynamicFormControlEvent { + createDynamicFormControlEvent(control: UntypedFormControl, group: UntypedFormGroup, model: DynamicFormControlModel, type: string): DynamicFormControlEvent { const $event = { value: (model as any).value, autoSave: false @@ -429,12 +429,12 @@ export class FormBuilderService extends DynamicFormService { return model.type === DYNAMIC_FORM_CONTROL_TYPE_INPUT; } - getFormControlById(id: string, formGroup: FormGroup, groupModel: DynamicFormControlModel[], index = 0): AbstractControl { + getFormControlById(id: string, formGroup: UntypedFormGroup, groupModel: DynamicFormControlModel[], index = 0): AbstractControl { const fieldModel = this.findById(id, groupModel, index); return isNotEmpty(fieldModel) ? formGroup.get(this.getPath(fieldModel)) : null; } - getFormControlByModel(formGroup: FormGroup, fieldModel: DynamicFormControlModel): AbstractControl { + getFormControlByModel(formGroup: UntypedFormGroup, fieldModel: DynamicFormControlModel): AbstractControl { return isNotEmpty(fieldModel) ? formGroup.get(this.getPath(fieldModel)) : null; } @@ -481,7 +481,7 @@ export class FormBuilderService extends DynamicFormService { * @param id id of model * @param formGroup FormGroup */ - addFormGroups(id: string, formGroup: FormGroup): void { + addFormGroups(id: string, formGroup: UntypedFormGroup): void { this.formGroups.set(id, formGroup); } @@ -505,9 +505,10 @@ export class FormBuilderService extends DynamicFormService { */ updateModelValue(fieldId: string, value: FormFieldMetadataValueObject): DynamicFormControlModel { let returnModel = null; - this.formModels.forEach((models, formId) => { + [...this.formModels.keys()].find((formId) => { + const models = this.formModels.get(formId); const fieldModel: any = this.findById(fieldId, models); - if (hasValue(fieldModel)) { + if (hasValue(fieldModel) && !fieldModel.hidden) { if (isNotEmpty(value)) { if (fieldModel.repeatable && isNotEmpty(fieldModel.value)) { // if model is repeatable and has already a value add a new field instead of replacing it @@ -527,8 +528,9 @@ export class FormBuilderService extends DynamicFormService { returnModel = fieldModel; } } - return; + return returnModel; } + return false; }); return returnModel; } @@ -647,13 +649,13 @@ export class FormBuilderService extends DynamicFormService { copyFormArrayGroup(index: number, formArray: FormArray, formArrayModel: DynamicFormArrayModel) { const groupModel = formArrayModel.insertGroup(index); - const previousGroup = formArray.controls[index] as FormGroup; + const previousGroup = formArray.controls[index] as UntypedFormGroup; const newGroup = this.createFormGroup(groupModel.group, null, groupModel); const previousKey = Object.keys(previousGroup.getRawValue())[0]; const newKey = Object.keys(newGroup.getRawValue())[0]; - - if (!isObjectEmpty(previousGroup.getRawValue()[previousKey])) { - newGroup.get(newKey).setValue(previousGroup.getRawValue()[previousKey]); + const rawValue = previousGroup.getRawValue()[previousKey]; + if (!isObjectEmpty(rawValue)) { + newGroup.get(newKey).patchValue(rawValue); } formArray.insert(index, newGroup); diff --git a/src/app/shared/form/builder/models/form-field-metadata-value.model.ts b/src/app/shared/form/builder/models/form-field-metadata-value.model.ts index 7e32f20d13d..3d727f05e9d 100644 --- a/src/app/shared/form/builder/models/form-field-metadata-value.model.ts +++ b/src/app/shared/form/builder/models/form-field-metadata-value.model.ts @@ -12,6 +12,10 @@ export interface OtherInformation { * A class representing a specific input-form field's value */ export class FormFieldMetadataValueObject implements MetadataValueInterface { + + static readonly AUTHORITY_SPLIT: string = '::'; + static readonly AUTHORITY_GENERATE: string = 'will be generated' + FormFieldMetadataValueObject.AUTHORITY_SPLIT; + metadata?: string; value: any; display: string; @@ -61,6 +65,13 @@ export class FormFieldMetadataValueObject implements MetadataValueInterface { return Metadata.hasValidAuthority(this.authority); } + /** + * Returns true if this object has an authority value that needs to be generated + */ + hasAuthorityToGenerate(): boolean { + return isNotEmpty(this.authority) && this.authority.startsWith(FormFieldMetadataValueObject.AUTHORITY_GENERATE); + } + /** * Returns true if this this object has a value */ diff --git a/src/app/shared/form/builder/models/form-field.model.ts b/src/app/shared/form/builder/models/form-field.model.ts index 1c961c75630..b6fae5363a0 100644 --- a/src/app/shared/form/builder/models/form-field.model.ts +++ b/src/app/shared/form/builder/models/form-field.model.ts @@ -127,6 +127,7 @@ export class FormFieldModel { */ @autoserialize visibility: SubmissionVisibilityType; + /** * The security config values */ diff --git a/src/app/shared/form/builder/parsers/concat-field-parser.ts b/src/app/shared/form/builder/parsers/concat-field-parser.ts index 89441041093..97a8e867029 100644 --- a/src/app/shared/form/builder/parsers/concat-field-parser.ts +++ b/src/app/shared/form/builder/parsers/concat-field-parser.ts @@ -1,4 +1,4 @@ -import {Inject} from '@angular/core'; +import { Inject } from '@angular/core'; import { FormFieldModel } from '../models/form-field.model'; import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model'; import { DynamicFormControlLayout } from '@ng-dynamic-forms/core'; @@ -85,6 +85,13 @@ export class ConcatFieldParser extends FieldParser { input1ModelConfig.hint = undefined; } + if (this.configData.mandatory) { + concatGroup.required = true; + input1ModelConfig.required = true; + } + + concatGroup.disabled = input1ModelConfig.readOnly; + if (isNotEmpty(this.firstPlaceholder)) { input1ModelConfig.placeholder = this.firstPlaceholder; } @@ -116,7 +123,7 @@ export class ConcatFieldParser extends FieldParser { control: 'form-row', } }; - this.initSecurityValue(concatGroup); + this.initSecurityValue(concatGroup, fieldValue); const concatModel = new DynamicConcatModel(concatGroup, clsGroup); concatModel.name = this.getFieldId(); diff --git a/src/app/shared/form/builder/parsers/field-parser.ts b/src/app/shared/form/builder/parsers/field-parser.ts index 15ce3068374..f6e52e5e826 100644 --- a/src/app/shared/form/builder/parsers/field-parser.ts +++ b/src/app/shared/form/builder/parsers/field-parser.ts @@ -24,6 +24,8 @@ import { ParserType } from './parser-type'; import { isNgbDateStruct } from '../../../date.util'; import { SubmissionVisibility } from '../../../../submission/utils/visibility.util'; import { SubmissionVisibilityType } from '../../../../core/config/models/config-submission-section.model'; +import { Metadata } from '../../../../core/shared/metadata.utils'; +import { MetadataValue } from '../../../../core/shared/metadata.models'; export const SUBMISSION_ID: InjectionToken = new InjectionToken('submissionId'); export const CONFIG_DATA: InjectionToken = new InjectionToken('configData'); @@ -194,12 +196,16 @@ export abstract class FieldParser { return modelConfig; } - public initSecurityValue(modelConfig: any) { - // preselect most restricted security level if is not yet selected + public initSecurityValue(modelConfig: any, forcedValue?: MetadataValue|string) { + // preselect the security level if is not yet selected // or if the current security level is not available in the current configuration if ((isEmpty(modelConfig.securityLevel) && isNotEmpty(modelConfig.securityConfigLevel)) || (isNotEmpty(modelConfig.securityLevel) && isNotEmpty(modelConfig.securityConfigLevel) && !modelConfig.securityConfigLevel.includes(modelConfig.securityLevel) )) { - modelConfig.securityLevel = modelConfig.securityConfigLevel[modelConfig.securityConfigLevel.length - 1]; + // take the first element of the securityConfigLevel array when the model config has already a value + // otherwise take the most restricted one + modelConfig.securityLevel = (Metadata.hasValue(modelConfig.value) || Metadata.hasValue(forcedValue)) ? + modelConfig.securityConfigLevel[0] : + modelConfig.securityConfigLevel[modelConfig.securityConfigLevel.length - 1]; } } @@ -309,7 +315,7 @@ export abstract class FieldParser { // Set read only option controlModel.readOnly = this.parserOptions.readOnly || this.isFieldReadOnly(this.configData.visibility, this.parserOptions.submissionScope); - controlModel.disabled = this.parserOptions.readOnly; + controlModel.disabled = controlModel.readOnly; controlModel.isModelOfInnerForm = this.parserOptions.isInnerForm; if (hasValue(this.configData.selectableRelationship)) { controlModel.relationship = Object.assign(new RelationshipOptions(), this.configData.selectableRelationship); diff --git a/src/app/shared/form/builder/parsers/onebox-field-parser.ts b/src/app/shared/form/builder/parsers/onebox-field-parser.ts index e597d82b61f..4d8f11d3f1e 100644 --- a/src/app/shared/form/builder/parsers/onebox-field-parser.ts +++ b/src/app/shared/form/builder/parsers/onebox-field-parser.ts @@ -70,11 +70,11 @@ export class OneboxFieldParser extends FieldParser { selectModelConfig.value = fieldValue.metadata; } selectModelConfig.disabled = inputModelConfig.readOnly; - inputSelectGroup.group.push(new DynamicSelectModel(selectModelConfig, clsSelect)); - inputSelectGroup.readOnly = selectModelConfig.disabled && inputModelConfig.readOnly; // in case of qualdrop do not show toggle of security inputModelConfig.toggleSecurityVisibility = false; + + inputSelectGroup.group.push(new DynamicSelectModel(selectModelConfig, clsSelect)); inputSelectGroup.group.push(new DsDynamicInputModel(inputModelConfig, clsInput)); return new DynamicQualdropModel(inputSelectGroup, clsGroup); diff --git a/src/app/shared/form/builder/parsers/relation-group-field-parser.ts b/src/app/shared/form/builder/parsers/relation-group-field-parser.ts index 29f0e92012f..2ea9ab0fb71 100644 --- a/src/app/shared/form/builder/parsers/relation-group-field-parser.ts +++ b/src/app/shared/form/builder/parsers/relation-group-field-parser.ts @@ -58,6 +58,7 @@ export class RelationGroupFieldParser extends FieldParser { } }; + this.initSecurityValue(modelConfiguration); const model = new DynamicRelationGroupModel(modelConfiguration, cls); model.name = this.getFieldId(); model.isInlineGroup = (this.configData.input.type === ParserType.InlineGroup); diff --git a/src/app/shared/form/builder/parsers/row-parser.ts b/src/app/shared/form/builder/parsers/row-parser.ts index b108ccb91bd..64c6bdc3fc0 100644 --- a/src/app/shared/form/builder/parsers/row-parser.ts +++ b/src/app/shared/form/builder/parsers/row-parser.ts @@ -108,7 +108,6 @@ export class RowParser { }); } else { setLayout(fieldModel, 'grid', 'host', layoutFieldClass); - config.group.push(fieldModel); } } diff --git a/src/app/shared/form/chips/chips.component.html b/src/app/shared/form/chips/chips.component.html index 175c9b99c2b..95e41b2725a 100644 --- a/src/app/shared/form/chips/chips.component.html +++ b/src/app/shared/form/chips/chips.component.html @@ -4,7 +4,7 @@
    - +
    - +
    diff --git a/src/app/shared/form/chips/chips.component.spec.ts b/src/app/shared/form/chips/chips.component.spec.ts index 35699ed10f6..7c24fddb57e 100644 --- a/src/app/shared/form/chips/chips.component.spec.ts +++ b/src/app/shared/form/chips/chips.component.spec.ts @@ -94,7 +94,7 @@ describe('ChipsComponent test suite', () => { })); it('should save chips item index when drag and drop start', fakeAsync(() => { - const de = chipsFixture.debugElement.query(By.css('li.nav-item')); + const de = chipsFixture.debugElement.query(By.css('div.nav-item')); de.triggerEventHandler('dragstart', null); @@ -103,7 +103,7 @@ describe('ChipsComponent test suite', () => { it('should update chips item order when drag and drop end', fakeAsync(() => { spyOn(chipsComp.chips, 'updateOrder'); - const de = chipsFixture.debugElement.query(By.css('li.nav-item')); + const de = chipsFixture.debugElement.query(By.css('div.nav-item')); de.triggerEventHandler('dragend', null); @@ -130,7 +130,7 @@ describe('ChipsComponent test suite', () => { }); it('should show icon for every field that has a configured icon', () => { - const de = chipsFixture.debugElement.query(By.css('li.nav-item')); + const de = chipsFixture.debugElement.query(By.css('div.nav-item')); const icons = de.queryAll(By.css('i.fas')); expect(icons.length).toBe(4); @@ -138,7 +138,7 @@ describe('ChipsComponent test suite', () => { }); it('should show tooltip on mouse over an icon', () => { - const de = chipsFixture.debugElement.query(By.css('li.nav-item')); + const de = chipsFixture.debugElement.query(By.css('div.nav-item')); const icons = de.queryAll(By.css('i.fas')); icons[0].triggerEventHandler('mouseover', null); @@ -166,7 +166,7 @@ describe('ChipsComponent test suite', () => { }); it('should not show tooltip on mouse over list item when display text is short', () => { - const de = chipsFixture.debugElement.query(By.css('li.nav-item')); + const de = chipsFixture.debugElement.query(By.css('div.nav-item')); de.triggerEventHandler('mouseover', null); expect(chipsComp.tipText$.value).toEqual([]); de.triggerEventHandler('mouseout', null); @@ -188,14 +188,14 @@ describe('ChipsComponent test suite', () => { }); it('should show tooltip on mouse over list item when display text is long', () => { - const de = chipsFixture.debugElement.query(By.css('li.nav-item')); + const de = chipsFixture.debugElement.query(By.css('div.nav-item')); de.triggerEventHandler('mouseover', null); expect(chipsComp.tipText$.value).toEqual(['long text to display is truncated but not in tooltip']); de.triggerEventHandler('mouseout', null); }); it('should show truncated text on list item when display text is long', () => { - const de = chipsFixture.debugElement.query(By.css('li.nav-item p.d-table-cell')); + const de = chipsFixture.debugElement.query(By.css('div.nav-item p.d-table-cell')); expect(de.nativeElement.innerText).toEqual(chipsComp.textTruncate('long text to display is truncated but not in tooltip')); }); }); diff --git a/src/app/shared/form/form.component.html b/src/app/shared/form/form.component.html index 8fb82659811..6e50faebfa9 100644 --- a/src/app/shared/form/form.component.html +++ b/src/app/shared/form/form.component.html @@ -14,13 +14,13 @@
    diff --git a/src/app/shared/form/form.component.scss b/src/app/shared/form/form.component.scss index 3c9fd327e30..f7c9da139b1 100644 --- a/src/app/shared/form/form.component.scss +++ b/src/app/shared/form/form.component.scss @@ -56,4 +56,9 @@ button.ds-form-add-more:focus { .white-background{ background-color: #fff; -} \ No newline at end of file +} + +.dropdown-remove { + padding-left: 10px; + padding-right: 5px; +} diff --git a/src/app/shared/form/form.component.ts b/src/app/shared/form/form.component.ts index 34d2d2266d6..c8a006a2818 100644 --- a/src/app/shared/form/form.component.ts +++ b/src/app/shared/form/form.component.ts @@ -1,6 +1,6 @@ import { distinctUntilChanged, filter, map } from 'rxjs/operators'; import { ChangeDetectorRef, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; -import { AbstractControl, FormArray, FormControl, FormGroup } from '@angular/forms'; +import { AbstractControl, FormArray, FormControl, UntypedFormArray, UntypedFormControl, UntypedFormGroup } from '@angular/forms'; import { Observable, Subscription } from 'rxjs'; import { @@ -78,7 +78,7 @@ export class FormComponent implements OnDestroy, OnInit { */ @Input() formModel: DynamicFormControlModel[]; @Input() parentFormModel: DynamicFormGroupModel | DynamicFormGroupModel[]; - @Input() formGroup: FormGroup; + @Input() formGroup: UntypedFormGroup; @Input() formLayout = null as DynamicFormLayout; @Input() arrayButtonsStyle: string; @Input() isInlineGroupForm: boolean; @@ -134,9 +134,9 @@ export class FormComponent implements OnDestroy, OnInit { })); }*/ - private getFormGroup(): FormGroup { + private getFormGroup(): UntypedFormGroup { if (!!this.parentFormModel) { - return this.formGroup.parent as FormGroup; + return this.formGroup.parent as UntypedFormGroup; } return this.formGroup; @@ -197,7 +197,7 @@ export class FormComponent implements OnDestroy, OnInit { const { fieldIndex } = error; let field: AbstractControl; if (!!this.parentFormModel) { - field = this.formBuilderService.getFormControlById(fieldId, formGroup.parent as FormGroup, formModel, fieldIndex); + field = this.formBuilderService.getFormControlById(fieldId, formGroup.parent as UntypedFormGroup, formModel, fieldIndex); } else { field = this.formBuilderService.getFormControlById(fieldId, formGroup, formModel, fieldIndex); } @@ -220,7 +220,7 @@ export class FormComponent implements OnDestroy, OnInit { const { fieldIndex } = error; let field: AbstractControl; if (!!this.parentFormModel) { - field = this.formBuilderService.getFormControlById(fieldId, formGroup.parent as FormGroup, formModel, fieldIndex); + field = this.formBuilderService.getFormControlById(fieldId, formGroup.parent as UntypedFormGroup, formModel, fieldIndex); } else { field = this.formBuilderService.getFormControlById(fieldId, formGroup, formModel, fieldIndex); } @@ -268,7 +268,7 @@ export class FormComponent implements OnDestroy, OnInit { onBlur(event: DynamicFormControlEvent): void { this.blur.emit(event); - const control: FormControl = event.control; + const control: UntypedFormControl = event.control; const fieldIndex: number = (event.context && event.context.index) ? event.context.index : 0; if (control.valid) { this.formService.removeError(this.formId, event.model.name, fieldIndex); @@ -282,7 +282,7 @@ export class FormComponent implements OnDestroy, OnInit { event.$event.updatedModels.forEach((model) => { const control: FormControl = this.formBuilderService.getFormControlByModel(this.formGroup, model) as FormControl; if (control) { - const changeEvent = this.formBuilderService.createDynamicFormControlEvent(control, control.parent as FormGroup, model, 'change'); + const changeEvent = this.formBuilderService.createDynamicFormControlEvent(control, control.parent as UntypedFormGroup, model, 'change'); this.onChange(changeEvent); } }); @@ -302,6 +302,12 @@ export class FormComponent implements OnDestroy, OnInit { if (this.emitChange) { this.change.emit(event); } + + const control: UntypedFormControl = event.control; + const fieldIndex: number = (event.context && event.context.index) ? event.context.index : 0; + if (control.valid) { + this.formService.removeError(this.formId, event.model.id, fieldIndex); + } } /** @@ -331,7 +337,7 @@ export class FormComponent implements OnDestroy, OnInit { } removeItem($event, arrayContext: DynamicFormArrayModel, index: number): void { - const formArrayControl = this.formGroup.get(this.formBuilderService.getPath(arrayContext)) as FormArray; + const formArrayControl = this.formGroup.get(this.formBuilderService.getPath(arrayContext)) as UntypedFormArray; const event = this.getEvent($event, arrayContext, index, 'remove'); if (this.formBuilderService.isQualdropGroup(event.model as DynamicFormControlModel) || this.isInlineGroupForm) { // In case of qualdrop value or inline-group remove event must be dispatched before removing the control from array @@ -361,7 +367,7 @@ export class FormComponent implements OnDestroy, OnInit { } insertItem($event, arrayContext: DynamicFormArrayModel, index: number): void { - const formArrayControl = this.formGroup.get(this.formBuilderService.getPath(arrayContext)) as FormArray; + const formArrayControl = this.formGroup.get(this.formBuilderService.getPath(arrayContext)) as UntypedFormArray; this.formBuilderService.insertFormArrayGroup(index, formArrayControl, arrayContext); this.addArrayItem.emit(this.getEvent($event, arrayContext, index, 'add')); this.formService.changeForm(this.formId, this.formModel); @@ -390,20 +396,20 @@ export class FormComponent implements OnDestroy, OnInit { return model.parent instanceof DynamicFormArrayGroupModel && model.parent?.group?.length === 1; } - protected getEvent($event: any, arrayContext: DynamicFormArrayModel, index: number, type: string, formGroup?: FormGroup): DynamicFormControlEvent { + protected getEvent($event: any, arrayContext: DynamicFormArrayModel, index: number, type: string, formGroup?: UntypedFormGroup): DynamicFormControlEvent { const context = arrayContext.groups[index]; const itemGroupModel = context.context; - let group = (formGroup) ? formGroup : this.formGroup.get(itemGroupModel.id) as FormGroup; + let group = (formGroup) ? formGroup : this.formGroup.get(itemGroupModel.id) as UntypedFormGroup; if (isNull(group)) { for (const key of Object.keys(this.formGroup.controls)) { - group = this.formGroup.controls[key].get(itemGroupModel.id) as FormGroup; + group = this.formGroup.controls[key].get(itemGroupModel.id) as UntypedFormGroup; if (isNotNull(group)) { break; } } } const model = context.group[0] as DynamicFormControlModel; - const control = group.controls[index] as FormControl; + const control = group.controls[index] as UntypedFormControl; return { $event, context, control, group, model, type }; } } diff --git a/src/app/shared/form/form.module.ts b/src/app/shared/form/form.module.ts index d101bfa72e6..6d95ea6c63a 100644 --- a/src/app/shared/form/form.module.ts +++ b/src/app/shared/form/form.module.ts @@ -32,7 +32,7 @@ import { NumberPickerComponent } from './number-picker/number-picker.component'; import { AuthorityConfidenceStateDirective } from './directives/authority-confidence-state.directive'; import { SortablejsModule } from 'ngx-sortablejs'; import { VocabularyTreeviewComponent } from './vocabulary-treeview/vocabulary-treeview.component'; -import { VocabularyTreeviewService } from './vocabulary-treeview/vocabulary-treeview.service'; +import { VocabularyTreeviewModalComponent } from './vocabulary-treeview-modal/vocabulary-treeview-modal.component'; import { FormBuilderService } from './builder/form-builder.service'; import { DsDynamicTypeBindRelationService } from './builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service'; import { FormService } from './form.service'; @@ -40,6 +40,8 @@ import { NgxMaskModule } from 'ngx-mask'; import { ThemedExternalSourceEntryImportModalComponent } from './builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/themed-external-source-entry-import-modal.component'; import { NgbDatepickerModule, NgbTimepickerModule } from '@ng-bootstrap/ng-bootstrap'; import { CdkTreeModule } from '@angular/cdk/tree'; +import { ThemedDynamicLookupRelationSearchTabComponent } from './builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/themed-dynamic-lookup-relation-search-tab.component'; +import { ThemedDynamicLookupRelationExternalSourceTabComponent } from './builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/themed-dynamic-lookup-relation-external-source-tab.component'; import { DsDynamicRelationGroupModalComponent } from './builder/ds-dynamic-form-ui/models/relation-group/modal/dynamic-relation-group-modal.components'; import { DsDynamicRelationInlineGroupComponent } from './builder/ds-dynamic-form-ui/models/relation-inline-group/dynamic-relation-inline-group.components'; @@ -50,8 +52,10 @@ const COMPONENTS = [ DsDynamicListComponent, DsDynamicLookupComponent, DsDynamicLookupRelationSearchTabComponent, + ThemedDynamicLookupRelationSearchTabComponent, DsDynamicLookupRelationSelectionTabComponent, DsDynamicLookupRelationExternalSourceTabComponent, + ThemedDynamicLookupRelationExternalSourceTabComponent, DsDynamicDisabledComponent, DsDynamicLookupRelationModalComponent, DsDynamicRelationGroupModalComponent, @@ -71,7 +75,8 @@ const COMPONENTS = [ ChipsComponent, NumberPickerComponent, VocabularyTreeviewComponent, - ThemedExternalSourceEntryImportModalComponent + VocabularyTreeviewModalComponent, + ThemedExternalSourceEntryImportModalComponent, ]; const DIRECTIVES = [ @@ -105,7 +110,6 @@ const DIRECTIVES = [ provide: DYNAMIC_FORM_CONTROL_MAP_FN, useValue: dsDynamicFormControlMapFn }, - VocabularyTreeviewService, DynamicFormLayoutService, DynamicFormService, DynamicFormValidationService, diff --git a/src/app/shared/form/form.service.spec.ts b/src/app/shared/form/form.service.spec.ts index f35845e115f..69f30e65699 100644 --- a/src/app/shared/form/form.service.spec.ts +++ b/src/app/shared/form/form.service.spec.ts @@ -1,6 +1,6 @@ import { Store, StoreModule } from '@ngrx/store'; import { inject, TestBed, waitForAsync } from '@angular/core/testing'; -import { AbstractControl, FormControl, FormGroup, Validators } from '@angular/forms'; +import { AbstractControl, FormControl, UntypedFormControl, UntypedFormGroup, Validators } from '@angular/forms'; import { DynamicFormControlModel, DynamicFormGroupModel, DynamicInputModel } from '@ng-dynamic-forms/core'; @@ -23,7 +23,7 @@ describe('FormService test suite', () => { const formId = 'testForm'; let service: FormService; let builderService: FormBuilderService; - let formGroup: FormGroup; + let formGroup: UntypedFormGroup; const formModel: DynamicFormControlModel[] = [ new DynamicInputModel({ id: 'author', value: 'test' }), @@ -113,23 +113,23 @@ describe('FormService test suite', () => { .subscribe((state) => { state.forms = formState; }); - const author: AbstractControl = new FormControl('test'); - const title: AbstractControl = new FormControl(undefined, Validators.required); - const date: AbstractControl = new FormControl(undefined); - const description: AbstractControl = new FormControl(undefined); - - const addressLocation: FormGroup = new FormGroup({ - zipCode: new FormControl(undefined), - state: new FormControl(undefined), - city: new FormControl(undefined), + const author: AbstractControl = new UntypedFormControl('test'); + const title: AbstractControl = new UntypedFormControl(undefined, Validators.required); + const date: AbstractControl = new UntypedFormControl(undefined); + const description: AbstractControl = new UntypedFormControl(undefined); + + const addressLocation: UntypedFormGroup = new UntypedFormGroup({ + zipCode: new UntypedFormControl(undefined), + state: new UntypedFormControl(undefined), + city: new UntypedFormControl(undefined), }); - const name: FormGroup = new FormGroup({ + const name: UntypedFormGroup = new UntypedFormGroup({ name_CONCAT_FIRST_INPUT: new FormControl(undefined), name_CONCAT_SECOND_INPUT: new FormControl(undefined) }); - formGroup = new FormGroup({ author, title, date, description, addressLocation, name }); + formGroup = new UntypedFormGroup({ author, title, date, description, addressLocation, name }); controls = { author, title, date, description , addressLocation, name }; service = new FormService(builderService, store); }) diff --git a/src/app/shared/form/form.service.ts b/src/app/shared/form/form.service.ts index 8441533284d..682c8dc1780 100644 --- a/src/app/shared/form/form.service.ts +++ b/src/app/shared/form/form.service.ts @@ -1,6 +1,6 @@ import { distinctUntilChanged, filter, map } from 'rxjs/operators'; import { Injectable } from '@angular/core'; -import { AbstractControl, FormArray, FormControl, FormGroup } from '@angular/forms'; +import { AbstractControl, UntypedFormArray, UntypedFormControl, UntypedFormGroup } from '@angular/forms'; import { Observable } from 'rxjs'; import { select, Store } from '@ngrx/store'; @@ -96,13 +96,13 @@ export class FormService { /** * Method to validate form's fields */ - public validateAllFormFields(formGroup: FormGroup | FormArray) { + public validateAllFormFields(formGroup: UntypedFormGroup | UntypedFormArray) { Object.keys(formGroup.controls).forEach((field) => { const control = formGroup.get(field); - if (control instanceof FormControl) { + if (control instanceof UntypedFormControl) { control.markAsTouched({ onlySelf: true }); control.markAsDirty({ onlySelf: true }); - } else if (control instanceof FormGroup || control instanceof FormArray) { + } else if (control instanceof UntypedFormGroup || control instanceof UntypedFormArray) { this.validateAllFormFields(control); } }); @@ -112,14 +112,14 @@ export class FormService { * Check if form group has an invalid form control * @param formGroup The form group to check */ - public hasValidationErrors(formGroup: FormGroup | FormArray): boolean { + public hasValidationErrors(formGroup: UntypedFormGroup | UntypedFormArray): boolean { let hasErrors = false; const fields: string[] = Object.keys(formGroup.controls); for (const field of fields) { const control = formGroup.get(field); - if (control instanceof FormControl) { + if (control instanceof UntypedFormControl) { hasErrors = !control.valid && control.touched; - } else if (control instanceof FormGroup || control instanceof FormArray) { + } else if (control instanceof UntypedFormGroup || control instanceof UntypedFormArray) { hasErrors = this.hasValidationErrors(control); } if (hasErrors) { @@ -163,7 +163,7 @@ export class FormService { } // if the field in question is a concat group, pass down the error to its fields - if (field instanceof FormGroup && model instanceof DynamicFormGroupModel && this.formBuilderService.isConcatGroup(model)) { + if (field instanceof UntypedFormGroup && model instanceof DynamicFormGroupModel && this.formBuilderService.isConcatGroup(model)) { model.group.forEach((subModel) => { const subField = field.controls[subModel.id]; @@ -186,7 +186,7 @@ export class FormService { } // if the field in question is a concat group, clear the error from its fields - if (field instanceof FormGroup && model instanceof DynamicFormGroupModel && this.formBuilderService.isConcatGroup(model)) { + if (field instanceof UntypedFormGroup && model instanceof DynamicFormGroupModel && this.formBuilderService.isConcatGroup(model)) { model.group.forEach((subModel) => { const subField = field.controls[subModel.id]; @@ -197,7 +197,7 @@ export class FormService { field.markAsUntouched(); } - public resetForm(formGroup: FormGroup, groupModel: DynamicFormControlModel[], formId: string) { + public resetForm(formGroup: UntypedFormGroup, groupModel: DynamicFormControlModel[], formId: string) { this.formBuilderService.clearAllModelsValue(groupModel); formGroup.reset(); this.store.dispatch(new FormChangeAction(formId, formGroup.value)); diff --git a/src/app/shared/form/number-picker/number-picker.component.ts b/src/app/shared/form/number-picker/number-picker.component.ts index 54800e52191..b47a63a75d1 100644 --- a/src/app/shared/form/number-picker/number-picker.component.ts +++ b/src/app/shared/form/number-picker/number-picker.component.ts @@ -1,5 +1,5 @@ import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output, SimpleChanges, } from '@angular/core'; -import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { ControlValueAccessor, UntypedFormBuilder, NG_VALUE_ACCESSOR } from '@angular/forms'; import { isEmpty } from '../../empty.util'; @Component({ @@ -32,7 +32,7 @@ export class NumberPickerComponent implements OnInit, ControlValueAccessor { startValue: number; - constructor(private fb: FormBuilder, private cd: ChangeDetectorRef) { + constructor(private fb: UntypedFormBuilder, private cd: ChangeDetectorRef) { } ngOnInit() { @@ -104,13 +104,12 @@ export class NumberPickerComponent implements OnInit, ControlValueAccessor { if (i >= this.min && i <= this.max) { this.value = i; - this.emitChange(); } else if (event.target.value === null || event.target.value === '') { this.value = null; - this.emitChange(); } else { this.value = undefined; } + this.emitChange(); } catch (e) { this.value = undefined; } diff --git a/src/app/shared/form/vocabulary-treeview-modal/vocabulary-treeview-modal.component.html b/src/app/shared/form/vocabulary-treeview-modal/vocabulary-treeview-modal.component.html new file mode 100644 index 00000000000..8498411a38c --- /dev/null +++ b/src/app/shared/form/vocabulary-treeview-modal/vocabulary-treeview-modal.component.html @@ -0,0 +1,17 @@ + + diff --git a/src/app/shared/form/vocabulary-treeview-modal/vocabulary-treeview-modal.component.scss b/src/app/shared/form/vocabulary-treeview-modal/vocabulary-treeview-modal.component.scss new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/app/shared/form/vocabulary-treeview-modal/vocabulary-treeview-modal.component.spec.ts b/src/app/shared/form/vocabulary-treeview-modal/vocabulary-treeview-modal.component.spec.ts new file mode 100644 index 00000000000..ddf7025a688 --- /dev/null +++ b/src/app/shared/form/vocabulary-treeview-modal/vocabulary-treeview-modal.component.spec.ts @@ -0,0 +1,42 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateModule } from '@ngx-translate/core'; + +import { VocabularyTreeviewModalComponent } from './vocabulary-treeview-modal.component'; +import { VocabularyOptions } from '../../../core/submission/vocabularies/models/vocabulary-options.model'; + +describe('VocabularyTreeviewModalComponent', () => { + let component: VocabularyTreeviewModalComponent; + let fixture: ComponentFixture; + + const modalStub = jasmine.createSpyObj('modalStub', ['close']); + const vocabularyOptions = new VocabularyOptions('vocabularyTest', null, null, false); + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ TranslateModule.forRoot() ], + declarations: [ VocabularyTreeviewModalComponent ], + providers: [ + { provide: NgbActiveModal, useValue: modalStub }, + ], + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(VocabularyTreeviewModalComponent); + component = fixture.componentInstance; + component.vocabularyOptions = vocabularyOptions; + spyOn(component as any, 'setDescription').and.callThrough(); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should init descrption message', () => { + expect((component as any).setDescription).toHaveBeenCalled(); + }); +}); diff --git a/src/app/shared/form/vocabulary-treeview-modal/vocabulary-treeview-modal.component.ts b/src/app/shared/form/vocabulary-treeview-modal/vocabulary-treeview-modal.component.ts new file mode 100644 index 00000000000..706b725f89e --- /dev/null +++ b/src/app/shared/form/vocabulary-treeview-modal/vocabulary-treeview-modal.component.ts @@ -0,0 +1,75 @@ +import { Component, Input, OnInit } from '@angular/core'; + +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateService } from '@ngx-translate/core'; + +import { VocabularyOptions } from '../../../core/submission/vocabularies/models/vocabulary-options.model'; +import { VocabularyEntryDetail } from '../../../core/submission/vocabularies/models/vocabulary-entry-detail.model'; + +@Component({ + selector: 'ds-vocabulary-treeview-modal', + templateUrl: './vocabulary-treeview-modal.component.html', + styleUrls: ['./vocabulary-treeview-modal.component.scss'] +}) +/** + * Component that contains a modal to display a VocabularyTreeviewComponent + */ +export class VocabularyTreeviewModalComponent implements OnInit { + + /** + * The {@link VocabularyOptions} object + */ + @Input() vocabularyOptions: VocabularyOptions; + + /** + * Representing how many tree level load at initialization + */ + @Input() preloadLevel = 2; + + /** + * The vocabulary entries already selected, if any + */ + @Input() selectedItems: VocabularyEntryDetail[] = []; + + /** + * Whether to allow selecting multiple values with checkboxes + */ + @Input() multiSelect = false; + + /** + * Contain a descriptive message for this vocabulary retrieved from i18n files + */ + description: string; + + /** + * Initialize instance variables + * + * @param {NgbActiveModal} activeModal + * @param {TranslateService} translate + */ + constructor( + public activeModal: NgbActiveModal, + protected translate: TranslateService + ) { } + + + ngOnInit(): void { + this.setDescription(); + } + + /** + * Method called on entry select + */ + onSelect(item: VocabularyEntryDetail) { + this.activeModal.close(item); + } + + /** + * Set the description message related to the given vocabulary + */ + private setDescription() { + const descriptionLabel = 'vocabulary-treeview.tree.description.' + this.vocabularyOptions.name; + this.description = this.translate.instant(descriptionLabel); + } + +} diff --git a/src/app/shared/form/vocabulary-treeview/vocabulary-treeview-node.model.ts b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview-node.model.ts index c167328cab0..4ac1b084254 100644 --- a/src/app/shared/form/vocabulary-treeview/vocabulary-treeview-node.model.ts +++ b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview-node.model.ts @@ -21,7 +21,8 @@ export class TreeviewNode { public pageInfo: PageInfo = new PageInfo(), public loadMoreParentItem: VocabularyEntryDetail | null = null, public isSearchNode = false, - public isInInitValueHierarchy = false) { + public isInInitValueHierarchy = false, + public isSelected = false) { } updatePageInfo(pageInfo: PageInfo) { @@ -38,7 +39,8 @@ export class TreeviewFlatNode { public pageInfo: PageInfo = new PageInfo(), public loadMoreParentItem: VocabularyEntryDetail | null = null, public isSearchNode = false, - public isInInitValueHierarchy = false) { + public isInInitValueHierarchy = false, + public isSelected = false) { } } diff --git a/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.html b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.html index 45ad5b3f16e..4de58d80b1c 100644 --- a/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.html +++ b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.html @@ -1,77 +1,101 @@ - - + + + + + + + +
    diff --git a/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.scss b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.scss index 39050ff85b8..3f0cea10d25 100644 --- a/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.scss +++ b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.scss @@ -5,3 +5,7 @@ cdk-tree .btn:focus { box-shadow: none !important; } + +label { + cursor: pointer; +} diff --git a/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.spec.ts b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.spec.ts index e156f1e8f1e..8af5ca3359d 100644 --- a/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.spec.ts +++ b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.spec.ts @@ -1,32 +1,29 @@ -import { ChangeDetectorRef, Component, NO_ERRORS_SCHEMA } from '@angular/core'; +import { Component, NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing'; import { CdkTreeModule } from '@angular/cdk/tree'; +import { By } from '@angular/platform-browser'; import { of as observableOf } from 'rxjs'; -import { StoreModule } from '@ngrx/store'; import { TranslateModule } from '@ngx-translate/core'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { provideMockStore } from '@ngrx/store/testing'; import { createTestComponent } from '../../testing/utils.test'; import { VocabularyTreeviewComponent } from './vocabulary-treeview.component'; import { VocabularyTreeviewService } from './vocabulary-treeview.service'; import { VocabularyEntryDetail } from '../../../core/submission/vocabularies/models/vocabulary-entry-detail.model'; -import { TreeviewFlatNode } from './vocabulary-treeview-node.model'; +import { TreeviewFlatNode, TreeviewNode } from './vocabulary-treeview-node.model'; import { FormFieldMetadataValueObject } from '../builder/models/form-field-metadata-value.model'; import { VocabularyOptions } from '../../../core/submission/vocabularies/models/vocabulary-options.model'; import { PageInfo } from '../../../core/shared/page-info.model'; +import { VocabularyService } from '../../../core/submission/vocabularies/vocabulary.service'; import { VocabularyEntry } from '../../../core/submission/vocabularies/models/vocabulary-entry.model'; -import { AuthTokenInfo } from '../../../core/auth/models/auth-token-info.model'; -import { authReducer } from '../../../core/auth/auth.reducer'; -import { storeModuleConfig } from '../../../app.reducer'; describe('VocabularyTreeviewComponent test suite', () => { let comp: VocabularyTreeviewComponent; let compAsAny: any; let fixture: ComponentFixture; - let initialState; + let de; const item = new VocabularyEntryDetail(); item.id = 'node1'; @@ -47,26 +44,19 @@ describe('VocabularyTreeviewComponent test suite', () => { restoreNodes: jasmine.createSpy('restoreNodes'), cleanTree: jasmine.createSpy('cleanTree'), }); - - initialState = { - core: { - auth: { - authenticated: true, - loaded: true, - blocking: false, - loading: false, - authToken: new AuthTokenInfo('test_token'), - userId: 'testid', - authMethods: [] - } - } - }; + const vocabularyServiceStub = jasmine.createSpyObj('VocabularyService', { + getVocabularyEntriesByValue: jasmine.createSpy('getVocabularyEntriesByValue'), + getEntryDetailParent: jasmine.createSpy('getEntryDetailParent'), + findEntryDetailById: jasmine.createSpy('findEntryDetailById'), + searchTopEntries: jasmine.createSpy('searchTopEntries'), + getEntryDetailChildren: jasmine.createSpy('getEntryDetailChildren'), + clearSearchTopRequests: jasmine.createSpy('clearSearchTopRequests') + }); beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [ CdkTreeModule, - StoreModule.forRoot({ auth: authReducer }, storeModuleConfig), TranslateModule.forRoot() ], declarations: [ @@ -75,9 +65,8 @@ describe('VocabularyTreeviewComponent test suite', () => { ], providers: [ { provide: VocabularyTreeviewService, useValue: vocabularyTreeviewServiceStub }, + { provide: VocabularyService, useValue: vocabularyServiceStub }, { provide: NgbActiveModal, useValue: modalStub }, - provideMockStore({ initialState }), - ChangeDetectorRef, VocabularyTreeviewComponent ], schemas: [NO_ERRORS_SCHEMA] @@ -117,13 +106,7 @@ describe('VocabularyTreeviewComponent test suite', () => { comp = fixture.componentInstance; compAsAny = comp; comp.vocabularyOptions = vocabularyOptions; - comp.selectedItem = null; - }); - - afterEach(() => { - fixture.destroy(); - comp = null; - compAsAny = null; + comp.selectedItems = []; }); it('should should init component properly', () => { @@ -138,10 +121,10 @@ describe('VocabularyTreeviewComponent test suite', () => { currentValue.otherInformation = { id: 'entryID' }; - comp.selectedItem = currentValue; + comp.selectedItems = [currentValue]; fixture.detectChanges(); expect(comp.dataSource.data).toEqual([]); - expect(vocabularyTreeviewServiceStub.initialize).toHaveBeenCalledWith(comp.vocabularyOptions, new PageInfo(), null); + expect(vocabularyTreeviewServiceStub.initialize).toHaveBeenCalledWith(comp.vocabularyOptions, new PageInfo(), ['entryID'], 'entryID'); }); it('should should init component properly with init value as VocabularyEntry', () => { @@ -150,30 +133,40 @@ describe('VocabularyTreeviewComponent test suite', () => { currentValue.otherInformation = { id: 'entryID' }; - comp.selectedItem = currentValue; + comp.selectedItems = [currentValue]; fixture.detectChanges(); expect(comp.dataSource.data).toEqual([]); - expect(vocabularyTreeviewServiceStub.initialize).toHaveBeenCalledWith(comp.vocabularyOptions, new PageInfo(), null); + expect(vocabularyTreeviewServiceStub.initialize).toHaveBeenCalledWith(comp.vocabularyOptions, new PageInfo(), ['entryID'], 'entryID'); + }); + + it('should should init component properly with init value as VocabularyEntryDetail', () => { + const currentValue = new VocabularyEntryDetail(); + currentValue.value = 'testValue'; + currentValue.id = 'entryID'; + comp.selectedItems = [currentValue]; + fixture.detectChanges(); + expect(comp.dataSource.data).toEqual([]); + expect(vocabularyTreeviewServiceStub.initialize).toHaveBeenCalledWith(comp.vocabularyOptions, new PageInfo(), ['entryID'], 'entryID'); }); it('should call loadMore function', () => { comp.loadMore(item); fixture.detectChanges(); - expect(vocabularyTreeviewServiceStub.loadMore).toHaveBeenCalledWith(item); + expect(vocabularyTreeviewServiceStub.loadMore).toHaveBeenCalledWith(item, []); }); it('should call loadMoreRoot function', () => { const node = new TreeviewFlatNode(item); comp.loadMoreRoot(node); fixture.detectChanges(); - expect(vocabularyTreeviewServiceStub.loadMoreRoot).toHaveBeenCalledWith(node); + expect(vocabularyTreeviewServiceStub.loadMoreRoot).toHaveBeenCalledWith(node, []); }); it('should call loadChildren function', () => { const node = new TreeviewFlatNode(item); comp.loadChildren(node); fixture.detectChanges(); - expect(vocabularyTreeviewServiceStub.loadMore).toHaveBeenCalledWith(node.item, true); + expect(vocabularyTreeviewServiceStub.loadMore).toHaveBeenCalledWith(node.item, [], true); }); it('should emit select event', () => { @@ -188,7 +181,7 @@ describe('VocabularyTreeviewComponent test suite', () => { comp.nodeMap.set('test', new TreeviewFlatNode(item)); comp.search(); fixture.detectChanges(); - expect(vocabularyTreeviewServiceStub.searchByQuery).toHaveBeenCalledWith('test search'); + expect(vocabularyTreeviewServiceStub.searchByQuery).toHaveBeenCalledWith('test search', []); expect(comp.storedNodeMap).toEqual(nodeMap); expect(comp.nodeMap).toEqual(emptyNodeMap); }); @@ -199,7 +192,7 @@ describe('VocabularyTreeviewComponent test suite', () => { comp.storedNodeMap.set('test', new TreeviewFlatNode(item2)); comp.search(); fixture.detectChanges(); - expect(vocabularyTreeviewServiceStub.searchByQuery).toHaveBeenCalledWith('test search'); + expect(vocabularyTreeviewServiceStub.searchByQuery).toHaveBeenCalledWith('test search', []); expect(comp.storedNodeMap).toEqual(storedNodeMap); expect(comp.nodeMap).toEqual(emptyNodeMap); }); @@ -229,6 +222,50 @@ describe('VocabularyTreeviewComponent test suite', () => { expect(vocabularyTreeviewServiceStub.cleanTree).toHaveBeenCalled(); }); }); + + describe('', () => { + beforeEach(() => { + vocabularyTreeviewServiceStub.getData.and.returnValue(observableOf([ + { + 'item': { + 'id': 'srsc:SCB11', + 'display': 'HUMANITIES and RELIGION' + } + } as TreeviewNode, + { + 'item': { + 'id': 'srsc:SCB12', + 'display': 'LAW/JURISPRUDENCE' + } + } as TreeviewNode, + { + 'item': { + 'id': 'srsc:SCB13', + 'display': 'SOCIAL SCIENCES' + } + } as TreeviewNode, + ])); + fixture = TestBed.createComponent(VocabularyTreeviewComponent); + comp = fixture.componentInstance; + compAsAny = comp; + comp.vocabularyOptions = vocabularyOptions; + comp.selectedItems = []; + de = fixture.debugElement; + }); + + it('should not display checkboxes by default', async () => { + fixture.detectChanges(); + expect(de.query(By.css('input[type=checkbox]'))).toBeNull(); + expect(de.queryAll(By.css('cdk-tree-node')).length).toEqual(3); + }); + + it('should display checkboxes if multiSelect is true', async () => { + comp.multiSelect = true; + fixture.detectChanges(); + expect(de.queryAll(By.css('input[type=checkbox]')).length).toEqual(3); + expect(de.queryAll(By.css('cdk-tree-node')).length).toEqual(3); + }); + }); }); // declare a test component diff --git a/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.ts b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.ts index 37da56caf2d..a78195a6811 100644 --- a/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.ts +++ b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.ts @@ -1,10 +1,7 @@ import { FlatTreeControl } from '@angular/cdk/tree'; -import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; +import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges } from '@angular/core'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { map } from 'rxjs/operators'; import { Observable, Subscription } from 'rxjs'; -import { Store } from '@ngrx/store'; import { TranslateService } from '@ngx-translate/core'; import { VocabularyEntryDetail } from '../../../core/submission/vocabularies/models/vocabulary-entry-detail.model'; @@ -16,19 +13,20 @@ import { PageInfo } from '../../../core/shared/page-info.model'; import { VocabularyEntry } from '../../../core/submission/vocabularies/models/vocabulary-entry.model'; import { VocabularyTreeFlattener } from './vocabulary-tree-flattener'; import { VocabularyTreeFlatDataSource } from './vocabulary-tree-flat-data-source'; -import { CoreState } from '../../../core/core-state.model'; -import { lowerCase } from 'lodash/string'; +import { VocabularyService } from '../../../core/submission/vocabularies/vocabulary.service'; import { FormFieldMetadataValueObject } from '../builder/models/form-field-metadata-value.model'; +export type VocabularyTreeItemType = FormFieldMetadataValueObject | VocabularyEntry | VocabularyEntryDetail; + /** - * Component that show a hierarchical vocabulary in a tree view + * Component that shows a hierarchical vocabulary in a tree view */ @Component({ selector: 'ds-vocabulary-treeview', templateUrl: './vocabulary-treeview.component.html', styleUrls: ['./vocabulary-treeview.component.scss'] }) -export class VocabularyTreeviewComponent implements OnDestroy, OnInit { +export class VocabularyTreeviewComponent implements OnDestroy, OnInit, OnChanges { /** * The {@link VocabularyOptions} object @@ -41,14 +39,24 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit { @Input() preloadLevel = 2; /** - * The vocabulary entry already selected, if any + * Contain a descriptive message for the tree + */ + @Input() description = ''; + + /** + * Whether to allow selecting multiple values with checkboxes + */ + @Input() multiSelect = false; + + /** + * The vocabulary entries already selected, if any */ - @Input() selectedItem: any = null; + @Input() showAdd = true; /** - * Contain a descriptive message for this vocabulary retrieved from i18n files + * The vocabulary entries already selected, if any */ - description: Observable; + @Input() selectedItems: VocabularyTreeItemType[] = []; /** * A map containing the current node showed by the tree @@ -87,14 +95,15 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit { /** * An event fired when a vocabulary entry is selected. - * Event's payload equals to {@link VocabularyEntryDetail} selected. + * Event's payload equals to {@link VocabularyTreeItemType} selected. */ - @Output() select: EventEmitter = new EventEmitter(null); + @Output() select: EventEmitter = new EventEmitter(null); /** - * A boolean representing if user is authenticated + * An event fired when a vocabulary entry is deselected. + * Event's payload equals to {@link VocabularyTreeItemType} deselected. */ - private isAuthenticated: Observable; + @Output() deselect: EventEmitter = new EventEmitter(null); /** * Array to track all subscriptions and unsubscribe them onDestroy @@ -104,15 +113,13 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit { /** * Initialize instance variables * - * @param {NgbActiveModal} activeModal * @param {VocabularyTreeviewService} vocabularyTreeviewService - * @param {Store} store + * @param {vocabularyService} vocabularyService * @param {TranslateService} translate */ constructor( - public activeModal: NgbActiveModal, private vocabularyTreeviewService: VocabularyTreeviewService, - private store: Store, + private vocabularyService: VocabularyService, private translate: TranslateService ) { this.treeFlattener = new VocabularyTreeFlattener(this.transformer, this.getLevel, @@ -135,7 +142,8 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit { * @param level The node level information */ transformer = (node: TreeviewNode, level: number) => { - const existingNode = this.nodeMap.get(node.item.id); + const entryId = this.getEntryId(node.item); + const existingNode = this.nodeMap.get(entryId); if (existingNode && existingNode.item.id !== LOAD_MORE && existingNode.item.id !== LOAD_MORE_ROOT) { return existingNode; @@ -148,9 +156,10 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit { node.pageInfo, node.loadMoreParentItem, node.isSearchNode, - node.isInInitValueHierarchy + node.isInInitValueHierarchy, + node.isSelected ); - this.nodeMap.set(node.item.id, newNode); + this.nodeMap.set(entryId, newNode); if ((((level + 1) < this.preloadLevel) && newNode.childrenLoaded) || (newNode.isSearchNode && newNode.childrenLoaded) @@ -203,15 +212,10 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit { }) ); - this.translate.get(`search.filters.filter.${this.vocabularyOptions.name}.head`).pipe( - map((type) => lowerCase(type)), - ).subscribe( - (type) => this.description = this.translate.get('vocabulary-treeview.info', { type }) - ); - this.loading = this.vocabularyTreeviewService.isLoading(); - this.vocabularyTreeviewService.initialize(this.vocabularyOptions, new PageInfo(), null); + const entryId: string = (this.selectedItems?.length > 0) ? this.getEntryId(this.selectedItems[0]) : null; + this.vocabularyTreeviewService.initialize(this.vocabularyOptions, new PageInfo(), this.getSelectedEntryIds(), entryId); } /** @@ -219,7 +223,7 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit { * @param item The VocabularyEntryDetail for which to load more nodes */ loadMore(item: VocabularyEntryDetail) { - this.vocabularyTreeviewService.loadMore(item); + this.vocabularyTreeviewService.loadMore(item, this.getSelectedEntryIds()); } /** @@ -227,7 +231,7 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit { * @param node The TreeviewFlatNode for which to load more nodes */ loadMoreRoot(node: TreeviewFlatNode) { - this.vocabularyTreeviewService.loadMoreRoot(node); + this.vocabularyTreeviewService.loadMoreRoot(node, this.getSelectedEntryIds()); } /** @@ -235,17 +239,20 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit { * @param node The TreeviewFlatNode for which to load children nodes */ loadChildren(node: TreeviewFlatNode) { - this.vocabularyTreeviewService.loadMore(node.item, true); + this.vocabularyTreeviewService.loadMore(node.item, this.getSelectedEntryIds(), true); } /** - * Method called on entry select - * Emit a new select Event + * Method called on entry select/deselect */ - onSelect(entry: VocabularyEntryDetail) { - const value = new FormFieldMetadataValueObject(entry.value, null, entry.securityLevel, entry.id, entry.display); - this.select.emit(value); - this.activeModal.close(value); + onSelect(item: VocabularyEntryDetail) { + if (!this.getSelectedEntryIds().includes(this.getEntryId(item))) { + this.selectedItems.push(item); + this.select.emit(item); + } else { + this.selectedItems = this.selectedItems.filter((detail: VocabularyTreeItemType) => this.getEntryId(detail) !== this.getEntryId(item)); + this.deselect.emit(item); + } } /** @@ -257,7 +264,7 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit { this.storedNodeMap = this.nodeMap; } this.nodeMap = new Map(); - this.vocabularyTreeviewService.searchByQuery(this.searchText); + this.vocabularyTreeviewService.searchByQuery(this.searchText, this.getSelectedEntryIds()); } } @@ -272,15 +279,32 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit { * Reset tree resulting from a previous search */ reset() { + this.searchText = ''; + for (const item of this.selectedItems) { + this.deselect.emit(item); + this.nodeMap.get(this.getEntryId(item)).isSelected = false; + } + this.selectedItems = []; + if (isNotEmpty(this.storedNodeMap)) { this.nodeMap = this.storedNodeMap; this.storedNodeMap = new Map(); this.vocabularyTreeviewService.restoreNodes(); } + } - this.searchText = ''; + add() { + const userVocabularyEntry = new FormFieldMetadataValueObject( + this.searchText, + null, + null, + null, + this.searchText + ); + this.select.emit(userVocabularyEntry); } + /** * Unsubscribe from all subscriptions */ @@ -292,9 +316,28 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit { } /** - * Return an id for a given {@link VocabularyEntry} + * Return an id for a given {@link VocabularyTreeItemType} + */ + private getEntryId(entry: VocabularyTreeItemType): string { + const entryId: string = entry?.authority || entry?.otherInformation?.id || (entry as any)?.id || undefined; + return entryId?.startsWith(this.vocabularyOptions.name) ? entryId.replace(`${this.vocabularyOptions.name}:`, '') : entryId; + } + + /** + * Return an ids for all selected entries */ - private getEntryId(entry: VocabularyEntry): string { - return entry.authority || entry?.otherInformation?.id || undefined; + private getSelectedEntryIds(): string[] { + return this.selectedItems + .map((entry: VocabularyTreeItemType) => this.getEntryId(entry)) + .filter((value) => isNotEmpty(value)); + } + + ngOnChanges(changes: SimpleChanges): void { + if (!changes.vocabularyOptions.isFirstChange() && changes.vocabularyOptions.currentValue !== changes.vocabularyOptions.previousValue) { + this.selectedItems = []; + this.searchText = ''; + this.vocabularyTreeviewService.cleanTree(); + this.vocabularyTreeviewService.initialize(this.vocabularyOptions, new PageInfo(), this.getSelectedEntryIds(), null); + } } } diff --git a/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.service.spec.ts b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.service.spec.ts index ccee570f159..499a3ed098b 100644 --- a/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.service.spec.ts +++ b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.service.spec.ts @@ -192,7 +192,7 @@ describe('VocabularyTreeviewService test suite', () => { a: createSuccessfulRemoteDataObject(buildPaginatedList(pageInfo, [item, item2, item3])) })); - scheduler.schedule(() => service.initialize(vocabularyOptions, pageInfo)); + scheduler.schedule(() => service.initialize(vocabularyOptions, pageInfo, [])); scheduler.flush(); expect(serviceAsAny.vocabularyName).toEqual(vocabularyOptions.name); @@ -214,7 +214,7 @@ describe('VocabularyTreeviewService test suite', () => { b: createSuccessfulRemoteDataObject(item) }) ); - scheduler.schedule(() => service.initialize(vocabularyOptions, pageInfo, 'root2')); + scheduler.schedule(() => service.initialize(vocabularyOptions, pageInfo, [], 'root2')); scheduler.flush(); expect(serviceAsAny.vocabularyName).toEqual(vocabularyOptions.name); @@ -233,11 +233,11 @@ describe('VocabularyTreeviewService test suite', () => { describe('loadMoreRoot', () => { it('should call retrieveTopNodes properly', () => { spyOn(serviceAsAny, 'retrieveTopNodes'); - service.initialize(vocabularyOptions, new PageInfo()); + service.initialize(vocabularyOptions, new PageInfo(), []); serviceAsAny.dataChange.next(treeNodeListWithLoadMoreRoot); - service.loadMoreRoot(loadMoreRootFlatNode); + service.loadMoreRoot(loadMoreRootFlatNode, []); - expect(serviceAsAny.retrieveTopNodes).toHaveBeenCalledWith(loadMoreRootFlatNode.pageInfo, treeNodeList); + expect(serviceAsAny.retrieveTopNodes).toHaveBeenCalledWith(loadMoreRootFlatNode.pageInfo, treeNodeList, []); }); }); @@ -263,7 +263,7 @@ describe('VocabularyTreeviewService test suite', () => { serviceAsAny.nodeMap = nodeMapWithChildren; treeNodeListWithChildren.push(new TreeviewNode(child2, false, new PageInfo(), item)); - scheduler.schedule(() => service.loadMore(item)); + scheduler.schedule(() => service.loadMore(item, [])); scheduler.flush(); expect(serviceAsAny.dataChange.value).toEqual(treeNodeListWithChildren); @@ -285,7 +285,7 @@ describe('VocabularyTreeviewService test suite', () => { treeNodeListWithChildren.push(childNode2); treeNodeListWithChildren.push(loadMoreNode); - scheduler.schedule(() => service.loadMore(item)); + scheduler.schedule(() => service.loadMore(item, [])); scheduler.flush(); expect(serviceAsAny.dataChange.value).toEqual(treeNodeListWithChildren); @@ -319,7 +319,7 @@ describe('VocabularyTreeviewService test suite', () => { ); vocabularyOptions.query = 'root1-child1-child1'; - scheduler.schedule(() => service.searchByQuery(vocabularyOptions)); + scheduler.schedule(() => service.searchByQuery(vocabularyOptions, [])); scheduler.flush(); // We can't check the tree by comparing root TreeviewNodes directly in this particular test; diff --git a/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.service.ts b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.service.ts index 6f877e91a50..3843b0cd221 100644 --- a/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.service.ts +++ b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.service.ts @@ -4,28 +4,22 @@ import { BehaviorSubject, Observable, of as observableOf } from 'rxjs'; import { map, merge, mergeMap, scan } from 'rxjs/operators'; import findIndex from 'lodash/findIndex'; -import { - LOAD_MORE_NODE, - LOAD_MORE_ROOT_NODE, - TreeviewFlatNode, - TreeviewNode -} from './vocabulary-treeview-node.model'; +import { LOAD_MORE_NODE, LOAD_MORE_ROOT_NODE, TreeviewFlatNode, TreeviewNode } from './vocabulary-treeview-node.model'; import { VocabularyEntry } from '../../../core/submission/vocabularies/models/vocabulary-entry.model'; import { VocabularyService } from '../../../core/submission/vocabularies/vocabulary.service'; import { PageInfo } from '../../../core/shared/page-info.model'; import { isEmpty, isNotEmpty } from '../../empty.util'; import { VocabularyOptions } from '../../../core/submission/vocabularies/models/vocabulary-options.model'; -import { - getFirstSucceededRemoteDataPayload, - getFirstSucceededRemoteListPayload -} from '../../../core/shared/operators'; +import { getFirstSucceededRemoteDataPayload, getFirstSucceededRemoteListPayload } from '../../../core/shared/operators'; import { PaginatedList } from '../../../core/data/paginated-list.model'; import { VocabularyEntryDetail } from '../../../core/submission/vocabularies/models/vocabulary-entry-detail.model'; /** * A service that provides methods to deal with vocabulary tree */ -@Injectable() +@Injectable({ + providedIn: 'root' +}) export class VocabularyTreeviewService { /** @@ -101,21 +95,22 @@ export class VocabularyTreeviewService { * * @param options The {@link VocabularyOptions} object * @param pageInfo The {@link PageInfo} object + * @param selectedItems The currently selected items * @param initValueId The entry id of the node to mark as selected, if any */ - initialize(options: VocabularyOptions, pageInfo: PageInfo, initValueId?: string): void { + initialize(options: VocabularyOptions, pageInfo: PageInfo, selectedItems: string[], initValueId?: string): void { this.loading.next(true); this.vocabularyOptions = options; this.vocabularyName = options.name; this.pageInfo = pageInfo; if (isNotEmpty(initValueId)) { - this.getNodeHierarchyById(initValueId) + this.getNodeHierarchyById(initValueId, selectedItems) .subscribe((hierarchy: string[]) => { this.initValueHierarchy = hierarchy; - this.retrieveTopNodes(pageInfo, []); + this.retrieveTopNodes(pageInfo, [], selectedItems); }); } else { - this.retrieveTopNodes(pageInfo, []); + this.retrieveTopNodes(pageInfo, [], selectedItems); } } @@ -129,19 +124,21 @@ export class VocabularyTreeviewService { /** * Expand the root node whose children are not loaded * @param node The root node + * @param selectedItems The currently selected items */ - loadMoreRoot(node: TreeviewFlatNode) { + loadMoreRoot(node: TreeviewFlatNode, selectedItems: string[]) { const nodes = this.dataChange.value; nodes.pop(); - this.retrieveTopNodes(node.pageInfo, nodes); + this.retrieveTopNodes(node.pageInfo, nodes, selectedItems); } /** * Expand a node whose children are not loaded * @param item + * @param selectedItems * @param onlyFirstTime */ - loadMore(item: VocabularyEntryDetail, onlyFirstTime = false) { + loadMore(item: VocabularyEntryDetail, selectedItems: string[], onlyFirstTime = false) { if (!this.nodeMap.has(item.otherInformation.id)) { return; } @@ -153,7 +150,7 @@ export class VocabularyTreeviewService { return; } - const newNodes: TreeviewNode[] = list.page.map((entry) => this._generateNode(entry)); + const newNodes: TreeviewNode[] = list.page.map((entry) => this._generateNode(entry, selectedItems)); if (newNodes.length > 0) { children.pop(); children.push(...newNodes); @@ -185,7 +182,7 @@ export class VocabularyTreeviewService { /** * Perform a search operation by query */ - searchByQuery(query: string) { + searchByQuery(query: string, selectedItems: string[]) { this.loading.next(true); if (isEmpty(this.storedNodes)) { this.storedNodes = this.dataChange.value; @@ -202,7 +199,7 @@ export class VocabularyTreeviewService { getFirstSucceededRemoteDataPayload() ) ), - mergeMap((entry: VocabularyEntryDetail) => this.getNodeHierarchy(entry)), + mergeMap((entry: VocabularyEntryDetail) => this.getNodeHierarchy(entry, selectedItems)), scan((acc: TreeviewNode[], value: TreeviewNode) => { if (isEmpty(value) || findIndex(acc, (node) => node.item.otherInformation.id === value.item.otherInformation.id) !== -1) { return acc; @@ -233,11 +230,12 @@ export class VocabularyTreeviewService { * Generate a {@link TreeviewNode} object from vocabulary entry * * @param entry The vocabulary entry detail + * @param selectedItems An array containing the currently selected items * @param isSearchNode A Boolean representing if given entry is the result of a search * @param toStore A Boolean representing if the node created is to store or not * @return TreeviewNode */ - private _generateNode(entry: VocabularyEntryDetail, isSearchNode = false, toStore = true): TreeviewNode { + private _generateNode(entry: VocabularyEntryDetail, selectedItems: string[], isSearchNode = false, toStore = true): TreeviewNode { const entryId = entry.otherInformation.id; if (this.nodeMap.has(entryId)) { return this.nodeMap.get(entryId)!; @@ -245,13 +243,15 @@ export class VocabularyTreeviewService { const hasChildren = entry.hasOtherInformation() && (entry.otherInformation as any)!.hasChildren === 'true'; const pageInfo: PageInfo = this.pageInfo; const isInInitValueHierarchy = this.initValueHierarchy.includes(entryId); + const isSelected: boolean = selectedItems.some(() => selectedItems.includes(entryId)); const result = new TreeviewNode( entry, hasChildren, pageInfo, null, isSearchNode, - isInInitValueHierarchy); + isInInitValueHierarchy, + isSelected); if (toStore) { this.nodeMap.set(entryId, result); @@ -262,12 +262,13 @@ export class VocabularyTreeviewService { /** * Return the node Hierarchy by a given node's id * @param id The node id + * @param selectedItems The currently selected items * @return Observable */ - private getNodeHierarchyById(id: string): Observable { + private getNodeHierarchyById(id: string, selectedItems: string[]): Observable { return this.getById(id).pipe( - mergeMap((entry: VocabularyEntryDetail) => this.getNodeHierarchy(entry, [], false)), - map((node: TreeviewNode) => this.getNodeHierarchyIds(node)) + mergeMap((entry: VocabularyEntryDetail) => this.getNodeHierarchy(entry, selectedItems,[], false)), + map((node: TreeviewNode) => this.getNodeHierarchyIds(node, selectedItems)) ); } @@ -308,13 +309,14 @@ export class VocabularyTreeviewService { * Retrieve the top level vocabulary entries * @param pageInfo The {@link PageInfo} object * @param nodes The top level nodes already loaded, if any + * @param selectedItems The currently selected items */ - private retrieveTopNodes(pageInfo: PageInfo, nodes: TreeviewNode[]): void { + private retrieveTopNodes(pageInfo: PageInfo, nodes: TreeviewNode[], selectedItems: string[]): void { this.vocabularyService.searchTopEntries(this.vocabularyName, pageInfo).pipe( getFirstSucceededRemoteDataPayload() ).subscribe((list: PaginatedList) => { this.vocabularyService.clearSearchTopRequests(); - const newNodes: TreeviewNode[] = list.page.map((entry: VocabularyEntryDetail) => this._generateNode(entry)); + const newNodes: TreeviewNode[] = list.page.map((entry: VocabularyEntryDetail) => this._generateNode(entry, selectedItems)); nodes.push(...newNodes); if ((list.pageInfo.currentPage + 1) <= list.pageInfo.totalPages) { @@ -336,15 +338,16 @@ export class VocabularyTreeviewService { * Build and return the tree node hierarchy by a given vocabulary entry * * @param item The vocabulary entry + * @param selectedItems The currently selected items * @param children The vocabulary entry * @param toStore A Boolean representing if the node created is to store or not * @return Observable */ - private getNodeHierarchy(item: VocabularyEntryDetail, children?: TreeviewNode[], toStore = true): Observable { + private getNodeHierarchy(item: VocabularyEntryDetail, selectedItems: string[], children?: TreeviewNode[], toStore = true): Observable { if (isEmpty(item)) { return observableOf(null); } - const node = this._generateNode(item, toStore, toStore); + const node = this._generateNode(item, selectedItems, toStore, toStore); if (isNotEmpty(children)) { const newChildren = children @@ -359,7 +362,7 @@ export class VocabularyTreeviewService { if (node.item.hasOtherInformation() && isNotEmpty(node.item.otherInformation.parent)) { return this.getParentNode(node.item.otherInformation.id).pipe( - mergeMap((parentItem: VocabularyEntryDetail) => this.getNodeHierarchy(parentItem, [node], toStore)) + mergeMap((parentItem: VocabularyEntryDetail) => this.getNodeHierarchy(parentItem, selectedItems, [node], toStore)) ); } else { return observableOf(node); @@ -370,15 +373,16 @@ export class VocabularyTreeviewService { * Build and return the node Hierarchy ids by a given node * * @param node The given node + * @param selectedItems The currently selected items * @param hierarchyIds The ids already present in the Hierarchy's array * @return string[] */ - private getNodeHierarchyIds(node: TreeviewNode, hierarchyIds: string[] = []): string[] { + private getNodeHierarchyIds(node: TreeviewNode, selectedItems: string[], hierarchyIds: string[] = []): string[] { if (!hierarchyIds.includes(node.item.otherInformation.id)) { hierarchyIds.push(node.item.otherInformation.id); } if (isNotEmpty(node.children)) { - return this.getNodeHierarchyIds(node.children[0], hierarchyIds); + return this.getNodeHierarchyIds(node.children[0], selectedItems, hierarchyIds); } else { return hierarchyIds; } diff --git a/src/app/shared/handle.service.spec.ts b/src/app/shared/handle.service.spec.ts index b326eb04163..8203940c6ad 100644 --- a/src/app/shared/handle.service.spec.ts +++ b/src/app/shared/handle.service.spec.ts @@ -1,47 +1,88 @@ -import { HandleService } from './handle.service'; +import { HandleService, CANONICAL_PREFIX_KEY } from './handle.service'; +import { TestBed } from '@angular/core/testing'; +import { ConfigurationDataServiceStub } from './testing/configuration-data.service.stub'; +import { ConfigurationDataService } from '../core/data/configuration-data.service'; +import { createSuccessfulRemoteDataObject$ } from './remote-data.utils'; +import { ConfigurationProperty } from '../core/shared/configuration-property.model'; describe('HandleService', () => { let service: HandleService; + let configurationService: ConfigurationDataServiceStub; + beforeEach(() => { - service = new HandleService(); + configurationService = new ConfigurationDataServiceStub(); + + TestBed.configureTestingModule({ + providers: [ + { provide: ConfigurationDataService, useValue: configurationService }, + ], + }); + service = TestBed.inject(HandleService); }); describe(`normalizeHandle`, () => { - it(`should simply return an already normalized handle`, () => { - let input, output; - - input = '123456789/123456'; - output = service.normalizeHandle(input); - expect(output).toEqual(input); + it('should normalize a handle url with custom conical prefix with trailing slash', (done: DoneFn) => { + spyOn(configurationService, 'findByPropertyName').and.returnValue(createSuccessfulRemoteDataObject$({ + ... new ConfigurationProperty(), + name: CANONICAL_PREFIX_KEY, + values: ['https://hdl.handle.net/'], + })); - input = '12.3456.789/123456'; - output = service.normalizeHandle(input); - expect(output).toEqual(input); + service.normalizeHandle('https://hdl.handle.net/123456789/123456').subscribe((handle: string | null) => { + expect(handle).toBe('123456789/123456'); + done(); + }); }); - it(`should normalize a handle url`, () => { - let input, output; + it('should normalize a handle url with custom conical prefix without trailing slash', (done: DoneFn) => { + spyOn(configurationService, 'findByPropertyName').and.returnValue(createSuccessfulRemoteDataObject$({ + ... new ConfigurationProperty(), + name: CANONICAL_PREFIX_KEY, + values: ['https://hdl.handle.net/'], + })); - input = 'https://hdl.handle.net/handle/123456789/123456'; - output = service.normalizeHandle(input); - expect(output).toEqual('123456789/123456'); + service.normalizeHandle('https://hdl.handle.net/123456789/123456').subscribe((handle: string | null) => { + expect(handle).toBe('123456789/123456'); + done(); + }); + }); + + describe('should simply return an already normalized handle', () => { + it('123456789/123456', (done: DoneFn) => { + service.normalizeHandle('123456789/123456').subscribe((handle: string | null) => { + expect(handle).toBe('123456789/123456'); + done(); + }); + }); - input = 'https://rest.api/server/handle/123456789/123456'; - output = service.normalizeHandle(input); - expect(output).toEqual('123456789/123456'); + it('12.3456.789/123456', (done: DoneFn) => { + service.normalizeHandle('12.3456.789/123456').subscribe((handle: string | null) => { + expect(handle).toBe('12.3456.789/123456'); + done(); + }); + }); }); - it(`should return null if the input doesn't contain a handle`, () => { - let input, output; + it('should normalize handle urls starting with handle', (done: DoneFn) => { + service.normalizeHandle('https://rest.api/server/handle/123456789/123456').subscribe((handle: string | null) => { + expect(handle).toBe('123456789/123456'); + done(); + }); + }); - input = 'https://hdl.handle.net/handle/123456789'; - output = service.normalizeHandle(input); - expect(output).toBeNull(); + it('should return null if the input doesn\'t contain a valid handle', (done: DoneFn) => { + service.normalizeHandle('https://hdl.handle.net/123456789').subscribe((handle: string | null) => { + expect(handle).toBeNull(); + done(); + }); + }); - input = 'something completely different'; - output = service.normalizeHandle(input); - expect(output).toBeNull(); + it('should return null if the input doesn\'t contain a handle', (done: DoneFn) => { + service.normalizeHandle('something completely different').subscribe((handle: string | null) => { + expect(handle).toBeNull(); + done(); + }); }); }); }); diff --git a/src/app/shared/handle.service.ts b/src/app/shared/handle.service.ts index da0f17f7de3..1f22c7d3045 100644 --- a/src/app/shared/handle.service.ts +++ b/src/app/shared/handle.service.ts @@ -1,7 +1,18 @@ import { Injectable } from '@angular/core'; -import { isNotEmpty, isEmpty } from './empty.util'; +import { isEmpty, hasNoValue } from './empty.util'; +import { ConfigurationDataService } from '../core/data/configuration-data.service'; +import { getFirstCompletedRemoteData } from '../core/shared/operators'; +import { map, take } from 'rxjs/operators'; +import { ConfigurationProperty } from '../core/shared/configuration-property.model'; +import { Observable, of as observableOf } from 'rxjs'; +import { RemoteData } from '../core/data/remote-data'; -const PREFIX_REGEX = /handle\/([^\/]+\/[^\/]+)$/; +export const CANONICAL_PREFIX_KEY = 'handle.canonical.prefix'; + +const PREFIX_REGEX = (prefix: string | undefined) => { + const formattedPrefix: string = prefix?.replace(/\/$/, ''); + return new RegExp(`(${formattedPrefix ? formattedPrefix + '|' : '' }handle)\/([^\/]+\/[^\/]+)$`); +}; const NO_PREFIX_REGEX = /^([^\/]+\/[^\/]+)$/; @Injectable({ @@ -9,33 +20,57 @@ const NO_PREFIX_REGEX = /^([^\/]+\/[^\/]+)$/; }) export class HandleService { + constructor( + protected configurationService: ConfigurationDataService, + ) { + } /** * Turns a handle string into the default 123456789/12345 format * - * @param handle the input handle + * When the handle.canonical.prefix doesn't end with handle, be sure to expose the variable so that the + * frontend can find the handle * - * normalizeHandle('123456789/123456') // '123456789/123456' - * normalizeHandle('12.3456.789/123456') // '12.3456.789/123456' - * normalizeHandle('https://hdl.handle.net/handle/123456789/123456') // '123456789/123456' - * normalizeHandle('https://rest.api/server/handle/123456789/123456') // '123456789/123456' - * normalizeHandle('https://rest.api/server/handle/123456789') // null + * @param handle the input handle + * @return + *
      + *
    • normalizeHandle('123456789/123456') // '123456789/123456'
    • + *
    • normalizeHandle('12.3456.789/123456') // '12.3456.789/123456'
    • + *
    • normalizeHandle('https://hdl.handle.net/123456789/123456') // '123456789/123456'
    • + *
    • normalizeHandle('https://rest.api/server/handle/123456789/123456') // '123456789/123456'
    • + *
    • normalizeHandle('https://rest.api/server/handle/123456789') // null
    • + *
    */ - normalizeHandle(handle: string): string { - let matches: string[]; - if (isNotEmpty(handle)) { - matches = handle.match(PREFIX_REGEX); + normalizeHandle(handle: string): Observable { + if (hasNoValue(handle)) { + return observableOf(null); } + return this.configurationService.findByPropertyName(CANONICAL_PREFIX_KEY).pipe( + getFirstCompletedRemoteData(), + map((configurationPropertyRD: RemoteData) => { + if (configurationPropertyRD.hasSucceeded) { + return configurationPropertyRD.payload.values.length >= 1 ? configurationPropertyRD.payload.values[0] : undefined; + } else { + return undefined; + } + }), + map((prefix: string | undefined) => { + let matches: string[]; - if (isEmpty(matches) || matches.length < 2) { - matches = handle.match(NO_PREFIX_REGEX); - } + matches = handle.match(PREFIX_REGEX(prefix)); - if (isEmpty(matches) || matches.length < 2) { - return null; - } else { - return matches[1]; - } + if (isEmpty(matches) || matches.length < 3) { + matches = handle.match(NO_PREFIX_REGEX); + } + + if (isEmpty(matches) || matches.length < 2) { + return null; + } else { + return matches[matches.length - 1]; + } + }), + take(1), + ); } } diff --git a/src/app/shared/impersonate-navbar/impersonate-navbar.component.html b/src/app/shared/impersonate-navbar/impersonate-navbar.component.html index 9f2b66694b7..9581ba8ea87 100644 --- a/src/app/shared/impersonate-navbar/impersonate-navbar.component.html +++ b/src/app/shared/impersonate-navbar/impersonate-navbar.component.html @@ -1,4 +1,4 @@ -