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..6f123fe8b86 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -7,7 +7,12 @@ "eslint-plugin-jsdoc", "eslint-plugin-deprecation", "unused-imports", +<<<<<<< HEAD "eslint-plugin-lodash" +======= + "eslint-plugin-lodash", + "eslint-plugin-jsonc" +>>>>>>> dspace-7.6.1 ], "overrides": [ { @@ -224,6 +229,45 @@ "@angular-eslint/template/no-negated-async": "off", "@angular-eslint/template/eqeqeq": "off" } +<<<<<<< HEAD +======= + }, + { + "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 + } + ] + } +>>>>>>> dspace-7.6.1 } ] } diff --git a/.gitattributes b/.gitattributes index f5e28069871..c4797c9ece5 100644 --- a/.gitattributes +++ b/.gitattributes @@ -13,4 +13,8 @@ *.css eol=lf *.scss eol=lf *.html eol=lf +<<<<<<< HEAD *.svg eol=lf +======= +*.svg eol=lf +>>>>>>> dspace-7.6.1 diff --git a/.github/disabled-workflows/issue_opened.yml b/.github/disabled-workflows/issue_opened.yml index 5d7c1c30f7d..97f77063aa2 100644 --- a/.github/disabled-workflows/issue_opened.yml +++ b/.github/disabled-workflows/issue_opened.yml @@ -16,7 +16,11 @@ 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) == '') +<<<<<<< HEAD:.github/disabled-workflows/issue_opened.yml uses: actions/add-to-project@v0.3.0 +======= + uses: actions/add-to-project@v0.5.0 +>>>>>>> dspace-7.6.1:.github/workflows/issue_opened.yml # 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/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/pull_request_template.md b/.github/pull_request_template.md index 76ff6196da6..5a2b7cb2201 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,3 +1,4 @@ +<<<<<<< HEAD | Phases | MP | MM | MB | MR | JM | Total | |-----------------|----:|----:|----:|-----:|-----:|-------:| | ETA | 0 | 0 | 0 | 0 | 0 | 0 | @@ -13,3 +14,33 @@ (Write here, if there is needed describe some specific problem. Erase it, when it is not needed.) ## Problems (Write here, if some unexpected problems occur during solving issues. Erase it, when it is not needed.) +======= +## References +_Add references/links to any related issues or PRs. These may include:_ +* Fixes #`issue-number` (if this fixes an issue ticket) +* Requires DSpace/DSpace#`pr-number` (if a REST API PR is required to test this) + +## Description +Short summary of changes (1-2 sentences). + +## Instructions for Reviewers +Please add a more detailed description of the changes made by your PR. At a minimum, providing a bulleted list of changes in your PR is helpful to reviewers. + +List of changes in this PR: +* First, ... +* Second, ... + +**Include guidance for how to test or review your PR.** This may include: steps to reproduce a bug, screenshots or description of a new feature, or reasons behind specific changes. + +## Checklist +_This checklist provides a reminder of what we are going to look for when reviewing your PR. You need not complete this checklist prior to creating your PR (draft PRs are always welcome). If you are unsure about an item in the checklist, don't hesitate to ask. We're here to help!_ + +- [ ] My PR is small in size (e.g. less than 1,000 lines of code, not including comments & specs/tests), or I have provided reasons as to why that's not possible. +- [ ] My PR passes [ESLint](https://eslint.org/) validation using `yarn lint` +- [ ] My PR doesn't introduce circular dependencies (verified via `yarn check-circ-deps`) +- [ ] My PR includes [TypeDoc](https://typedoc.org/) comments for _all new (or modified) public methods and classes_. It also includes TypeDoc for large or complex private methods. +- [ ] My PR passes all specs/tests and includes new/updated specs or tests based on the [Code Testing Guide](https://wiki.lyrasis.org/display/DSPACE/Code+Testing+Guide). +- [ ] If my PR includes new libraries/dependencies (in `package.json`), I've made sure their licenses align with the [DSpace BSD License](https://github.com/DSpace/DSpace/blob/main/LICENSE) based on the [Licensing of Contributions](https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines#CodeContributionGuidelines-LicensingofContributions) documentation. +- [ ] If my PR includes new features or configurations, I've provided basic technical documentation in the PR itself. +- [ ] If my PR fixes an issue ticket, I've [linked them together](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue). +>>>>>>> dspace-7.6.1 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 53bf70dcd84..a3f10f3fa7d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,23 +13,43 @@ on: permissions: contents: read # to fetch code (actions/checkout) +permissions: + contents: read # to fetch code (actions/checkout) + jobs: tests: runs-on: ubuntu-latest 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. +<<<<<<< HEAD # Spin up UI on 127.0.0.1 to avoid host resolution issues in e2e tests with Node 18+ DSPACE_CI_IMAGE: 'dataquest/dspace:dspace-7_x-test' +======= + # NOTE: These settings should be kept in sync with those in [src]/docker/docker-compose-ci.yml +>>>>>>> dspace-7.6.1 DSPACE_REST_HOST: 127.0.0.1 DSPACE_REST_PORT: 8080 DSPACE_REST_NAMESPACE: '/server' DSPACE_REST_SSL: false +<<<<<<< HEAD + DSPACE_UI_HOST: 127.0.0.1 + DSPACE_UI_PORT: 4000 +======= + # 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 +>>>>>>> dspace-7.6.1 # When Chrome version is specified, we pin to a specific version of Chrome # Comment this out to use the latest release #CHROME_VERSION: "90.0.4430.212-1" + # Bump Node heap size (OOM in CI after upgrading to Angular 15) + NODE_OPTIONS: '--max-old-space-size=4096' strategy: # Create a matrix of Node versions to test against (in parallel) matrix: @@ -67,7 +87,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: @@ -92,12 +112,24 @@ 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 +<<<<<<< HEAD # 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 +>>>>>>> dspace-7.6.1 # Using docker-compose start backend using CI configuration # and load assetstore from a cached copy @@ -111,11 +143,14 @@ 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) +<<<<<<< HEAD uses: cypress-io/github-action@v4 +======= + uses: cypress-io/github-action@v5 +>>>>>>> dspace-7.6.1 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 @@ -175,3 +210,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..fec8d9681b8 100644 --- a/.github/workflows/codescan.yml +++ b/.github/workflows/codescan.yml @@ -5,12 +5,25 @@ # because CodeQL requires a fresh build with all tests *disabled*. name: "Code Scanning" +<<<<<<< HEAD # Run this code scan for all pushes / PRs to main branch. Also run once a week. on: push: branches: [ main ] pull_request: branches: [ main ] +======= +# Run this code scan for all pushes / PRs to main or maintenance branches. Also run once a week. +on: + push: + branches: + - main + - 'dspace-**' + pull_request: + branches: + - main + - 'dspace-**' +>>>>>>> dspace-7.6.1 # 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 46d04a54764..aef395cc15d 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -13,7 +13,32 @@ on: permissions: contents: read # to fetch code (actions/checkout) +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: +<<<<<<< HEAD docker: # Ensure this job never runs on forked repos. It's only executed for our repo if: github.repository == 'dataquest-dev/dspace-angular' @@ -36,6 +61,15 @@ jobs: # 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' || '' }} +======= + ############################################### + # 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 +>>>>>>> dspace-7.6.1 steps: # https://github.com/actions/checkout @@ -59,9 +93,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 @@ -87,7 +118,66 @@ jobs: tags: ${{ steps.meta_build.outputs.tags }} labels: ${{ steps.meta_build.outputs.labels }} +<<<<<<< HEAD deploy: needs: docker uses: dataquest-dev/dspace-angular/.github/workflows/deploy.yml@dtq-dev secrets: inherit +======= + ############################################################# + # 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 }} +>>>>>>> dspace-7.6.1 diff --git a/.github/workflows/label_merge_conflicts.yml b/.github/workflows/label_merge_conflicts.yml new file mode 100644 index 00000000000..ccc6c401c0b --- /dev/null +++ b/.github/workflows/label_merge_conflicts.yml @@ -0,0 +1,39 @@ +# This workflow checks open PRs for merge conflicts and labels them when conflicts are found +name: Check for merge conflicts + +# Run this for all pushes (i.e. merges) to 'main' or maintenance branches +on: + push: + 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: + types: [ synchronize ] + +permissions: {} + +jobs: + triage: + # Ensure this job never runs on forked repos. It's only executed for 'dspace/dspace-angular' + if: github.repository == 'dspace/dspace-angular' + runs-on: ubuntu-latest + permissions: + pull-requests: write + 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@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 + with: + conflict_label_name: 'merge conflict' + github_token: ${{ secrets.GITHUB_TOKEN }} + conflict_comment: | + Hi @${author}, + Conflicts have been detected against the base branch. + Please [resolve these conflicts](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/addressing-merge-conflicts/about-merge-conflicts) as soon as you can. Thanks! \ No newline at end of file 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 482b09e6ea2..d56f267ef85 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,7 @@ package-lock.json .env /nbproject/ +<<<<<<< HEAD # import data python module python_data_import/debug.log.txt python_data_import/logs.txt @@ -45,3 +46,8 @@ python_data_import/date.txt */__pycache__/ junit.xml +======= +junit.xml + +/src/mirador-viewer/config.local.js +>>>>>>> dspace-7.6.1 diff --git a/Dockerfile b/Dockerfile index 31d51e6ce43..19bbe63e75b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,13 +13,28 @@ 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 +<<<<<<< HEAD RUN yarn install --network-timeout 2000000 +======= +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" +>>>>>>> dspace-7.6.1 # 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 +<<<<<<< HEAD # if you have a public IP. See https://github.com/DSpace/dspace-angular/issues/1485 RUN apk add tzdata RUN yarn build:prod RUN npm install pm2 -g CMD /bin/sh -c "pm2-runtime start dspace-ui.json > /dev/null 2> /dev/null" +======= +# 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 +>>>>>>> dspace-7.6.1 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 90c5c1b2353..a873db79d16 100644 --- a/README.md +++ b/README.md @@ -157,8 +157,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`** @@ -288,7 +288,11 @@ 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: +<<<<<<< HEAD 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. +>>>>>>> dspace-7.6.1 * 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: ``` @@ -413,8 +417,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 d828887b214..66269600f08 100644 --- a/angular.json +++ b/angular.json @@ -63,11 +63,15 @@ "bundleName": "dspace-theme" } ], +<<<<<<< HEAD "scripts": [ "src/license-selector.js", "src/license-selector-creation.js", "node_modules/bootstrap/dist/js/bootstrap.bundle.min.js" ], +======= + "scripts": [], +>>>>>>> dspace-7.6.1 "baseHref": "/" }, "configurations": { @@ -272,16 +276,34 @@ "options": { "lintFilePatterns": [ "src/**/*.ts", +<<<<<<< HEAD "src/**/*.html" +======= + "src/**/*.html", + "src/**/*.json5" +>>>>>>> dspace-7.6.1 ] } } } } }, - "defaultProject": "dspace-angular", "cli": { "analytics": false, +<<<<<<< HEAD "defaultCollection": "@angular-eslint/schematics" +======= + "schematicCollections": [ + "@angular-eslint/schematics" + ] + }, + "schematics": { + "@angular-eslint/schematics:application": { + "setParserOptionsProject": true + }, + "@angular-eslint/schematics:library": { + "setParserOptionsProject": true + } +>>>>>>> dspace-7.6.1 } } diff --git a/config/config.example.yml b/config/config.example.yml index 500c2c476ae..c94361d6bc3 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -22,7 +22,7 @@ ui: # 'synced' with the 'dspace.server.url' setting in your backend's local.cfg. rest: ssl: true - host: api7.dspace.org + host: demo.dspace.org port: 443 # NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript nameSpace: /server @@ -187,6 +187,9 @@ languages: - code: gd label: Gàidhlig active: true + - code: it + label: Italiano + active: true - code: lv label: Latviešu active: true @@ -205,6 +208,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 @@ -214,6 +220,12 @@ languages: - code: tr label: Türkçe active: true +<<<<<<< HEAD +======= + - code: vi + label: Tiếng Việt + active: true +>>>>>>> dspace-7.6.1 - code: kk label: Қазақ active: true @@ -226,6 +238,12 @@ languages: - code: el label: Ελληνικά active: true +<<<<<<< HEAD +======= + - code: sr-cyr + label: Српски + active: true +>>>>>>> dspace-7.6.1 - code: uk label: Yкраї́нська active: true @@ -286,33 +304,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 @@ -369,3 +387,11 @@ vocabularies: - filter: 'subject' vocabulary: 'srsc' enabled: true +<<<<<<< HEAD +======= + +# 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' +>>>>>>> dspace-7.6.1 diff --git a/config/config.yml b/config/config.yml index 6016a55b498..15fb77675f6 100644 --- a/config/config.yml +++ b/config/config.yml @@ -1,9 +1,15 @@ debug: false rest: +<<<<<<< HEAD ssl: false host: localhost port: 8080 +======= + ssl: true + host: demo.dspace.org + port: 443 +>>>>>>> dspace-7.6.1 nameSpace: /server # Caching settings 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/integration/admin-menu.spec.ts b/cypress/e2e/admin-menu.spec.ts similarity index 100% rename from cypress/integration/admin-menu.spec.ts rename to cypress/e2e/admin-menu.spec.ts diff --git a/cypress/integration/breadcrumbs.spec.ts b/cypress/e2e/breadcrumbs.cy.ts similarity index 73% rename from cypress/integration/breadcrumbs.spec.ts rename to cypress/e2e/breadcrumbs.cy.ts index 62b9a8ad1d3..ea6acdafcde 100644 --- a/cypress/integration/breadcrumbs.spec.ts +++ b/cypress/e2e/breadcrumbs.cy.ts @@ -1,10 +1,10 @@ -import { TEST_ENTITY_PUBLICATION } from 'cypress/support'; +import { TEST_ENTITY_PUBLICATION } from 'cypress/support/e2e'; import { testA11y } from 'cypress/support/utils'; describe('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/clarin-licenses-page.spec.ts b/cypress/e2e/clarin-licenses-page.spec.ts similarity index 100% rename from cypress/integration/clarin-licenses-page.spec.ts rename to cypress/e2e/clarin-licenses-page.spec.ts diff --git a/cypress/integration/collection-page.spec.ts b/cypress/e2e/collection-page.cy.ts similarity index 69% rename from cypress/integration/collection-page.spec.ts rename to cypress/e2e/collection-page.cy.ts index dd744ca4e49..e4e17d19c6d 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'); // TODO accessibility tests are failing because the UI has been changed // Analyze for accessibility issues diff --git a/cypress/e2e/collection-statistics.cy.ts b/cypress/e2e/collection-statistics.cy.ts new file mode 100644 index 00000000000..6df4e9a4542 --- /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'; + +describe('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 69% rename from cypress/integration/community-page.spec.ts rename to cypress/e2e/community-page.cy.ts index d2e46bef5c3..13e29e4fa07 100644 --- a/cypress/integration/community-page.spec.ts +++ b/cypress/e2e/community-page.cy.ts @@ -1,13 +1,13 @@ -import { TEST_COMMUNITY } from 'cypress/support'; +import { TEST_COMMUNITY } from 'cypress/support/e2e'; import { testA11y } from 'cypress/support/utils'; 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'); // TODO accessibility tests are failing because the UI has been changed // Analyze for accessibility issues diff --git a/cypress/e2e/community-statistics.cy.ts b/cypress/e2e/community-statistics.cy.ts new file mode 100644 index 00000000000..710450e7972 --- /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'; + +describe('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/handle-page.ts b/cypress/e2e/handle-page.ts similarity index 100% rename from cypress/integration/handle-page.ts rename to cypress/e2e/handle-page.ts diff --git a/cypress/integration/header.spec.ts b/cypress/e2e/header.cy.ts similarity index 69% rename from cypress/integration/header.spec.ts rename to cypress/e2e/header.cy.ts index f2437a687a9..0cd18d77b4c 100644 --- a/cypress/integration/header.spec.ts +++ b/cypress/e2e/header.cy.ts @@ -9,6 +9,7 @@ describe('Header', () => { // TODO accessibility tests are failing because the UI has been changed // Analyze for accessibility +<<<<<<< HEAD:cypress/integration/header.spec.ts // testA11y({ // include: ['ds-header'], // exclude: [ @@ -16,5 +17,13 @@ describe('Header', () => { // ['.dropdownLogin'] // "Log in" link has color contrast issues. Will be fixed in #1149 // ], // }); +======= + testA11y({ + include: ['ds-header'], + exclude: [ + ['#search-navbar-container'] // search in navbar has duplicative ID. Will be fixed in #1174 + ], + }); +>>>>>>> dspace-7.6.1:cypress/e2e/header.cy.ts }); }); diff --git a/cypress/e2e/homepage-statistics.cy.ts b/cypress/e2e/homepage-statistics.cy.ts new file mode 100644 index 00000000000..2a1ab9785ab --- /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'; + +describe('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 51% rename from cypress/integration/homepage.spec.ts rename to cypress/e2e/homepage.cy.ts index 59582adb7bc..65101a443d8 100644 --- a/cypress/integration/homepage.spec.ts +++ b/cypress/e2e/homepage.cy.ts @@ -1,5 +1,6 @@ import { testA11y } from 'cypress/support/utils'; +<<<<<<< HEAD:cypress/integration/homepage.spec.ts // NOTE: We changed homepage and these tests are failing // describe('Homepage', () => { // beforeEach(() => { @@ -31,3 +32,35 @@ import { testA11y } from 'cypress/support/utils'; // testA11y('ds-home-page'); // }); // }); +======= +describe('Homepage', () => { + beforeEach(() => { + // All tests start with visiting homepage + cy.visit('/'); + }); + + it('should display translated title "DSpace Repository :: Home"', () => { + cy.title().should('eq', 'DSpace Repository :: Home'); + }); + + it('should contain a news section', () => { + cy.get('ds-home-news').should('be.visible'); + }); + + it('should have a working search box', () => { + const queryString = 'test'; + cy.get('[data-test="search-box"]').type(queryString); + cy.get('[data-test="search-button"]').click(); + cy.url().should('include', '/search'); + cy.url().should('include', 'query=' + encodeURI(queryString)); + }); + + it('should pass accessibility tests', () => { + // Wait for homepage tag to appear + cy.get('ds-home-page').should('be.visible'); + + // Analyze for accessibility issues + testA11y('ds-home-page'); + }); +}); +>>>>>>> dspace-7.6.1:cypress/e2e/homepage.cy.ts diff --git a/cypress/e2e/item-page.cy.ts b/cypress/e2e/item-page.cy.ts new file mode 100644 index 00000000000..9dba6eb8cea --- /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'; + +describe('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/e2e/item-statistics.cy.ts b/cypress/e2e/item-statistics.cy.ts new file mode 100644 index 00000000000..47ddcf9f2e8 --- /dev/null +++ b/cypress/e2e/item-statistics.cy.ts @@ -0,0 +1,53 @@ +import { REGEX_MATCH_NON_EMPTY_TEXT, TEST_ENTITY_PUBLICATION } from 'cypress/support/e2e'; +import { testA11y } from 'cypress/support/utils'; + +describe('Item Statistics Page', () => { + const ITEMSTATISTICSPAGE = '/statistics/items/'.concat(TEST_ENTITY_PUBLICATION); + +<<<<<<< HEAD:cypress/integration/item-statistics.spec.ts + // TODO add statistics to the navbar and change this test + // it('should load if you click on "Statistics" from an Item/Entity page', () => { + // cy.visit('/entities/publication/' + TEST_ENTITY_PUBLICATION); + // cy.get('ds-navbar ds-link-menu-item a[title="Statistics"]').click(); + // cy.location('pathname').should('eq', ITEMSTATISTICSPAGE); + // }); +======= + it('should load if you click on "Statistics" from an Item/Entity page', () => { + 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); + }); +>>>>>>> dspace-7.6.1:cypress/e2e/item-statistics.cy.ts + + 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('be.visible'); + cy.get('ds-item-page').should('not.exist'); + }); + + it('should contain a "Total visits" section', () => { + cy.visit(ITEMSTATISTICSPAGE); + cy.get('table[data-test="TotalVisits"]').should('be.visible'); + }); + + it('should contain a "Total visits per month" section', () => { + cy.visit(ITEMSTATISTICSPAGE); + // 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('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); + + // TODO accessibility tests are failing because the UI has been changed + // Analyze for accessibility issues + // testA11y('ds-item-statistics-page'); + }); +}); diff --git a/cypress/e2e/login-modal.cy.ts b/cypress/e2e/login-modal.cy.ts new file mode 100644 index 00000000000..d29c13c2f96 --- /dev/null +++ b/cypress/e2e/login-modal.cy.ts @@ -0,0 +1,138 @@ +import { TEST_ADMIN_PASSWORD, TEST_ADMIN_USER, TEST_ENTITY_PUBLICATION } from 'cypress/support/e2e'; +import { testA11y } from 'cypress/support/utils'; + +const page = { + openLoginMenu() { + // Click the "Log In" dropdown menu in header + cy.get('ds-themed-navbar [data-test="login-menu"]').click(); + }, + openUserMenu() { + // Once logged in, click the User menu in header + cy.get('ds-themed-navbar [data-test="user-menu"]').click(); + }, + submitLoginAndPasswordByPressingButton(email, password) { + // Enter email + cy.get('ds-themed-navbar [data-test="email"]').type(email); + // Enter password + cy.get('ds-themed-navbar [data-test="password"]').type(password); + // Click login button + cy.get('ds-themed-navbar [data-test="login-button"]').click(); + }, + submitLoginAndPasswordByPressingEnter(email, password) { + // In opened Login modal, fill out email & password, then click Enter + cy.get('ds-themed-navbar [data-test="email"]').type(email); + cy.get('ds-themed-navbar [data-test="password"]').type(password); + cy.get('ds-themed-navbar [data-test="password"]').type('{enter}'); + }, + submitLogoutByPressingButton() { + // This is the POST command that will actually log us out + cy.intercept('POST', '/server/api/authn/logout').as('logout'); + // Click logout button + cy.get('ds-themed-navbar [data-test="logout-button"]').click(); + // Wait until above POST command responds before continuing + // (This ensures next action waits until logout completes) + cy.wait('@logout'); + } +}; + +describe('Login Modal', () => { + it('should login when clicking button & stay on same page', () => { + const ENTITYPAGE = '/entities/publication/'.concat(TEST_ENTITY_PUBLICATION); + cy.visit(ENTITYPAGE); + + // Login menu should exist + cy.get('ds-log-in').should('exist'); + + // Login, and the tag should no longer exist + page.openLoginMenu(); + cy.get('.form-login').should('be.visible'); + + page.submitLoginAndPasswordByPressingButton(TEST_ADMIN_USER, TEST_ADMIN_PASSWORD); + cy.get('ds-log-in').should('not.exist'); + + // Verify we are still on the same page + cy.url().should('include', ENTITYPAGE); + + // Open user menu, verify user menu & logout button now available + page.openUserMenu(); + cy.get('ds-user-menu').should('be.visible'); + cy.get('ds-log-out').should('be.visible'); + }); + + it('should login when clicking enter key & stay on same page', () => { + cy.visit('/home'); + + // Open login menu in header & verify tag is visible + page.openLoginMenu(); + cy.get('.form-login').should('be.visible'); + + // Login, and the tag should no longer exist + page.submitLoginAndPasswordByPressingEnter(TEST_ADMIN_USER, TEST_ADMIN_PASSWORD); + cy.get('.form-login').should('not.exist'); + + // Verify we are still on homepage + cy.url().should('include', '/home'); + + // Open user menu, verify user menu & logout button now available + page.openUserMenu(); + cy.get('ds-user-menu').should('be.visible'); + cy.get('ds-log-out').should('be.visible'); + }); + + it('should support logout', () => { + // First authenticate & access homepage + cy.login(TEST_ADMIN_USER, TEST_ADMIN_PASSWORD); + cy.visit('/'); + + // Verify ds-log-in tag doesn't exist, but ds-log-out tag does exist + cy.get('ds-log-in').should('not.exist'); + cy.get('ds-log-out').should('exist'); + + // Click logout button + page.openUserMenu(); + page.submitLogoutByPressingButton(); + + // Verify ds-log-in tag now exists + cy.get('ds-log-in').should('exist'); + cy.get('ds-log-out').should('not.exist'); + }); + + it('should allow new user registration', () => { + cy.visit('/'); + + page.openLoginMenu(); + + // Registration link should be visible + cy.get('ds-themed-navbar [data-test="register"]').should('be.visible'); + + // Click registration link & you should go to registration page + cy.get('ds-themed-navbar [data-test="register"]').click(); + cy.location('pathname').should('eq', '/register'); + cy.get('ds-register-email').should('exist'); + }); + + it('should allow forgot password', () => { + cy.visit('/'); + + page.openLoginMenu(); + + // Forgot password link should be visible + cy.get('ds-themed-navbar [data-test="forgot"]').should('be.visible'); + + // Click link & you should go to Forgot Password page + cy.get('ds-themed-navbar [data-test="forgot"]').click(); + 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/login-modal.spec.ts b/cypress/e2e/login-modal.spec.ts similarity index 100% rename from cypress/integration/login-modal.spec.ts rename to cypress/e2e/login-modal.spec.ts diff --git a/cypress/e2e/my-dspace.cy.ts b/cypress/e2e/my-dspace.cy.ts new file mode 100644 index 00000000000..13f4a1b5471 --- /dev/null +++ b/cypress/e2e/my-dspace.cy.ts @@ -0,0 +1,141 @@ +import { Options } from 'cypress-axe'; +import { TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD, TEST_SUBMIT_COLLECTION_NAME } from 'cypress/support/e2e'; +import { testA11y } from 'cypress/support/utils'; + +describe('My DSpace page', () => { + it('should display recent submissions and pass accessibility tests', () => { + cy.visit('/mydspace'); + + // This page is restricted, so we will be shown the login form. Fill it out & submit. + cy.loginViaForm(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD); + + cy.get('ds-my-dspace-page').should('be.visible'); + + // At least one recent submission should be displayed + cy.get('[data-test="list-object"]').should('be.visible'); + + // Click each filter toggle to open *every* filter + // (As we want to scan filter section for accessibility issues as well) + cy.get('.filter-toggle').click({ multiple: true }); + + // Analyze for accessibility issues + testA11y('ds-my-dspace-page'); + }); + + it('should have a working detailed view that passes accessibility tests', () => { + cy.visit('/mydspace'); + + // This page is restricted, so we will be shown the login form. Fill it out & submit. + cy.loginViaForm(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD); + + cy.get('ds-my-dspace-page').should('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('be.visible'); + + // Analyze for accessibility issues + testA11y('ds-my-dspace-page', + { + rules: { + // Search filters fail these two "moderate" impact rules + 'heading-order': { enabled: false }, + 'landmark-unique': { enabled: false } + } + } as Options + ); + }); + + // NOTE: Deleting existing submissions is exercised by submission.spec.ts + it('should let you start a new submission & edit in-progress submissions', () => { + cy.visit('/mydspace'); + + // This page is restricted, so we will be shown the login form. Fill it out & submit. + cy.loginViaForm(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD); + + // Open the New Submission dropdown + cy.get('button[data-test="submission-dropdown"]').click(); + // Click on the "Item" type in that dropdown + cy.get('#entityControlsDropdownMenu button[title="none"]').click(); + + // This should display the (popup window) + cy.get('ds-create-item-parent-selector').should('be.visible'); + + // Type in a known Collection name in the search box + 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="'.concat(TEST_SUBMIT_COLLECTION_NAME).concat('"]')).click(); + + // New URL should include /workspaceitems, as we've started a new submission + cy.url().should('include', '/workspaceitems'); + + // The Submission edit form tag should be visible + cy.get('ds-submission-edit').should('be.visible'); + + // A Collection menu button should exist & its value should be the selected collection + cy.get('#collectionControlsMenuButton span').should('have.text', TEST_SUBMIT_COLLECTION_NAME); + + // Now that we've created a submission, we'll test that we can go back and Edit it. + // Get our Submission URL, to parse out the ID of this new submission + cy.location().then(fullUrl => { + // This will be the full path (/workspaceitems/[id]/edit) + const path = fullUrl.pathname; + // Split on the slashes + const subpaths = path.split('/'); + // Part 2 will be the [id] of the submission + const id = subpaths[2]; + + // Click the "Save for Later" button to save this submission + cy.get('ds-submission-form-footer [data-test="save-for-later"]').click(); + + // "Save for Later" should send us to MyDSpace + cy.url().should('include', '/mydspace'); + + // Close any open notifications, to make sure they don't get in the way of next steps + cy.get('[data-dismiss="alert"]').click({multiple: true}); + + // This is the GET command that will actually run the search + cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results'); + // On MyDSpace, find the submission we just created via its ID + cy.get('[data-test="search-box"]').type(id); + cy.get('[data-test="search-button"]').click(); + + // Wait for search results to come back from the above GET command + cy.wait('@search-results'); + + // Click the Edit button for this in-progress submission + cy.get('#edit_' + id).click(); + + // Should send us back to the submission form + cy.url().should('include', '/workspaceitems/' + id + '/edit'); + + // Discard our new submission by clicking Discard in Submission form & confirming + cy.get('ds-submission-form-footer [data-test="discard"]').click(); + cy.get('button#discard_submit').click(); + + // Discarding should send us back to MyDSpace + cy.url().should('include', '/mydspace'); + }); + }); + + it('should let you import from external sources', () => { + cy.visit('/mydspace'); + + // This page is restricted, so we will be shown the login form. Fill it out & submit. + cy.loginViaForm(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD); + + // Open the New Import dropdown + cy.get('button[data-test="import-dropdown"]').click(); + // Click on the "Item" type in that dropdown + cy.get('#importControlsDropdownMenu button[title="none"]').click(); + + // New URL should include /import-external, as we've moved to the import page + cy.url().should('include', '/import-external'); + + // The external import searchbox should be visible + cy.get('ds-submission-import-external-searchbar').should('be.visible'); + }); + +}); diff --git a/cypress/integration/my-dspace.spec.ts b/cypress/e2e/my-dspace.spec.ts similarity index 100% rename from cypress/integration/my-dspace.spec.ts rename to cypress/e2e/my-dspace.spec.ts 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/e2e/search-navbar.cy.ts b/cypress/e2e/search-navbar.cy.ts new file mode 100644 index 00000000000..648db17fe65 --- /dev/null +++ b/cypress/e2e/search-navbar.cy.ts @@ -0,0 +1,66 @@ +import { TEST_SEARCH_TERM } from 'cypress/support/e2e'; + +const page = { + fillOutQueryInNavBar(query) { + // Click the magnifying glass + cy.get('ds-themed-navbar [data-test="header-search-icon"]').click(); + // Fill out a query in input that appears + cy.get('ds-themed-navbar [data-test="header-search-box"]').type(query); + }, + submitQueryByPressingEnter() { + cy.get('ds-themed-navbar [data-test="header-search-box"]').type('{enter}'); + }, + submitQueryByPressingIcon() { + cy.get('ds-themed-navbar [data-test="header-search-icon"]').click(); + } +}; + +describe('Search from Navigation Bar', () => { + // NOTE: these tests currently assume this query will return results! + const query = TEST_SEARCH_TERM; + + it('should go to search page with correct query if submitted (from home)', () => { + cy.visit('/'); + // This is the GET command that will actually run the search + cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results'); + // Run the search + page.fillOutQueryInNavBar(query); + page.submitQueryByPressingEnter(); + // New URL should include query param + 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 + cy.get('[data-test="list-object"]').should('be.visible'); + }); + + it('should go to search page with correct query if submitted (from search)', () => { + cy.visit('/search'); + // This is the GET command that will actually run the search + cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results'); + // Run the search + page.fillOutQueryInNavBar(query); + page.submitQueryByPressingEnter(); + // New URL should include query param + 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 + cy.get('[data-test="list-object"]').should('be.visible'); + }); + + it('should allow user to also submit query by clicking icon', () => { + cy.visit('/'); + // This is the GET command that will actually run the search + cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results'); + // Run the search + page.fillOutQueryInNavBar(query); + page.submitQueryByPressingIcon(); + // New URL should include query param + 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 + cy.get('[data-test="list-object"]').should('be.visible'); + }); +}); diff --git a/cypress/e2e/search-page.cy.ts b/cypress/e2e/search-page.cy.ts new file mode 100644 index 00000000000..755f8eaac6c --- /dev/null +++ b/cypress/e2e/search-page.cy.ts @@ -0,0 +1,56 @@ +import { Options } from 'cypress-axe'; +import { TEST_SEARCH_TERM } from 'cypress/support/e2e'; +import { testA11y } from 'cypress/support/utils'; + +describe('Search Page', () => { + it('should redirect to the correct url when query was set and submit button was triggered', () => { + const queryString = 'Another interesting query string'; + cy.visit('/search'); + // Type query in searchbox & click search button + cy.get('[data-test="search-box"]').type(queryString); + cy.get('[data-test="search-button"]').click(); + cy.url().should('include', 'query=' + encodeURI(queryString)); + }); + + it('should load results and pass accessibility tests', () => { + 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('be.visible'); + + // At least one search result should be displayed + cy.get('[data-test="list-object"]').should('be.visible'); + + // Click each filter toggle to open *every* filter + // (As we want to scan filter section for accessibility issues as well) + cy.get('[data-test="filter-toggle"]').click({ multiple: true }); + + // Analyze for accessibility issues + testA11y('ds-search-page'); + }); + + it('should have a working grid view that passes accessibility tests', () => { + 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('be.visible'); + + // At least one grid object (card) should be displayed + cy.get('[data-test="grid-object"]').should('be.visible'); + + // Analyze for accessibility issues + testA11y('ds-search-page', + { + rules: { + // Search filters fail these two "moderate" impact rules + 'heading-order': { enabled: false }, + 'landmark-unique': { enabled: false } + } + } as Options + ); + }); +}); diff --git a/cypress/integration/submission-ui.spec.ts b/cypress/e2e/submission-ui.spec.ts similarity index 100% rename from cypress/integration/submission-ui.spec.ts rename to cypress/e2e/submission-ui.spec.ts diff --git a/cypress/e2e/submission.cy.ts b/cypress/e2e/submission.cy.ts new file mode 100644 index 00000000000..ed10b2d13aa --- /dev/null +++ b/cypress/e2e/submission.cy.ts @@ -0,0 +1,134 @@ +import { TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD, TEST_SUBMIT_COLLECTION_NAME, TEST_SUBMIT_COLLECTION_UUID } from 'cypress/support/e2e'; + +describe('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='.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); + + // Should redirect to /workspaceitems, as we've started a new submission + cy.url().should('include', '/workspaceitems'); + + // The Submission edit form tag should be visible + cy.get('ds-submission-edit').should('be.visible'); + + // A Collection menu button should exist & it's value should be the selected collection + cy.get('#collectionControlsMenuButton span').should('have.text', TEST_SUBMIT_COLLECTION_NAME); + + // 4 sections should be visible by default + cy.get('div#section_traditionalpageone').should('be.visible'); + cy.get('div#section_traditionalpagetwo').should('be.visible'); + cy.get('div#section_upload').should('be.visible'); + cy.get('div#section_license').should('be.visible'); + + // Discard button should work + // Clicking it will display a confirmation, which we will confirm with another click + cy.get('button#discard').click(); + cy.get('button#discard_submit').click(); + }); + + it('should block submission & show errors if required fields are missing', () => { + // Create a new submission + 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); + + // Attempt an immediate deposit without filling out any fields + cy.get('button#deposit').click(); + + // A warning alert should display. + cy.get('ds-notification div.alert-success').should('not.exist'); + cy.get('ds-notification div.alert-warning').should('be.visible'); + + // First section should have an exclamation error in the header + // (as it has required fields) + cy.get('div#traditionalpageone-header i.fa-exclamation-circle').should('be.visible'); + + // Title field should have class "is-invalid" applied, as it's required + cy.get('input#dc_title').should('have.class', 'is-invalid'); + + // Date Year field should also have "is-valid" class + cy.get('input#dc_date_issued_year').should('have.class', 'is-invalid'); + + // FINALLY, cleanup after ourselves. This also exercises the MyDSpace delete button. + // Get our Submission URL, to parse out the ID of this submission + cy.location().then(fullUrl => { + // This will be the full path (/workspaceitems/[id]/edit) + const path = fullUrl.pathname; + // Split on the slashes + const subpaths = path.split('/'); + // Part 2 will be the [id] of the submission + const id = subpaths[2]; + + // Even though form is incomplete, the "Save for Later" button should still work + cy.get('button#saveForLater').click(); + + // "Save for Later" should send us to MyDSpace + cy.url().should('include', '/mydspace'); + + // A success alert should be visible + cy.get('ds-notification div.alert-success').should('be.visible'); + // Now, dismiss any open alert boxes (may be multiple, as tests run quickly) + cy.get('[data-dismiss="alert"]').click({multiple: true}); + + // This is the GET command that will actually run the search + cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results'); + // On MyDSpace, find the submission we just saved via its ID + cy.get('[data-test="search-box"]').type(id); + cy.get('[data-test="search-button"]').click(); + + // Wait for search results to come back from the above GET command + cy.wait('@search-results'); + + // Delete our created submission & confirm deletion + cy.get('button#delete_' + id).click(); + cy.get('button#delete_confirm').click(); + }); + }); + + it('should allow for deposit if all required fields completed & file uploaded', () => { + // Create a new submission + 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); + + // Fill out all required fields (Title, Date) + cy.get('input#dc_title').type('DSpace logo uploaded via e2e tests'); + cy.get('input#dc_date_issued_year').type('2022'); + + // Confirm the required license by checking checkbox + // (NOTE: requires "force:true" cause Cypress claims this checkbox is covered by its own ) + cy.get('input#granted').check( {force: true} ); + + // Before using Cypress drag & drop, we have to manually trigger the "dragover" event. + // This ensures our UI displays the dropzone that covers the entire submission page. + // (For some reason Cypress drag & drop doesn't trigger this even itself & upload won't work without this trigger) + cy.get('ds-uploader').trigger('dragover'); + + // This is the POST command that will upload the file + cy.intercept('POST', '/server/api/submission/workspaceitems/*').as('upload'); + + // Upload our DSpace logo via drag & drop onto submission form + // cy.get('div#section_upload') + cy.get('div.ds-document-drop-zone').selectFile('src/assets/images/dspace-logo.png', { + action: 'drag-drop' + }); + + // Wait for upload to complete before proceeding + cy.wait('@upload'); + + // Wait for deposit button to not be disabled & click it. + cy.get('button#deposit').should('not.be.disabled').click(); + + // No warnings should exist. Instead, just successful deposit alert is displayed + cy.get('ds-notification div.alert-warning').should('not.exist'); + cy.get('ds-notification div.alert-success').should('be.visible'); + }); + +}); diff --git a/cypress/integration/submission.spec.ts b/cypress/e2e/submission.spec.ts similarity index 100% rename from cypress/integration/submission.spec.ts rename to cypress/e2e/submission.spec.ts diff --git a/cypress/integration/tombstone.spec.ts b/cypress/e2e/tombstone.spec.ts similarity index 100% rename from cypress/integration/tombstone.spec.ts rename to cypress/e2e/tombstone.spec.ts diff --git a/cypress/integration/item-statistics.spec.ts b/cypress/integration/item-statistics.spec.ts deleted file mode 100644 index be777c224c7..00000000000 --- a/cypress/integration/item-statistics.spec.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { TEST_ENTITY_PUBLICATION } from 'cypress/support'; -import { testA11y } from 'cypress/support/utils'; - -describe('Item Statistics Page', () => { - const ITEMSTATISTICSPAGE = '/statistics/items/' + TEST_ENTITY_PUBLICATION; - - // TODO add statistics to the navbar and change this test - // it('should load if you click on "Statistics" from an Item/Entity page', () => { - // cy.visit('/entities/publication/' + 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-page').should('not.exist'); - }); - - it('should contain a "Total visits" section', () => { - cy.visit(ITEMSTATISTICSPAGE); - cy.get('.' + TEST_ENTITY_PUBLICATION + '_TotalVisits').should('exist'); - }); - - it('should contain a "Total visits per month" section', () => { - cy.visit(ITEMSTATISTICSPAGE); - cy.get('.' + TEST_ENTITY_PUBLICATION + '_TotalVisitsPerMonth').should('exist'); - }); - - it('should pass accessibility tests', () => { - cy.visit(ITEMSTATISTICSPAGE); - - // tag must be loaded - cy.get('ds-item-statistics-page').should('exist'); - - // TODO accessibility tests are failing because the UI has been changed - // Analyze for accessibility issues - // testA11y('ds-item-statistics-page'); - }); -}); diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index 1964ccc0dd4..d0a7c5096a7 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -4,12 +4,26 @@ // *********************************************** import { AuthTokenInfo, TOKENITEM } from 'src/app/core/auth/models/auth-token-info.model'; +<<<<<<< HEAD import { FALLBACK_TEST_REST_BASE_URL, TEST_COLLECTION_NAME } from '.'; // 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 { +======= +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 +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace +>>>>>>> dspace-7.6.1 namespace Cypress { interface Chainable { /** @@ -27,6 +41,18 @@ declare global { * @param password password to login as */ loginViaForm(email: string, password: string): typeof loginViaForm; +<<<<<<< HEAD +======= + + /** + * 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; +>>>>>>> dspace-7.6.1 } } } @@ -53,6 +79,7 @@ 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 { +<<<<<<< HEAD console.log("Found 'rest.baseUrl' in config.json. Using this REST API for login: " + config.rest.baseUrl); baseRestUrl = config.rest.baseUrl; } @@ -87,11 +114,54 @@ function login(email: string, password: string): void { }); }); +======= + //console.log("Found 'rest.baseUrl' in config.json. Using this REST API for login: ".concat(config.rest.baseUrl)); + 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 = '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); +>>>>>>> dspace-7.6.1 }); } // Add as a Cypress command (i.e. assign to 'cy.login') Cypress.Commands.add('login', login); +<<<<<<< HEAD // IT test should not fail on some console error Cypress.on('uncaught:exception', (err, runnable) => { // returning false here prevents Cypress from @@ -99,12 +169,18 @@ Cypress.on('uncaught:exception', (err, runnable) => { return false; }); +======= +>>>>>>> dspace-7.6.1 /** * Login user via displayed login form * @param email email to login as * @param password password to login as */ +<<<<<<< HEAD function loginViaForm(email: string, password: string): void { +======= +function loginViaForm(email: string, password: string): void { +>>>>>>> dspace-7.6.1 // Enter email cy.get('ds-log-in [data-test="email"]').type(email); // Enter password @@ -115,6 +191,7 @@ Cypress.on('uncaught:exception', (err, runnable) => { // Add as a Cypress command (i.e. assign to 'cy.loginViaForm') Cypress.Commands.add('loginViaForm', loginViaForm); +<<<<<<< HEAD // Add as a Cypress command (i.e. assign to 'cy.login') Cypress.Commands.add('login', login); @@ -288,4 +365,71 @@ export const createItemProcess = { cy.get('ds-dynamic-autocomplete input[placeholder = "Last name"]').eq(0).click({force: true}).type(value); } }; +======= + +/** + * 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); +>>>>>>> dspace-7.6.1 diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts new file mode 100644 index 00000000000..dd7ee1824c4 --- /dev/null +++ b/cypress/support/e2e.ts @@ -0,0 +1,66 @@ +// *********************************************************** +// This example support/index.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import all custom Commands (from commands.ts) for all tests +import './commands'; + +// Import Cypress Axe tools for all tests +// https://github.com/component-driven/cypress-axe +import 'cypress-axe'; + +// Runs once before the first test in each "block" +beforeEach(() => { + // Pre-agree to all Klaro cookies by setting the klaro-anonymous cookie + // This just ensures it doesn't get in the way of matching other objects in the page. + cy.setCookie('klaro-anonymous', '{%22authentication%22:true%2C%22preferences%22:true%2C%22acknowledgement%22:true%2C%22google-analytics%22:true%2C%22google-recaptcha%22:true}'); +}); + +// 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(() => { + cy.window().then((win) => { + win.location.href = 'about:blank'; + }); +});*/ + + +// Global constants used in tests +// 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 +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'; +// Community/collection/publication used for view/edit tests +export const TEST_COLLECTION = Cypress.env('DSPACE_TEST_COLLECTION') || '282164f5-d325-4740-8dd1-fa4d6d3e7200'; +export const TEST_COMMUNITY = Cypress.env('DSPACE_TEST_COMMUNITY') || '0958c910-2037-42a9-81c7-dca80e3892b4'; +export const TEST_ENTITY_PUBLICATION = Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION') || 'e98b0f27-5c19-49a0-960d-eb6ad5287067'; +// Search term (should return results) used in search tests +export const TEST_SEARCH_TERM = Cypress.env('DSPACE_TEST_SEARCH_TERM') || 'test'; +// Collection used for submission tests +export const TEST_SUBMIT_COLLECTION_NAME = Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_NAME') || 'Sample Collection'; +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..6e549bcc0bc 100644 --- a/docker/README.md +++ b/docker/README.md @@ -6,7 +6,24 @@ 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. *** +<<<<<<< HEAD ## '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 + +>>>>>>> dspace-7.6.1 This Dockerfile is used to build a *development* DSpace 7 Angular UI image, published as 'dspace/dspace-angular' ``` @@ -20,7 +37,22 @@ Admins to our DockerHub repo can manually publish with the following command. docker push dspace/dspace-angular:dspace-7_x ``` +<<<<<<< HEAD ## 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 +>>>>>>> dspace-7.6.1 - 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 +77,64 @@ docker-compose -f docker/docker-compose.yml build ## To start DSpace (REST and Angular) from your branch +<<<<<<< HEAD +======= +This command provides a quick way to start both the frontend & backend from this single codebase +>>>>>>> dspace-7.6.1 ``` docker-compose -p d7 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml up -d ``` +<<<<<<< HEAD ## Run DSpace REST and DSpace Angular from local branches. _The system will be started in 2 steps. Each step shares the same docker network._ From DSpace/DSpace (build as needed) +======= +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' clone (build first as needed): +>>>>>>> dspace-7.6.1 ``` docker-compose -p d7 up -d ``` +<<<<<<< HEAD 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) +>>>>>>> dspace-7.6.1 ``` docker-compose -p d7 -f docker/docker-compose.yml up -d ``` +<<<<<<< HEAD +======= +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 +``` + +>>>>>>> dspace-7.6.1 ## Ingest test data from AIPDIR Create an administrator @@ -87,9 +160,18 @@ 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 ``` +<<<<<<< HEAD ## 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._ ``` docker-compose -p d7ci -f docker/docker-compose-travis.yml up -d +======= +## 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-ci.yml up -d +>>>>>>> dspace-7.6.1 ``` diff --git a/docker/docker-compose-ci.yml b/docker/docker-compose-ci.yml index 11d145a48bf..214cbcb2201 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_CI_IMAGE:-dataquest/dspace:dspace-7_x-test} @@ -53,7 +56,11 @@ services: - '-c' - | while (! /dev/null 2>&1; do sleep 1; done; +<<<<<<< HEAD /dspace/bin/dspace database migrate force +======= + /dspace/bin/dspace database migrate ignored +>>>>>>> dspace-7.6.1 catalina.sh run # DSpace database container # NOTE: This is customized to use our loadsql image, so that we are using a database with existing test data @@ -63,7 +70,11 @@ services: # This LOADSQL should be kept in sync with the LOADSQL in # https://github.com/DSpace/DSpace/blob/main/dspace/src/main/docker-compose/db.entities.yml # This SQL is available from https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data +<<<<<<< HEAD LOADSQL: https://github.com/dataquest-dev/DSpace/releases/download/data/dspace-test-database-dump_23.6.2023.sql +======= + LOADSQL: https://github.com/DSpace-Labs/AIP-Files/releases/download/demo-entities-data/dspace7-entities-data.sql +>>>>>>> dspace-7.6.1 PGDATA: /pgdata image: dspace/dspace-postgres-pgcrypto:loadsql networks: 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 edea0b710e8..19c6a588e85 100644 --- a/docker/docker-compose-rest.yml +++ b/docker/docker-compose-rest.yml @@ -33,6 +33,7 @@ services: # db.url: Ensure we are using the 'dspacedb' image for our database db__P__url: 'jdbc:postgresql://dspacedb:543${INSTANCE}/dspace' # solr.server: Ensure we are using the 'dspacesolr' image for Solr +<<<<<<< HEAD solr__P__server: http://dspacesolr:898${INSTANCE}/solr #S3 config assetstore__P__index__P__primary: ${S3_STORAGE:-0} @@ -46,6 +47,13 @@ services: assetstore__P__s3__P__pathStyleAccessEnabled: ${S3_PATH_STYLE_ACCESS:-false} assetstore__P__s3__P__endpoint: ${S3_ENDPOINT:-} image: ${DSPACE_REST_IMAGE:-dataquest/dspace:dtq-dev-7.5} +======= + solr__P__server: http://dspacesolr:8983/solr + # 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: "${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-dspace-7_x-test}" +>>>>>>> dspace-7.6.1 depends_on: - dspacedb networks: @@ -98,12 +106,17 @@ services: command: -p 543${INSTANCE} # DSpace Solr container dspacesolr: +<<<<<<< HEAD environment: TZ: ${TIMEZONE:-Europe/Bratislava} restart: unless-stopped container_name: dspacesolr${INSTANCE} # Uses official Solr image at https://hub.docker.com/_/solr/ image: solr:8.11-slim +======= + container_name: dspacesolr + image: "${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-dspace-7_x}" +>>>>>>> dspace-7.6.1 # Needs main 'dspace' container to start first to guarantee access to solr_configs depends_on: - dspace @@ -117,21 +130,20 @@ 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 - solr_logs:/var/solr/logs # 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 +<<<<<<< HEAD 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 @@ -141,6 +153,17 @@ services: precreate-core statistics /opt/solr/server/solr/configsets/dspace/statistics cp -r -u /opt/solr/server/solr/configsets/dspace/statistics/* statistics exec solr -p 898${INSTANCE} -f -m 4g +======= + 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 +>>>>>>> dspace-7.6.1 volumes: assetstore: pgdata: 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/package.json b/package.json index 7abfb20d4a6..bc755d0b1fc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,10 @@ { "name": "dspace-angular", +<<<<<<< HEAD "version": "7.5.0", +======= + "version": "7.6.1", +>>>>>>> dspace-7.6.1 "scripts": { "ng": "ng", "config:watch": "nodemon", @@ -15,14 +19,14 @@ "analyze": "webpack-bundle-analyzer dist/browser/stats.json", "build": "ng build --configuration development", "build:stats": "ng build --stats-json", - "build:prod": "yarn run build:ssr", + "build:prod": "cross-env NODE_ENV=production yarn run build:ssr", "build:ssr": "ng build --configuration production && ng run dspace-angular:server:production", - "test": "ng test --sourceMap=true --watch=false --configuration test", - "test:watch": "nodemon --exec \"ng test --sourceMap=true --watch=true --configuration test\"", - "test:headless": "ng test --sourceMap=true --watch=false --configuration test --browsers=ChromeHeadless --code-coverage", + "test": "ng test --source-map=true --watch=false --configuration test", + "test:watch": "nodemon --exec \"ng test --source-map=true --watch=true --configuration test\"", + "test:headless": "ng test --source-map=true --watch=false --configuration test --browsers=ChromeHeadless --code-coverage", "lint": "ng lint", "lint-fix": "ng lint --fix=true", - "e2e": "ng e2e", + "e2e": "cross-env NODE_ENV=production ng e2e", "clean:dev:config": "rimraf src/assets/config.json", "clean:coverage": "rimraf coverage", "clean:dist": "rimraf dist", @@ -55,6 +59,7 @@ "ts-node": "10.2.1" }, "dependencies": { +<<<<<<< HEAD "@angular/animations": "~13.3.12", "@angular/cdk": "^13.2.6", "@angular/common": "~13.3.12", @@ -114,10 +119,69 @@ "lru-cache": "^7.14.1", "markdown-it": "^13.0.1", "markdown-it-mathjax3": "^4.3.1", +======= + "@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.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", + "@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", + "@types/grecaptcha": "^3.0.4", + "angular-idle-preload": "3.0.0", + "angulartics2": "^12.2.0", + "axios": "^1.6.0", + "bootstrap": "^4.6.1", + "cerialize": "0.1.18", + "cli-progress": "^3.12.0", + "colors": "^1.4.0", + "compression": "^1.7.4", + "cookie-parser": "1.4.6", + "core-js": "^3.30.1", + "date-fns": "^2.29.3", + "date-fns-tz": "^1.3.7", + "deepmerge": "^4.3.1", + "ejs": "^3.1.9", + "express": "^4.18.2", + "express-rate-limit": "^5.1.3", + "fast-json-patch": "^3.1.1", + "filesize": "^6.1.0", + "http-proxy-middleware": "^1.0.5", + "http-terminator": "^3.2.0", + "isbot": "^3.6.10", + "js-cookie": "2.2.1", + "js-yaml": "^4.1.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.2", +>>>>>>> dspace-7.6.1 "mirador": "^3.3.0", "mirador-dl-plugin": "^0.13.0", "mirador-share-plugin": "^0.11.0", "morgan": "^1.10.0", +<<<<<<< HEAD "ng-mocks": "^13.1.1", "ng2-file-upload": "1.4.0", "ng2-nouislider": "^1.8.3", @@ -133,11 +197,29 @@ "rxjs": "^7.5.5", "sanitize-html": "^2.7.2", "sortablejs": "1.13.0", +======= + "ng-mocks": "^14.10.0", + "ng2-file-upload": "1.4.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": "^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.8.0", + "sanitize-html": "^2.10.0", + "sortablejs": "1.15.0", +>>>>>>> dspace-7.6.1 "uuid": "^8.3.2", "webfontloader": "1.6.28", "zone.js": "~0.11.5" }, "devDependencies": { +<<<<<<< HEAD "@angular-builders/custom-webpack": "~13.1.0", "@angular-devkit/build-angular": "~13.3.10", "@angular-eslint/builder": "13.1.0", @@ -156,10 +238,31 @@ "@types/deep-freeze": "0.1.2", "@types/ejs": "^3.1.1", "@types/express": "^4.17.9", +======= + "@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.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.2", + "@types/express": "^4.17.17", +>>>>>>> dspace-7.6.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", +<<<<<<< HEAD "@types/sanitize-html": "^2.6.2", "@typescript-eslint/eslint-plugin": "5.11.0", "@typescript-eslint/parser": "5.11.0", @@ -188,6 +291,37 @@ "ngx-mask": "~13.1.7", "nodemon": "^2.0.20", "postcss": "^8.1", +======= + "@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": "12.17.4", + "cypress-axe": "^1.4.0", + "deep-freeze": "0.0.1", + "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.7", + "jasmine-core": "^3.8.0", + "jasmine-marbles": "0.9.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-mask": "^13.1.7", + "nodemon": "^2.0.22", + "postcss": "^8.4", +>>>>>>> dspace-7.6.1 "postcss-apply": "0.12.0", "postcss-import": "^14.0.0", "postcss-loader": "^4.0.3", @@ -197,6 +331,7 @@ "react-dom": "^16.14.0", "rimraf": "^3.0.2", "rxjs-spy": "^8.0.2", +<<<<<<< HEAD "sass": "~1.33.0", "sass-loader": "^12.6.0", "sass-resources-loader": "^2.1.1", @@ -204,7 +339,16 @@ "typescript": "~4.5.5", "webpack": "^5.69.1", "webpack-bundle-analyzer": "^4.4.0", +======= + "sass": "~1.62.0", + "sass-loader": "^12.6.0", + "sass-resources-loader": "^2.2.5", + "ts-node": "^8.10.2", + "typescript": "~4.8.4", + "webpack": "5.76.1", + "webpack-bundle-analyzer": "^4.8.0", +>>>>>>> dspace-7.6.1 "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..7b672fcfeb1 100644 --- a/server.ts +++ b/server.ts @@ -26,15 +26,24 @@ import * as ejs from 'ejs'; import * as compression from 'compression'; import * as expressStaticGzip from 'express-static-gzip'; /* eslint-enable import/no-namespace */ +<<<<<<< HEAD +======= +>>>>>>> dspace-7.6.1 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'; +<<<<<<< HEAD import { existsSync, readFileSync } from 'fs'; +======= +import { createHttpTerminator } from 'http-terminator'; + +import { readFileSync } from 'fs'; +>>>>>>> dspace-7.6.1 import { join } from 'path'; import { enableProdMode } from '@angular/core'; @@ -54,7 +63,11 @@ 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'; +<<<<<<< HEAD import { TOKENITEM } from 'src/app/core/auth/models/auth-token-info.model'; +======= +import { TOKENITEM } from './src/app/core/auth/models/auth-token-info.model'; +>>>>>>> dspace-7.6.1 /* @@ -179,6 +192,18 @@ export function app() { pathRewrite: path => path.replace(environment.ui.nameSpace, '/'), changeOrigin: true })); +<<<<<<< HEAD +======= + + /** + * Proxy the linksets + */ + router.use('/signposting**', createProxyMiddleware({ + target: `${environment.rest.baseUrl}`, + pathRewrite: path => path.replace(environment.ui.nameSpace, '/'), + changeOrigin: true + })); +>>>>>>> dspace-7.6.1 /** * Checks if the rateLimiter property is present @@ -212,6 +237,7 @@ export function app() { * Checking server status */ server.get('/app/health', healthCheck); +<<<<<<< HEAD /** * Default sending all incoming requests to ngApp() function, after first checking for a cached @@ -219,6 +245,15 @@ export function app() { */ router.get('*', cacheCheck, ngApp); +======= + + /** + * Default sending all incoming requests to ngApp() function, after first checking for a cached + * copy of the page (see cacheCheck()) + */ + router.get('*', cacheCheck, ngApp); + +>>>>>>> dspace-7.6.1 server.use(environment.ui.nameSpace, router); return server; @@ -312,22 +347,39 @@ 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 +<<<<<<< HEAD // When enabled, each page defaults to expiring after 1 day botCache = new LRU( { max: environment.cache.serverSide.botCache.max, ttl: environment.cache.serverSide.botCache.timeToLive || 24 * 60 * 60 * 1000, // 1 day allowStale: environment.cache.serverSide.botCache.allowStale ?? true // if object is stale, return stale value before deleting +======= + // 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, + allowStale: environment.cache.serverSide.botCache.allowStale +>>>>>>> dspace-7.6.1 }); } if (anonymousCacheEnabled()) { // NOTE: While caches may share SSR pages, this cache must be kept separately because the timeToLive // may expire pages more frequently. +<<<<<<< HEAD // When enabled, each page defaults to expiring after 10 seconds (to minimize anonymous users seeing out-of-date content) anonymousCache = new LRU( { max: environment.cache.serverSide.anonymousCache.max, ttl: environment.cache.serverSide.anonymousCache.timeToLive || 10 * 1000, // 10 seconds allowStale: environment.cache.serverSide.anonymousCache.allowStale ?? true // if object is stale, return stale value before deleting +======= + // 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, + allowStale: environment.cache.serverSide.anonymousCache.allowStale +>>>>>>> dspace-7.6.1 }); } } @@ -366,9 +418,25 @@ function cacheCheck(req, res, next) { } // If cached copy exists, return it to the user. +<<<<<<< HEAD if (cachedCopy) { res.locals.ssr = true; // mark response as SSR-generated (enables text compression) res.send(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.page); +>>>>>>> dspace-7.6.1 // 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,23 +511,66 @@ 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; } +<<<<<<< HEAD // 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); +======= + // 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, headers }); +>>>>>>> dspace-7.6.1 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)) { +<<<<<<< HEAD anonymousCache.set(key, page); +======= + anonymousCache.set(key, { page, headers }); +>>>>>>> dspace-7.6.1 if (environment.cache.serverSide.debug) { console.log(`CACHE SAVE FOR ${key} in anonymous cache.`); } } } } /** +<<<<<<< HEAD +======= + * 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; +} +/** +>>>>>>> dspace-7.6.1 * Whether a user is authenticated or not */ function isUserAuthenticated(req): boolean { @@ -479,23 +590,50 @@ function serverStarted() { * @param keys SSL credentials */ function createHttpsServer(keys) { +<<<<<<< HEAD createServer({ +======= + const listener = createServer({ +>>>>>>> dspace-7.6.1 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() { 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..a137bdf55e8 100644 --- a/src/app/access-control/access-control.module.ts +++ b/src/app/access-control/access-control.module.ts @@ -12,6 +12,15 @@ 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'; +<<<<<<< HEAD +======= +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'; +>>>>>>> dspace-7.6.1 /** * Condition for displaying error messages on email form field @@ -28,6 +37,12 @@ export const ValidateEmailErrorStateMatcher: DynamicErrorMessagesMatcher = RouterModule, AccessControlRoutingModule, FormModule, +<<<<<<< HEAD +======= + NgbAccordionModule, + SearchModule, + AccessControlFormModule, +>>>>>>> dspace-7.6.1 ], exports: [ MembersListComponent, @@ -39,6 +54,12 @@ export const ValidateEmailErrorStateMatcher: DynamicErrorMessagesMatcher = GroupFormComponent, SubgroupsListComponent, MembersListComponent, +<<<<<<< HEAD +======= + BulkAccessComponent, + BulkAccessBrowseComponent, + BulkAccessSettingsComponent, +>>>>>>> dspace-7.6.1 ], 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/object-collection/shared/mydspace-item-status/my-dspace-item-status.component.scss b/src/app/access-control/bulk-access/browse/bulk-access-browse.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/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/access-control/bulk-access/bulk-access.component.scss b/src/app/access-control/bulk-access/bulk-access.component.scss new file mode 100644 index 00000000000..e69de29bb2d 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/app/access-control/bulk-access/settings/bulk-access-settings.component.scss b/src/app/access-control/bulk-access/settings/bulk-access-settings.component.scss new file mode 100644 index 00000000000..e69de29bb2d 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..6d3010661e2 100644 --- a/src/app/access-control/epeople-registry/epeople-registry.component.html +++ b/src/app/access-control/epeople-registry/epeople-registry.component.html @@ -4,47 +4,44 @@
-
+
- + + +
+ +
+
+
+ + -
-
- -
- +
+
+ +
+ +<<<<<<< HEAD {{labelPrefix + 'search.head' | trans [collectionSize]="(pageInfoState$ | async)?.totalElements" [hideGear]="true" [hidePagerWhenSinglePage]="true"> +======= + + +>>>>>>> dspace-7.6.1 -
- - - - - - - - - - - - - - - - - -
{{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 c0d70fd0b25..e2cee5e9356 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'; @@ -42,6 +42,7 @@ describe('EPeopleRegistryComponent', () => { let paginationService; beforeEach(waitForAsync(() => { + jasmine.getEnv().allowRespy(true); mockEPeople = [EPersonMock, EPersonMock2]; ePersonDataServiceStub = { activeEPerson: null, @@ -98,7 +99,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) { @@ -202,36 +203,6 @@ describe('EPeopleRegistryComponent', () => { }); }); - describe('toggleEditEPerson', () => { - describe('when you click on first edit eperson button', () => { - beforeEach(fakeAsync(() => { - const editButtons = fixture.debugElement.queryAll(By.css('.access-control-editEPersonButton')); - editButtons[0].triggerEventHandler('click', { - preventDefault: () => {/**/ - } - }); - tick(); - fixture.detectChanges(); - })); - - it('editEPerson form is toggled', () => { - const ePeopleIds = fixture.debugElement.queryAll(By.css('#epeople tr td:first-child')); - ePersonDataServiceStub.getActiveEPerson().subscribe((activeEPerson: EPerson) => { - if (ePeopleIds[0] && activeEPerson === ePeopleIds[0].nativeElement.textContent) { - expect(component.isEPersonFormShown).toEqual(false); - } else { - expect(component.isEPersonFormShown).toEqual(true); - } - - }); - }); - - it('EPerson search section is hidden', () => { - expect(fixture.debugElement.query(By.css('#search'))).toBeNull(); - }); - }); - }); - describe('deleteEPerson', () => { describe('when you click on first delete eperson button', () => { let ePeopleIdsFoundBeforeDelete; @@ -260,17 +231,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 55233d8173d..6e0723ae78c 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'; @Component({ selector: 'ds-epeople-registry', @@ -63,11 +65,6 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy { currentPage: 1 }); - /** - * Whether or not to show the EPerson form - */ - isEPersonFormShown: boolean; - // The search form searchForm; @@ -89,11 +86,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, - public requestService: RequestService) { + public requestService: RequestService, + public dsoNameService: DSONameService, + ) { this.currentSearchQuery = ''; this.currentSearchScope = 'metadata'; this.searchForm = this.formBuilder.group(({ @@ -111,17 +110,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(); @@ -157,14 +150,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; @@ -202,23 +195,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 */ @@ -237,9 +213,13 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy { if (hasValue(ePerson.id)) { this.epersonService.deleteEPerson(ePerson).pipe(getFirstCompletedRemoteData()).subscribe((restResponse: RemoteData) => { if (restResponse.hasSucceeded) { +<<<<<<< HEAD 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)})); +>>>>>>> dspace-7.6.1 } 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 })); } }); } @@ -261,16 +241,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 */ @@ -281,17 +251,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..b4dd4c7690f 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,105 @@ -
+
+
+
- -

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

-
+
- -

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

-
+ +

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

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

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

+
+<<<<<<< HEAD +======= + +
+ +
+
+ +
+
+ + +
+ +
+>>>>>>> dspace-7.6.1 -
-
{{messagePrefix + '.groupsEPersonIsMemberOf' | translate}}
+ +<<<<<<< HEAD +======= +
+
{{messagePrefix + '.groupsEPersonIsMemberOf' | translate}}
+>>>>>>> dspace-7.6.1 - + -
- - - - - - - - - - - - - - - -
{{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 bf03e1defbc..b9aeeb0af26 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'; describe('EPersonFormComponent', () => { let component: EPersonFormComponent; @@ -43,6 +47,8 @@ describe('EPersonFormComponent', () => { let authorizationService: AuthorizationDataService; let groupsDataService: GroupDataService; let epersonRegistrationService: EpersonRegistrationService; + let route: ActivatedRouteStub; + let router: RouterStub; let paginationService; @@ -106,6 +112,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(),{ @@ -116,9 +125,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 { @@ -182,6 +191,8 @@ describe('EPersonFormComponent', () => { }); paginationService = new PaginationServiceStub(); + route = new ActivatedRouteStub(); + router = new RouterStub(); TestBed.configureTestingModule({ imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, TranslateModule.forRoot({ @@ -202,6 +213,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 }, EPeopleRegistryComponent ], schemas: [NO_ERRORS_SCHEMA] @@ -263,24 +276,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', () => { @@ -290,24 +297,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(); - }); - })); + }); }); @@ -316,12 +317,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', () => { @@ -336,12 +335,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(); - }); - })); + }); }); @@ -393,11 +390,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', () => { @@ -428,11 +423,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); + }); }); }); @@ -491,16 +484,16 @@ describe('EPersonFormComponent', () => { }); - it('the delete button should be active if the eperson can be deleted', () => { + it('the delete button should be visible if the ePerson can be deleted', () => { const deleteButton = fixture.debugElement.query(By.css('.delete-button')); - 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 c5c9f8f9544..61ae3ab1908 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,12 @@ 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'; +<<<<<<< HEAD +======= +import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; +import { ActivatedRoute, Router } from '@angular/router'; +import { getEPersonsRoute } from '../../access-control-routing-paths'; +>>>>>>> dspace-7.6.1 @Component({ selector: 'ds-eperson-form', @@ -108,7 +114,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 @@ -165,6 +171,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 */ @@ -183,11 +198,16 @@ export class EPersonFormComponent implements OnInit, OnDestroy { private paginationService: PaginationService, public requestService: RequestService, private epersonRegistrationService: EpersonRegistrationService, + public dsoNameService: DSONameService, + protected route: ActivatedRoute, + protected router: Router, ) { 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'; } })); } @@ -200,15 +220,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, @@ -326,6 +348,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy { onCancel() { this.epersonService.cancelEditEPerson(); this.cancelForm.emit(); + void this.router.navigate([getEPersonsRoute()]); } /** @@ -375,10 +398,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(); } }); @@ -414,10 +439,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(); } }); @@ -450,31 +476,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(); }); } @@ -510,7 +548,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)) { @@ -518,16 +555,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 @@ -543,7 +570,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..7fcf1a0c4c3 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,17 @@
-
+

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

+<<<<<<< HEAD +======= + +>>>>>>> dspace-7.6.1

+ [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..5ed0f16a076 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,11 @@ 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'; +<<<<<<< HEAD import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; +======= +import { UntypedFormControl, UntypedFormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; +>>>>>>> dspace-7.6.1 import { BrowserModule, By } from '@angular/platform-browser'; import { ActivatedRoute, Router } from '@angular/router'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; @@ -36,6 +40,11 @@ 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'; +<<<<<<< HEAD +======= +import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; +import { DSONameServiceMock } from '../../../shared/mocks/dso-name.service.mock'; +>>>>>>> dspace-7.6.1 describe('GroupFormComponent', () => { let component: GroupFormComponent; @@ -130,9 +139,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 +197,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 +207,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 +250,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 +313,13 @@ describe('GroupFormComponent', () => { expect(groupsDataServiceStub.patch).toHaveBeenCalledWith(expected, operations); }); +<<<<<<< HEAD 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(() => { +>>>>>>> dspace-7.6.1 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 4302d126ea2..1e5c61429d0 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 { Component, EventEmitter, HostListener, OnDestroy, OnInit, Output, ChangeDetectorRef } from '@angular/core'; -import { FormGroup } from '@angular/forms'; +import { UntypedFormGroup } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { @@ -10,7 +10,6 @@ import { } from '@ng-dynamic-forms/core'; import { TranslateService } from '@ngx-translate/core'; import { - ObservedValueOf, combineLatest as observableCombineLatest, Observable, of as observableOf, @@ -37,7 +36,7 @@ import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } 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, isNotEmpty, hasValueOperator } from '../../../shared/empty.util'; import { FormBuilderService } from '../../../shared/form/builder/form-builder.service'; @@ -46,7 +45,13 @@ 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'; +<<<<<<< HEAD import { environment } from '../../../../environments/environment'; +======= +import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; +import { environment } from '../../../../environments/environment'; +import { getGroupEditRoute, getGroupsRoute } from '../../access-control-routing-paths'; +>>>>>>> dspace-7.6.1 @Component({ selector: 'ds-group-form', @@ -95,7 +100,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 +139,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 +151,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 +169,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 +219,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 +234,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 +269,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 +316,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 +341,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 +374,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 +437,15 @@ export class GroupFormComponent implements OnInit, OnDestroy { this.groupDataService.delete(group.id).pipe(getFirstCompletedRemoteData()) .subscribe((rd: RemoteData) => { if (rd.hasSucceeded) { +<<<<<<< HEAD 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) })); +>>>>>>> dspace-7.6.1 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..909893644ee 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,6 +1,7 @@

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

+<<<<<<< HEAD
+======= +

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

+>>>>>>> dspace-7.6.1 -
- +
@@ -55,16 +59,21 @@ - - - + + + @@ -89,23 +105,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 : '-' }}
+<<<<<<< HEAD
+
@@ -115,16 +158,21 @@

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

- - - + + + - - + + - - + + +<<<<<<< HEAD +======= + +>>>>>>> dspace-7.6.1 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 e478aa3ef37..b8ea5835c45 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 @@ -19,7 +19,11 @@ import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote- 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'; +<<<<<<< HEAD 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'; +>>>>>>> dspace-7.6.1 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..0c8c8808180 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,12 @@ import { Router } from '@angular/router'; * Represents a non-expandable section in the admin sidebar */ @Component({ +<<<<<<< HEAD /* eslint-disable @angular-eslint/component-selector */ selector: 'li[ds-admin-sidebar-section]', +======= + selector: 'ds-admin-sidebar-section', +>>>>>>> dspace-7.6.1 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..84b6d1ba72d 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,10 @@ 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'; +<<<<<<< HEAD +======= +import { DSONameService } from '../../../../../../core/breadcrumbs/dso-name.service'; +>>>>>>> dspace-7.6.1 export interface SupervisionOrderListEntry { supervisionOrder: SupervisionOrder; @@ -33,6 +37,14 @@ export class SupervisionOrderStatusComponent implements OnChanges { @Output() delete: EventEmitter = new EventEmitter(); +<<<<<<< HEAD +======= + constructor( + public dsoNameService: DSONameService, + ) { + } + +>>>>>>> dspace-7.6.1 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/actions/workspace-item/workspace-item-admin-workflow-actions.component.spec.ts b/src/app/admin/admin-workflow-page/admin-workflow-search-results/actions/workspace-item/workspace-item-admin-workflow-actions.component.spec.ts index 628fc3f89ca..fbeb8be2802 100644 --- a/src/app/admin/admin-workflow-page/admin-workflow-search-results/actions/workspace-item/workspace-item-admin-workflow-actions.component.spec.ts +++ b/src/app/admin/admin-workflow-page/admin-workflow-search-results/actions/workspace-item/workspace-item-admin-workflow-actions.component.spec.ts @@ -11,7 +11,11 @@ import { URLCombiner } from '../../../../../core/url-combiner/url-combiner'; import { WorkspaceItemAdminWorkflowActionsComponent } from './workspace-item-admin-workflow-actions.component'; import { WorkspaceItem } from '../../../../../core/submission/models/workspaceitem.model'; import { +<<<<<<< HEAD getWorkflowItemDeleteRoute, +======= + getWorkspaceItemDeleteRoute, +>>>>>>> dspace-7.6.1 } from '../../../../../workflowitems-edit-page/workflowitems-edit-page-routing-paths'; import { Item } from '../../../../../core/shared/item.model'; import { RemoteData } from '../../../../../core/data/remote-data'; @@ -83,7 +87,11 @@ describe('WorkspaceItemAdminWorkflowActionsComponent', () => { it('should render a delete button with the correct link', () => { const button = fixture.debugElement.query(By.css('a.delete-link')); const link = button.nativeElement.href; +<<<<<<< HEAD expect(link).toContain(new URLCombiner(getWorkflowItemDeleteRoute(wsi.id)).toString()); +======= + expect(link).toContain(new URLCombiner(getWorkspaceItemDeleteRoute(wsi.id)).toString()); +>>>>>>> dspace-7.6.1 }); it('should render a policies button with the correct link', () => { diff --git a/src/app/admin/admin-workflow-page/admin-workflow-search-results/actions/workspace-item/workspace-item-admin-workflow-actions.component.ts b/src/app/admin/admin-workflow-page/admin-workflow-search-results/actions/workspace-item/workspace-item-admin-workflow-actions.component.ts index adbd4216289..c5af7037cf4 100644 --- a/src/app/admin/admin-workflow-page/admin-workflow-search-results/actions/workspace-item/workspace-item-admin-workflow-actions.component.ts +++ b/src/app/admin/admin-workflow-page/admin-workflow-search-results/actions/workspace-item/workspace-item-admin-workflow-actions.component.ts @@ -11,7 +11,11 @@ import { SupervisionOrderGroupSelectorComponent } from './supervision-order-group-selector/supervision-order-group-selector.component'; import { +<<<<<<< HEAD getWorkflowItemDeleteRoute +======= + getWorkspaceItemDeleteRoute +>>>>>>> dspace-7.6.1 } from '../../../../../workflowitems-edit-page/workflowitems-edit-page-routing-paths'; import { ITEM_EDIT_AUTHORIZATIONS_PATH } from '../../../../../item-page/edit-item-page/edit-item-page.routing-paths'; import { WorkspaceItem } from '../../../../../core/submission/models/workspaceitem.model'; @@ -105,10 +109,17 @@ export class WorkspaceItemAdminWorkflowActionsComponent implements OnInit { } /** +<<<<<<< HEAD * Returns the path to the delete page of this workflow item */ getDeleteRoute(): string { return getWorkflowItemDeleteRoute(this.wsi.id); +======= + * Returns the path to the delete page of this workspace item + */ + getDeleteRoute(): string { + return getWorkspaceItemDeleteRoute(this.wsi.id); +>>>>>>> dspace-7.6.1 } /** 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..f9e1baa8852 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,8 @@ +<<<<<<< HEAD import { Component, ComponentFactoryResolver, ElementRef, ViewChild } from '@angular/core'; +======= +import { Component, ComponentFactoryResolver, ElementRef, ViewChild, OnInit } from '@angular/core'; +>>>>>>> dspace-7.6.1 import { BehaviorSubject, Observable } from 'rxjs'; import { map, mergeMap, take, tap } from 'rxjs/operators'; @@ -36,6 +40,10 @@ 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'; +<<<<<<< HEAD +======= +import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service'; +>>>>>>> dspace-7.6.1 @listableObjectComponent(WorkspaceItemSearchResult, ViewMode.GridElement, Context.AdminWorkflowSearch) @Component({ @@ -46,7 +54,11 @@ import { SupervisionOrderDataService } from '../../../../../core/supervision-ord /** * The component for displaying a grid element for an workflow item on the admin workflow search page */ +<<<<<<< HEAD export class WorkspaceItemSearchResultAdminWorkflowGridElementComponent extends SearchResultGridElementComponent { +======= +export class WorkspaceItemSearchResultAdminWorkflowGridElementComponent extends SearchResultGridElementComponent implements OnInit { +>>>>>>> dspace-7.6.1 /** * The item linked to the workspace item @@ -79,6 +91,10 @@ export class WorkspaceItemSearchResultAdminWorkflowGridElementComponent extends @ViewChild('buttons', { static: true }) buttons: ElementRef; constructor( +<<<<<<< HEAD +======= + public dsoNameService: DSONameService, +>>>>>>> dspace-7.6.1 private componentFactoryResolver: ComponentFactoryResolver, private linkService: LinkService, protected truncatableService: TruncatableService, @@ -86,7 +102,11 @@ export class WorkspaceItemSearchResultAdminWorkflowGridElementComponent extends protected bitstreamDataService: BitstreamDataService, protected supervisionOrderDataService: SupervisionOrderDataService, ) { +<<<<<<< HEAD super(truncatableService, bitstreamDataService); +======= + super(dsoNameService, truncatableService, bitstreamDataService); +>>>>>>> dspace-7.6.1 } /** 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..754dfa7e3d8 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,11 @@ export class WorkflowItemSearchResultAdminWorkflowListElementComponent extends S constructor(private linkService: LinkService, protected truncatableService: TruncatableService, +<<<<<<< HEAD protected dsoNameService: DSONameService, +======= + public dsoNameService: DSONameService, +>>>>>>> dspace-7.6.1 @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..41b26ff5220 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,11 @@ export class WorkspaceItemSearchResultAdminWorkflowListElementComponent extends public supervisionOrder$: BehaviorSubject = new BehaviorSubject([]); constructor(private linkService: LinkService, +<<<<<<< HEAD protected dsoNameService: DSONameService, +======= + public dsoNameService: DSONameService, +>>>>>>> dspace-7.6.1 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 dff2e506c30..b171c21f501 100644 --- a/src/app/admin/admin.module.ts +++ b/src/app/admin/admin.module.ts @@ -10,6 +10,10 @@ import { AdminSearchModule } from './admin-search-page/admin-search.module'; import { AdminSidebarSectionComponent } from './admin-sidebar/admin-sidebar-section/admin-sidebar-section.component'; import { ExpandableAdminSidebarSectionComponent } from './admin-sidebar/expandable-admin-sidebar-section/expandable-admin-sidebar-section.component'; import { BatchImportPageComponent } from './admin-import-batch-page/batch-import-page.component'; +<<<<<<< HEAD +======= +import { UiSwitchModule } from 'ngx-ui-switch'; +>>>>>>> dspace-7.6.1 import { UploadModule } from '../shared/upload/upload.module'; const ENTRY_COMPONENTS = [ @@ -27,6 +31,10 @@ const ENTRY_COMPONENTS = [ AdminSearchModule.withEntryComponents(), AdminWorkflowModuleModule.withEntryComponents(), SharedModule, +<<<<<<< HEAD +======= + UiSwitchModule, +>>>>>>> dspace-7.6.1 UploadModule, ], declarations: [ diff --git a/src/app/app-routing-paths.ts b/src/app/app-routing-paths.ts index f44650e73e4..b83b2137fed 100644 --- a/src/app/app-routing-paths.ts +++ b/src/app/app-routing-paths.ts @@ -125,6 +125,7 @@ export function getRequestCopyModulePath() { return `/${REQUEST_COPY_MODULE_PATH}`; } +<<<<<<< HEAD export const LICENSES_MODULE_PATH = 'licenses'; export function getLicensesModulePath() { return `/${LICENSES_MODULE_PATH}`; @@ -141,6 +142,8 @@ export function getLicenseContractPagePath() { return `/${CONTRACT_PAGE_MODULE_PATH}`; } +======= +>>>>>>> dspace-7.6.1 export const HEALTH_PAGE_PATH = 'health'; export const SUBSCRIPTIONS_MODULE_PATH = 'subscriptions'; diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index d903f6f9e62..caac0f623c6 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -42,8 +42,11 @@ import { import { ServerCheckGuard } from './core/server-check/server-check.guard'; import { MenuResolver } from './menu.resolver'; import { ThemedPageErrorComponent } from './page-error/themed-page-error.component'; +<<<<<<< HEAD import { HANDLE_TABLE_MODULE_PATH } from './handle-page/handle-page-routing-paths'; import { STATIC_PAGE_PATH } from './static-page/static-page-routing-paths'; +======= +>>>>>>> dspace-7.6.1 @NgModule({ imports: [ @@ -218,7 +221,7 @@ import { STATIC_PAGE_PATH } from './static-page/static-page-routing-paths'; { path: REQUEST_COPY_MODULE_PATH, loadChildren: () => import('./request-copy/request-copy.module').then((m) => m.RequestCopyModule), - canActivate: [AuthenticatedGuard, EndUserAgreementCurrentUserGuard] + canActivate: [EndUserAgreementCurrentUserGuard] }, { path: FORBIDDEN_PATH, @@ -227,7 +230,13 @@ import { STATIC_PAGE_PATH } from './static-page/static-page-routing-paths'; { path: 'statistics', loadChildren: () => import('./statistics-page/statistics-page-routing.module') - .then((m) => m.StatisticsPageRoutingModule) + .then((m) => m.StatisticsPageRoutingModule), + canActivate: [EndUserAgreementCurrentUserGuard], + }, + { + path: HEALTH_PAGE_PATH, + loadChildren: () => import('./health-page/health-page.module') + .then((m) => m.HealthPageModule) }, { path: HEALTH_PAGE_PATH, @@ -237,7 +246,13 @@ import { STATIC_PAGE_PATH } from './static-page/static-page-routing-paths'; { path: ACCESS_CONTROL_MODULE_PATH, loadChildren: () => import('./access-control/access-control.module').then((m) => m.AccessControlModule), - canActivate: [GroupAdministratorGuard], + canActivate: [GroupAdministratorGuard, EndUserAgreementCurrentUserGuard], + }, + { + path: 'subscriptions', + loadChildren: () => import('./subscriptions-page/subscriptions-page-routing.module') + .then((m) => m.SubscriptionsPageRoutingModule), + canActivate: [AuthenticatedGuard] }, { path: 'subscriptions', diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 04673a89484..938b1798857 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,12 +1,20 @@ import { APP_BASE_HREF, CommonModule, DOCUMENT } from '@angular/common'; import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http'; +<<<<<<< HEAD import { NgModule} from '@angular/core'; +======= +import { NgModule } from '@angular/core'; +>>>>>>> dspace-7.6.1 import { BrowserModule } from '@angular/platform-browser'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { EffectsModule } from '@ngrx/effects'; import { RouterStateSerializer, StoreRouterConnectingModule } from '@ngrx/router-store'; +<<<<<<< HEAD import { MetaReducer, StoreModule, USER_PROVIDED_META_REDUCERS} from '@ngrx/store'; +======= +import { MetaReducer, StoreModule, USER_PROVIDED_META_REDUCERS } from '@ngrx/store'; +>>>>>>> dspace-7.6.1 import { TranslateModule } from '@ngx-translate/core'; import { ScrollToModule } from '@nicky-lenaers/ngx-scroll-to'; import { DYNAMIC_MATCHER_PROVIDERS } from '@ng-dynamic-forms/core'; @@ -27,12 +35,18 @@ import { LocaleInterceptor } from './core/locale/locale.interceptor'; import { XsrfInterceptor } from './core/xsrf/xsrf.interceptor'; import { LogInterceptor } from './core/log/log.interceptor'; import { EagerThemesModule } from '../themes/eager-themes.module'; +<<<<<<< HEAD import { APP_CONFIG, AppConfig } from '../config/app-config.interface'; import { StoreDevModules } from '../config/store/devtools'; import { RootModule } from './root.module'; import { ScriptLoaderService } from './clarin-navbar-top/script-loader-service'; +======= +import { APP_CONFIG, AppConfig } from '../config/app-config.interface'; +import { StoreDevModules } from '../config/store/devtools'; +import { RootModule } from './root.module'; +>>>>>>> dspace-7.6.1 export function getConfig() { return environment; @@ -82,7 +96,10 @@ const PROVIDERS = [ useClass: DSpaceRouterStateSerializer }, ClientCookieService, +<<<<<<< HEAD ScriptLoaderService, +======= +>>>>>>> dspace-7.6.1 // register AuthInterceptor as HttpInterceptor { provide: HTTP_INTERCEPTORS, 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 eddc5e345a0..5340c8cf7c4 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,5 +1,9 @@
    +<<<<<<< HEAD

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

    +======= +

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

    +>>>>>>> dspace-7.6.1
    -

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

    +

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

    >>>>>> dspace-7.6.1 } } diff --git a/src/app/collection-page/edit-collection-page/collection-access-control/collection-access-control.component.html b/src/app/collection-page/edit-collection-page/collection-access-control/collection-access-control.component.html new file mode 100644 index 00000000000..4e957cf8671 --- /dev/null +++ b/src/app/collection-page/edit-collection-page/collection-access-control/collection-access-control.component.html @@ -0,0 +1,7 @@ + + + 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..8e8d4be25dc 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,10 @@ describe('CollectionMetadataComponent', () => { { provide: ActivatedRoute, useValue: { parent: { data: observableOf({ dso: createSuccessfulRemoteDataObject(collection) }) } } }, { provide: NotificationsService, useValue: notificationsService }, { provide: RequestService, useValue: requestService }, +<<<<<<< HEAD +======= + { provide: Router, useValue: routerMock} +>>>>>>> dspace-7.6.1 ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); @@ -70,8 +79,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 +95,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)]); }); }); @@ -94,11 +105,19 @@ describe('CollectionMetadataComponent', () => { (itemTemplateService.delete as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$({})); comp.deleteItemTemplate(); }); +<<<<<<< HEAD + + it('should call ItemTemplateService.delete', () => { + expect(itemTemplateService.delete).toHaveBeenCalledWith(template.uuid); + }); + +======= it('should call ItemTemplateService.delete', () => { expect(itemTemplateService.delete).toHaveBeenCalledWith(template.uuid); }); +>>>>>>> dspace-7.6.1 describe('when delete returns a success', () => { it('should display a success notification', () => { expect(notificationsService.success).toHaveBeenCalled(); 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..cae5c66e4c3 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,30 @@ export class CollectionMetadataComponent extends ComcolMetadataComponent>>>>>> dspace-7.6.1 ) { super(collectionDataService, router, route, notificationsService, translate); } + /** + * Cheking if the navigation is done and if so, initialize the collection's item template, + * to ensure that the item template is always up to date. + * Check when a NavigationEnd event (URL change) or a Scroll event followed by a NavigationEnd event (refresh event), occurs + */ ngOnInit(): void { - super.ngOnInit(); - this.initTemplateItem(); + this.router.events.subscribe((event) => { + 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..5e869872683 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,11 @@ 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'; +<<<<<<< HEAD +======= +import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; +import { DSONameServiceMock } from '../../../shared/mocks/dso-name.service.mock'; +>>>>>>> dspace-7.6.1 describe('CollectionRolesComponent', () => { @@ -79,6 +84,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 fbaac87ed62..e7e98d95233 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 512faa53118..2d1308cc83a 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 @@ -14,7 +14,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 } from 'rxjs'; @@ -202,7 +202,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..9cdc4571c7f 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,10 @@ import { ComcolModule } from '../../shared/comcol/comcol.module'; ResourcePoliciesModule, FormModule, ComcolModule, +<<<<<<< HEAD +======= + AccessControlFormModule, +>>>>>>> dspace-7.6.1 ], declarations: [ EditCollectionPageComponent, @@ -33,7 +41,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..c8b93d43cdc 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,11 @@
    +<<<<<<< HEAD

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

    +======= +

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

    +>>>>>>> dspace-7.6.1
    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..8e67745b9b0 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,8 @@
    +<<<<<<< HEAD

    {{ 'communityList.title' | translate }}

    +======= +

    {{ 'communityList.title' | translate }}

    +>>>>>>> dspace-7.6.1
    diff --git a/src/app/community-list-page/community-list-service.ts b/src/app/community-list-page/community-list-service.ts index 99e9dbeb0de..54cb052d452 100644 --- a/src/app/community-list-page/community-list-service.ts +++ b/src/app/community-list-page/community-list-service.ts @@ -24,8 +24,14 @@ 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'; +<<<<<<< HEAD // Helper method to combine an flatten an array of observables of flatNode arrays +======= +import { v4 as uuidv4 } from 'uuid'; + +// Helper method to combine and flatten an array of observables of flatNode arrays +>>>>>>> dspace-7.6.1 export const combineAndFlatten = (obsList: Observable[]): Observable => observableCombineLatest([...obsList]).pipe( map((matrix: any[][]) => [].concat(...matrix)), @@ -186,7 +192,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 +205,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 +263,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 +281,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..9caae7cc4ff 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,9 @@ +<<<<<<< HEAD +======= + +>>>>>>> dspace-7.6.1 @@ -8,10 +12,17 @@
    +<<<<<<< HEAD {{ 'communityList.showMore' | translate }} +======= + +>>>>>>> dspace-7.6.1
    @@ -25,18 +36,23 @@ class="example-tree-node expandable-node">
    -
    - - {{node.name}} - -
    +
    + + + {{ dsoNameService.getName(node.payload) }} + +   + {{node.payload.archivedItemsCount}} + +
    @@ -65,12 +81,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..c01576513cf 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,11 @@ 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'; +<<<<<<< HEAD +======= +import { RouterLinkWithHref } from '@angular/router'; +import { v4 as uuidv4 } from 'uuid'; +>>>>>>> dspace-7.6.1 describe('CommunityListComponent', () => { let component: CommunityListComponent; @@ -137,7 +142,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 +169,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 +198,8 @@ describe('CommunityListComponent', () => { }, }), CdkTreeModule, - RouterTestingModule], + RouterTestingModule, + RouterLinkWithHref], declarations: [CommunityListComponent], providers: [CommunityListComponent, { provide: CommunityListService, useValue: communityListServiceStub },], @@ -230,9 +236,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 +251,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..8637c9e4d9d 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,10 @@ 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'; +<<<<<<< HEAD +======= +import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; +>>>>>>> dspace-7.6.1 /** * A tree-structured list of nodes representing the communities, their subCommunities and collections. @@ -27,12 +31,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 +60,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 +104,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..923889f0109 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,10 @@ /** * The show more links in the community tree are also represented by a flatNode so we know where in +<<<<<<< HEAD * 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) +>>>>>>> dspace-7.6.1 */ 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 8e06fd2db3d..00775deddca 100644 --- a/src/app/community-page/community-page.component.html +++ b/src/app/community-page/community-page.component.html @@ -5,7 +5,7 @@
    - + @@ -21,9 +21,12 @@
    +<<<<<<< HEAD
    +======= +>>>>>>> dspace-7.6.1
    diff --git a/src/app/community-page/community-page.component.ts b/src/app/community-page/community-page.component.ts index b1a0cfc9466..da20c14f65d 100644 --- a/src/app/community-page/community-page.component.ts +++ b/src/app/community-page/community-page.component.ts @@ -19,6 +19,10 @@ 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'; +<<<<<<< HEAD +======= +import { DSONameService } from '../core/breadcrumbs/dso-name.service'; +>>>>>>> dspace-7.6.1 @Component({ selector: 'ds-community-page', @@ -57,7 +61,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/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..796d775eeae 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 @@ -68,7 +68,11 @@ describe('DsoEditMetadataValueComponent', () => { }); it('should not show a badge', () => { +<<<<<<< HEAD expect(fixture.debugElement.query(By.css('ds-type-badge'))).toBeNull(); +======= + expect(fixture.debugElement.query(By.css('ds-themed-type-badge'))).toBeNull(); +>>>>>>> dspace-7.6.1 }); describe('when no changes have been made', () => { @@ -134,7 +138,11 @@ describe('DsoEditMetadataValueComponent', () => { }); it('should show a badge', () => { +<<<<<<< HEAD expect(fixture.debugElement.query(By.css('ds-type-badge'))).toBeTruthy(); +======= + expect(fixture.debugElement.query(By.css('ds-themed-type-badge'))).toBeTruthy(); +>>>>>>> dspace-7.6.1 }); assertButton(EDIT_BTN, true, true); diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.ts b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.ts index d67a7ea738d..d6008619e8b 100644 --- a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.ts +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.ts @@ -1,5 +1,9 @@ import { Component, Inject, Injector, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; +<<<<<<< HEAD import { AlertType } from '../../shared/alert/aletr-type'; +======= +import { AlertType } from '../../shared/alert/alert-type'; +>>>>>>> dspace-7.6.1 import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { DsoEditMetadataForm } from './dso-edit-metadata-form'; import { map } from 'rxjs/operators'; diff --git a/src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.spec.ts b/src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.spec.ts index e0fde0e8f2b..3fc7d5fd291 100644 --- a/src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.spec.ts +++ b/src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.spec.ts @@ -12,6 +12,10 @@ import { createPaginatedList } from '../../../shared/testing/utils.test'; import { followLink } from '../../../shared/utils/follow-link-config.model'; import { By } from '@angular/platform-browser'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; +<<<<<<< HEAD +======= +import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model'; +>>>>>>> dspace-7.6.1 describe('MetadataFieldSelectorComponent', () => { let component: MetadataFieldSelectorComponent; @@ -79,7 +83,11 @@ describe('MetadataFieldSelectorComponent', () => { }); it('should query the registry service for metadata fields and include the schema', () => { +<<<<<<< HEAD expect(registryService.queryMetadataFields).toHaveBeenCalledWith(query, null, true, false, followLink('schema')); +======= + expect(registryService.queryMetadataFields).toHaveBeenCalledWith(query, { elementsPerPage: 10, sort: new SortOptions('fieldName', SortDirection.ASC) }, true, false, followLink('schema')); +>>>>>>> dspace-7.6.1 }); }); diff --git a/src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.ts b/src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.ts index 5053a4b83dd..325a1b1296f 100644 --- a/src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.ts +++ b/src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.ts @@ -9,21 +9,37 @@ import { Output, ViewChild } from '@angular/core'; +<<<<<<< HEAD import { switchMap, debounceTime, distinctUntilChanged, map, tap, take } from 'rxjs/operators'; import { followLink } from '../../../shared/utils/follow-link-config.model'; import { getAllSucceededRemoteData, getFirstCompletedRemoteData, +======= +import { debounceTime, distinctUntilChanged, map, switchMap, take, tap } from 'rxjs/operators'; +import { followLink } from '../../../shared/utils/follow-link-config.model'; +import { + getAllSucceededRemoteData, + getFirstCompletedRemoteData, +>>>>>>> dspace-7.6.1 metadataFieldsToString } from '../../../core/shared/operators'; import { Observable } from 'rxjs/internal/Observable'; import { RegistryService } from '../../../core/registry/registry.service'; +<<<<<<< HEAD import { FormControl } from '@angular/forms'; +======= +import { UntypedFormControl } from '@angular/forms'; +>>>>>>> dspace-7.6.1 import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; import { hasValue } from '../../../shared/empty.util'; import { Subscription } from 'rxjs/internal/Subscription'; import { of } from 'rxjs/internal/observable/of'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { TranslateService } from '@ngx-translate/core'; +<<<<<<< HEAD +======= +import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model'; +>>>>>>> dspace-7.6.1 @Component({ selector: 'ds-metadata-field-selector', @@ -70,7 +86,11 @@ export class MetadataFieldSelectorComponent implements OnInit, OnDestroy, AfterV /** * FormControl for the input */ +<<<<<<< HEAD public input: FormControl = new FormControl(); +======= + public input: UntypedFormControl = new UntypedFormControl(); +>>>>>>> dspace-7.6.1 /** * The current query to update mdFieldOptions$ for @@ -127,7 +147,11 @@ export class MetadataFieldSelectorComponent implements OnInit, OnDestroy, AfterV switchMap((query: string) => { this.showInvalid = false; if (query !== null) { +<<<<<<< HEAD return this.registryService.queryMetadataFields(query, null, true, false, followLink('schema')).pipe( +======= + return this.registryService.queryMetadataFields(query, { elementsPerPage: 10, sort: new SortOptions('fieldName', SortDirection.ASC) }, true, false, followLink('schema')).pipe( +>>>>>>> dspace-7.6.1 getAllSucceededRemoteData(), metadataFieldsToString(), ); diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.spec.ts b/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.spec.ts index df9e8ea7fd0..6c556167609 100644 --- a/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-grid-element.component.spec.ts +++ b/src/app/entity-groups/journal-entities/item-grid-elements/journal-issue/journal-issue-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('JournalIssueGridElementComponent', () => { imports: [NoopAnimationsModule], declarations: [JournalIssueGridElementComponent, 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-grid-elements/journal-volume/journal-volume-grid-element.component.spec.ts b/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.spec.ts index ac082e7f1b9..14f63d57814 100644 --- a/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-grid-element.component.spec.ts +++ b/src/app/entity-groups/journal-entities/item-grid-elements/journal-volume/journal-volume-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('JournalVolumeGridElementComponent', () => { imports: [NoopAnimationsModule], declarations: [JournalVolumeGridElementComponent, 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-grid-elements/journal/journal-grid-element.component.spec.ts b/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.spec.ts index 93287313aa9..e4a23754419 100644 --- a/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-grid-element.component.spec.ts +++ b/src/app/entity-groups/journal-entities/item-grid-elements/journal/journal-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(), [])), @@ -54,6 +56,7 @@ describe('JournalGridElementComponent', () => { imports: [NoopAnimationsModule], declarations: [JournalGridElementComponent, 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-grid-elements/search-result-grid-elements/journal-issue/journal-issue-search-result-grid-element.component.html b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-issue/journal-issue-search-result-grid-element.component.html index c6c29c2f1a8..fe659ed63ff 100644 --- a/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-issue/journal-issue-search-result-grid-element.component.html +++ b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-issue/journal-issue-search-result-grid-element.component.html @@ -5,21 +5,25 @@
    >>>>>> dspace-7.6.1 class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate">
    - - + +
    - - + +
    diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-volume/journal-volume-search-result-grid-element.component.html b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-volume/journal-volume-search-result-grid-element.component.html index 5849105f96d..d15320aaeb2 100644 --- a/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-volume/journal-volume-search-result-grid-element.component.html +++ b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-volume/journal-volume-search-result-grid-element.component.html @@ -5,21 +5,25 @@
    >>>>>> dspace-7.6.1 class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate">
    - - + +
    - - + +
    diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal/journal-search-result-grid-element.component.html b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal/journal-search-result-grid-element.component.html index 9a3ea95c07c..9c9fdc65c84 100644 --- a/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal/journal-search-result-grid-element.component.html +++ b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal/journal-search-result-grid-element.component.html @@ -5,21 +5,25 @@
    >>>>>> dspace-7.6.1 class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate">
    - - + +
    - - + +
    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 36b7e98c513..4f4648cfbe8 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,11 @@
    +<<<<<<< HEAD + + >>>>>> dspace-7.6.1 [routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out" [innerHTML]="dsoTitle"> >>>>>> dspace-7.6.1 } diff --git a/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal-volume/journal-volume-search-result-list-element.component.html b/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal-volume/journal-volume-search-result-list-element.component.html index 46683d7cdc7..f6f22d74f68 100644 --- a/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal-volume/journal-volume-search-result-list-element.component.html +++ b/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal-volume/journal-volume-search-result-list-element.component.html @@ -1,7 +1,11 @@
    +<<<<<<< HEAD + + >>>>>> dspace-7.6.1 [routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out" [innerHTML]="dsoTitle"> >>>>>> dspace-7.6.1 } diff --git a/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal/journal-search-result-list-element.component.html b/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal/journal-search-result-list-element.component.html index bf09305c862..bc97ebaf480 100644 --- a/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal/journal-search-result-list-element.component.html +++ b/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal/journal-search-result-list-element.component.html @@ -1,6 +1,10 @@
    +<<<<<<< HEAD + + >>>>>> dspace-7.6.1 [routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out" [innerHTML]="dsoTitle"> >>>>>> dspace-7.6.1 } diff --git a/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.html b/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.html index fce2b5c162c..8bd1f16c00e 100644 --- a/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.html +++ b/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.html @@ -1,13 +1,18 @@
    +<<<<<<< HEAD +======= + + +>>>>>>> dspace-7.6.1
    - +
    +<<<<<<< HEAD +======= + + +>>>>>>> dspace-7.6.1
    - +
    +<<<<<<< HEAD +======= + + +>>>>>>> dspace-7.6.1
    - + { 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..3f57fec12ae 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,25 @@
    >>>>>> dspace-7.6.1 class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate">
    - - + +
    - - + +
    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 853a7179657..12c2589ed61 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,25 @@
    >>>>>> dspace-7.6.1 class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate">
    - - + +
    - - + +
    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..26b841125b6 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,25 @@
    >>>>>> dspace-7.6.1 class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate">
    - - + +
    - - + +
    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 0bba83a209f..6e9b6a37b27 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,11 @@
    +<<<<<<< HEAD + + >>>>>> dspace-7.6.1 [routerLink]="[itemPageRoute]" class="lead" [innerHTML]="dsoTitle || ('orgunit.listelement.no-title' | translate)"> >>>>>> dspace-7.6.1 } diff --git a/src/app/entity-groups/research-entities/item-list-elements/search-result-list-elements/person/person-search-result-list-element.component.html b/src/app/entity-groups/research-entities/item-list-elements/search-result-list-elements/person/person-search-result-list-element.component.html index 4332b7a5538..2b4cdcf90c5 100644 --- a/src/app/entity-groups/research-entities/item-list-elements/search-result-list-elements/person/person-search-result-list-element.component.html +++ b/src/app/entity-groups/research-entities/item-list-elements/search-result-list-elements/person/person-search-result-list-element.component.html @@ -1,7 +1,11 @@
    +<<<<<<< HEAD + + >>>>>> dspace-7.6.1 [routerLink]="[itemPageRoute]" class="lead" [innerHTML]="dsoTitle || ('person.listelement.no-title' | translate)"> >>>>>> dspace-7.6.1 @Inject(APP_CONFIG) protected appConfig: AppConfig ) { super(truncatableService, dsoNameService, appConfig); diff --git a/src/app/entity-groups/research-entities/item-list-elements/search-result-list-elements/project/project-search-result-list-element.component.html b/src/app/entity-groups/research-entities/item-list-elements/search-result-list-elements/project/project-search-result-list-element.component.html index 8883a9c80de..a9a9177065b 100644 --- a/src/app/entity-groups/research-entities/item-list-elements/search-result-list-elements/project/project-search-result-list-element.component.html +++ b/src/app/entity-groups/research-entities/item-list-elements/search-result-list-elements/project/project-search-result-list-element.component.html @@ -1,7 +1,11 @@
    >>>>>> dspace-7.6.1 [routerLink]="[itemPageRoute]" class="dont-break-out">
    +<<<<<<< HEAD + >>>>>> dspace-7.6.1 [routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out" [innerHTML]="dsoTitle"> >>>>>> dspace-7.6.1 } diff --git a/src/app/entity-groups/research-entities/item-list-elements/sidebar-search-list-elements/person/person-sidebar-search-list-element.component.ts b/src/app/entity-groups/research-entities/item-list-elements/sidebar-search-list-elements/person/person-sidebar-search-list-element.component.ts index bc9d3d4302f..edae6c9ec9b 100644 --- a/src/app/entity-groups/research-entities/item-list-elements/sidebar-search-list-elements/person/person-sidebar-search-list-element.component.ts +++ b/src/app/entity-groups/research-entities/item-list-elements/sidebar-search-list-elements/person/person-sidebar-search-list-element.component.ts @@ -25,7 +25,7 @@ export class PersonSidebarSearchListElementComponent extends SidebarSearchListEl constructor(protected truncatableService: TruncatableService, protected linkService: LinkService, protected translateService: TranslateService, - protected dsoNameService: DSONameService + public dsoNameService: DSONameService, ) { super(truncatableService, linkService, dsoNameService); } diff --git a/src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.html b/src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.html index 8251f6b465d..7e61f496886 100644 --- a/src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.html +++ b/src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.html @@ -1,18 +1,23 @@
    +<<<<<<< HEAD +======= + + +>>>>>>> dspace-7.6.1
    - - +
    +<<<<<<< HEAD +======= + + +>>>>>>> dspace-7.6.1
    - - +
    +<<<<<<< HEAD +======= + + +>>>>>>> dspace-7.6.1
    - - + - - + 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 680e1bd79f7..95b183f6303 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'; @@ -36,6 +37,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/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 @@
    - +
    >>>>>> dspace-7.6.1 @Inject(APP_CONFIG) protected appConfig: AppConfig ) { super(truncatableService, dsoNameService, appConfig); diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.html b/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.html index 6872b3f609e..29ec2cd702d 100644 --- a/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.html +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.html @@ -1,7 +1,11 @@
    +======= + [attr.rel]="(linkType == linkTypes.ExternalLink) ? 'noopener noreferrer' : null" class="dont-break-out"> +>>>>>>> dspace-7.6.1 >>>>>> dspace-7.6.1 @Inject(APP_CONFIG) protected appConfig: AppConfig ) { super(truncatableService, dsoNameService, appConfig); diff --git a/src/app/footer/footer.component.html b/src/app/footer/footer.component.html index b8babf08944..8562ffa926e 100644 --- a/src/app/footer/footer.component.html +++ b/src/app/footer/footer.component.html @@ -84,6 +84,7 @@

    Services

    m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) })(window,document,'script','//www.google-analytics.com/analytics.js','ga'); +<<<<<<< HEAD // main LINDAT/CLARIAH-CZ tracker ga('create', 'UA-27008245-2', 'cuni.cz'); ga('send', 'pageview'); @@ -112,5 +113,36 @@

    Services

    +======= + + diff --git a/src/app/footer/footer.component.spec.ts b/src/app/footer/footer.component.spec.ts index eda2eb25fd9..74ee4c5e7a3 100644 --- a/src/app/footer/footer.component.spec.ts +++ b/src/app/footer/footer.component.spec.ts @@ -15,9 +15,14 @@ import { FooterComponent } from './footer.component'; import { TranslateLoaderMock } from '../shared/mocks/translate-loader.mock'; import { storeModuleConfig } from '../app.reducer'; +<<<<<<< HEAD import { ConfigurationDataService } from '../core/data/configuration-data.service'; import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; import { ConfigurationProperty } from '../core/shared/configuration-property.model'; +======= +import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service'; +import { AuthorizationDataServiceStub } from '../shared/testing/authorization-service.stub'; +>>>>>>> dspace-7.6.1 let comp: FooterComponent; let fixture: ComponentFixture; @@ -47,7 +52,11 @@ describe('Footer component', () => { declarations: [FooterComponent], // declare the test component providers: [ FooterComponent, +<<<<<<< HEAD { provide: ConfigurationDataService, useValue: mockConfigurationDataService } +======= + { provide: AuthorizationDataService, useClass: AuthorizationDataServiceStub }, +>>>>>>> dspace-7.6.1 ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }); diff --git a/src/app/footer/footer.component.ts b/src/app/footer/footer.component.ts index 592bc34b81f..46dadf707d8 100644 --- a/src/app/footer/footer.component.ts +++ b/src/app/footer/footer.component.ts @@ -3,9 +3,14 @@ import { hasValue } from '../shared/empty.util'; import { KlaroService } from '../shared/cookies/klaro.service'; import { environment } from '../../environments/environment'; import { Observable } from 'rxjs'; +<<<<<<< HEAD import { RemoteData } from '../core/data/remote-data'; import { ConfigurationProperty } from '../core/shared/configuration-property.model'; import { ConfigurationDataService } from '../core/data/configuration-data.service'; +======= +import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service'; +import { FeatureID } from '../core/data/feature-authorization/feature-id'; +>>>>>>> dspace-7.6.1 @Component({ selector: 'ds-footer', @@ -21,6 +26,7 @@ export class FooterComponent implements OnInit { showTopFooter = false; showPrivacyPolicy = environment.info.enablePrivacyStatement; showEndUserAgreement = environment.info.enableEndUserAgreement; +<<<<<<< HEAD /** * The company url which customized this DSpace with redirection to the DSpace section @@ -38,6 +44,15 @@ export class FooterComponent implements OnInit { ngOnInit(): void { this.loadThemedByProps(); +======= + showSendFeedback$: Observable; + + constructor( + @Optional() private cookies: KlaroService, + private authorizationService: AuthorizationDataService, + ) { + this.showSendFeedback$ = this.authorizationService.isAuthorized(FeatureID.CanSendFeedback); +>>>>>>> dspace-7.6.1 } showCookieSettings() { diff --git a/src/app/footer/themed-footer.component.ts b/src/app/footer/themed-footer.component.ts index c52a0af29f5..e8f64f3434b 100644 --- a/src/app/footer/themed-footer.component.ts +++ b/src/app/footer/themed-footer.component.ts @@ -7,7 +7,7 @@ import { FooterComponent } from './footer.component'; */ @Component({ selector: 'ds-themed-footer', - styleUrls: ['footer.component.scss'], + styleUrls: [], templateUrl: '../shared/theme-support/themed.component.html', }) export class ThemedFooterComponent extends ThemedComponent { @@ -20,6 +20,6 @@ export class ThemedFooterComponent extends ThemedComponent { } protected importUnthemedComponent(): Promise { - return import(`./footer.component`); + return import('./footer.component'); } } diff --git a/src/app/forgot-password/forgot-password-email/forgot-email.component.html b/src/app/forgot-password/forgot-password-email/forgot-email.component.html index 995108cdbc3..6e66168d173 100644 --- a/src/app/forgot-password/forgot-password-email/forgot-email.component.html +++ b/src/app/forgot-password/forgot-password-email/forgot-email.component.html @@ -1,3 +1,9 @@ +<<<<<<< HEAD +======= + + +>>>>>>> dspace-7.6.1 diff --git a/src/app/forgot-password/forgot-password-form/forgot-password-form.component.spec.ts b/src/app/forgot-password/forgot-password-form/forgot-password-form.component.spec.ts index 68566efaecb..92d72d83df6 100644 --- a/src/app/forgot-password/forgot-password-form/forgot-password-form.component.spec.ts +++ b/src/app/forgot-password/forgot-password-form/forgot-password-form.component.spec.ts @@ -5,7 +5,7 @@ import { NotificationsServiceStub } from '../../shared/testing/notifications-ser import { CommonModule } from '@angular/common'; import { RouterTestingModule } from '@angular/router/testing'; import { TranslateModule } from '@ngx-translate/core'; -import { FormBuilder, ReactiveFormsModule } from '@angular/forms'; +import { UntypedFormBuilder, ReactiveFormsModule } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { Store } from '@ngrx/store'; import { EPersonDataService } from '../../core/eperson/eperson-data.service'; @@ -60,7 +60,7 @@ describe('ForgotPasswordFormComponent', () => { {provide: ActivatedRoute, useValue: route}, {provide: Store, useValue: store}, {provide: EPersonDataService, useValue: ePersonDataService}, - {provide: FormBuilder, useValue: new FormBuilder()}, + {provide: UntypedFormBuilder, useValue: new UntypedFormBuilder()}, {provide: NotificationsService, useValue: notificationsService}, ], schemas: [CUSTOM_ELEMENTS_SCHEMA] diff --git a/src/app/header-nav-wrapper/header-navbar-wrapper.component.html b/src/app/header-nav-wrapper/header-navbar-wrapper.component.html index f99070b738f..5756ad32b0b 100644 --- a/src/app/header-nav-wrapper/header-navbar-wrapper.component.html +++ b/src/app/header-nav-wrapper/header-navbar-wrapper.component.html @@ -1,4 +1,4 @@ -
    +
    diff --git a/src/app/header-nav-wrapper/header-navbar-wrapper.component.scss b/src/app/header-nav-wrapper/header-navbar-wrapper.component.scss index b297979fd05..c1bc9c7e909 100644 --- a/src/app/header-nav-wrapper/header-navbar-wrapper.component.scss +++ b/src/app/header-nav-wrapper/header-navbar-wrapper.component.scss @@ -1,7 +1,6 @@ -@media screen and (max-width: map-get($grid-breakpoints, md)) { - :host.open { - background-color: var(--bs-white); - top: 0; - position: sticky; - } +:host { + position: relative; + div#header-navbar-wrapper { + border-bottom: 1px var(--ds-header-navbar-border-bottom-color) solid; + } } diff --git a/src/app/header-nav-wrapper/themed-header-navbar-wrapper.component.scss b/src/app/header-nav-wrapper/themed-header-navbar-wrapper.component.scss deleted file mode 100644 index db392096aaf..00000000000 --- a/src/app/header-nav-wrapper/themed-header-navbar-wrapper.component.scss +++ /dev/null @@ -1,3 +0,0 @@ -:host { - z-index: var(--ds-nav-z-index); -} diff --git a/src/app/header-nav-wrapper/themed-header-navbar-wrapper.component.ts b/src/app/header-nav-wrapper/themed-header-navbar-wrapper.component.ts index 7f9c181fe2e..02d09c44ef4 100644 --- a/src/app/header-nav-wrapper/themed-header-navbar-wrapper.component.ts +++ b/src/app/header-nav-wrapper/themed-header-navbar-wrapper.component.ts @@ -3,11 +3,11 @@ import { ThemedComponent } from '../shared/theme-support/themed.component'; import { HeaderNavbarWrapperComponent } from './header-navbar-wrapper.component'; /** - * Themed wrapper for BreadcrumbsComponent + * Themed wrapper for {@link HeaderNavbarWrapperComponent} */ @Component({ selector: 'ds-themed-header-navbar-wrapper', - styleUrls: ['./themed-header-navbar-wrapper.component.scss'], + styleUrls: [], templateUrl: '../shared/theme-support/themed.component.html', }) export class ThemedHeaderNavbarWrapperComponent extends ThemedComponent { @@ -20,6 +20,6 @@ export class ThemedHeaderNavbarWrapperComponent extends ThemedComponent { - return import(`./header-navbar-wrapper.component`); + return import('./header-navbar-wrapper.component'); } } diff --git a/src/app/header/context-help-toggle/context-help-toggle.component.ts b/src/app/header/context-help-toggle/context-help-toggle.component.ts index 6685df71063..7ee6ed6178c 100644 --- a/src/app/header/context-help-toggle/context-help-toggle.component.ts +++ b/src/app/header/context-help-toggle/context-help-toggle.component.ts @@ -1,6 +1,12 @@ +<<<<<<< HEAD import { Component, OnInit } from '@angular/core'; import { ContextHelpService } from '../../shared/context-help.service'; import { Observable } from 'rxjs'; +======= +import { Component, OnInit, ElementRef } from '@angular/core'; +import { ContextHelpService } from '../../shared/context-help.service'; +import { Observable, Subscription } from 'rxjs'; +>>>>>>> dspace-7.6.1 import { map } from 'rxjs/operators'; /** @@ -15,12 +21,32 @@ import { map } from 'rxjs/operators'; export class ContextHelpToggleComponent implements OnInit { buttonVisible$: Observable; +<<<<<<< HEAD constructor( private contextHelpService: ContextHelpService, ) { } ngOnInit(): void { this.buttonVisible$ = this.contextHelpService.tooltipCount$().pipe(map(x => x > 0)); +======= + subscriptions: Subscription[] = []; + + constructor( + protected elRef: ElementRef, + protected contextHelpService: ContextHelpService, + ) { + } + + ngOnInit(): void { + this.buttonVisible$ = this.contextHelpService.tooltipCount$().pipe(map(x => x > 0)); + this.subscriptions.push(this.buttonVisible$.subscribe((showContextHelpToggle: boolean) => { + if (showContextHelpToggle) { + this.elRef.nativeElement.classList.remove('d-none'); + } else { + this.elRef.nativeElement.classList.add('d-none'); + } + })); +>>>>>>> dspace-7.6.1 } onClick() { diff --git a/src/app/header/header.component.html b/src/app/header/header.component.html index 2ea5209dd1e..1b01e64e6ef 100644 --- a/src/app/header/header.component.html +++ b/src/app/header/header.component.html @@ -9,12 +9,16 @@
    {{messagePrefix + '.table.id' | translate}}
    {{ePerson.eperson.id}}{{ePerson.eperson.name}}
    {{eperson.id}} - {{messagePrefix + '.table.email' | translate}}: {{ ePerson.eperson.email ? ePerson.eperson.email : '-' }}
    - {{messagePrefix + '.table.netid' | translate}}: {{ ePerson.eperson.netid ? ePerson.eperson.netid : '-' }} + + {{ dsoNameService.getName(eperson) }} + +
    + {{messagePrefix + '.table.email' | translate}}: {{ eperson.email ? eperson.email : '-' }}
    + {{messagePrefix + '.table.netid' | translate}}: {{ eperson.netid ? eperson.netid : '-' }}
    +<<<<<<< HEAD
    @@ -148,9 +202,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 b7536177cdf..20ab5e1bbc3 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'; describe('MembersListComponent', () => { let component: MembersListComponent; @@ -37,28 +39,31 @@ 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, +<<<<<<< HEAD subgroupMembers: subgroupMembers, findListByHref(href: string): Observable>> { +======= + epersonNonMembers: epersonNonMembers, + // This method is used to get all the current members + findListByHref(_href: string): Observable>> { +>>>>>>> dspace-7.6.1 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(), [])); }, @@ -75,22 +80,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() { @@ -103,14 +108,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')); } }; @@ -118,7 +123,7 @@ describe('MembersListComponent', () => { translateService = getMockTranslateService(); paginationService = new PaginationServiceStub(); - TestBed.configureTestingModule({ + return TestBed.configureTestingModule({ imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, TranslateModule.forRoot({ loader: { @@ -135,6 +140,7 @@ describe('MembersListComponent', () => { { provide: FormBuilderService, useValue: builderService }, { provide: Router, useValue: new RouterMock() }, { provide: PaginationService, useValue: paginationService }, + { provide: DSONameService, useValue: new DSONameServiceMock() }, ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); @@ -147,6 +153,7 @@ describe('MembersListComponent', () => { }); afterEach(fakeAsync(() => { fixture.destroy(); + fixture.debugElement.nativeElement.remove(); flush(); component = null; fixture.debugElement.nativeElement.remove(); @@ -156,19 +163,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(); @@ -176,69 +207,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 58d252f0b4a..42c62ce4612 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,40 +1,78 @@ 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'; +<<<<<<< HEAD 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'; +>>>>>>> dspace-7.6.1 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 { +<<<<<<< HEAD getFirstSucceededRemoteData, +======= +>>>>>>> dspace-7.6.1 getFirstCompletedRemoteData, getAllCompletedRemoteData, getRemoteDataPayload } from '../../../../core/shared/operators'; import { NotificationsService } from '../../../../shared/notifications/notifications.service'; import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model'; +<<<<<<< HEAD import { EpersonDtoModel } from '../../../../core/eperson/models/eperson-dto.model'; +======= +>>>>>>> dspace-7.6.1 import { PaginationService } from '../../../../core/pagination/pagination.service'; +import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; /** * Keys to keep track of specific subscriptions */ enum SubKey { ActiveGroup, - MembersDTO, - SearchResultsDTO, + Members, + SearchResults, +} + +/** + * The layout config of the buttons in the last column + */ +export interface EPersonActionConfig { + /** + * The css classes that should be added to the button + */ + css?: string; + /** + * Whether the button should be disabled + */ + disabled: boolean; + /** + * The Font Awesome icon that should be used + */ + icon: string; +} + +/** + * The {@link EPersonActionConfig} that should be used to display the button. The remove config will be used when the + * {@link EPerson} is already a member of the {@link Group} and the remove config will be used otherwise. + * + * *See {@link actionConfig} for an example* + */ +export interface EPersonListActionConfig { + add: EPersonActionConfig; + remove: EPersonActionConfig; } /** @@ -95,11 +133,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 @@ -128,7 +166,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; @@ -141,23 +178,29 @@ export class MembersListComponent implements OnInit, OnDestroy { public ePersonDataService: EPersonDataService, protected translateService: TranslateService, protected notificationsService: NotificationsService, +<<<<<<< HEAD protected formBuilder: FormBuilder, protected paginationService: PaginationService, private router: Router +======= + protected formBuilder: UntypedFormBuilder, + protected paginationService: PaginationService, + protected router: Router, + public dsoNameService: DSONameService, +>>>>>>> dspace-7.6.1 ) { 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: ''}); } })); } @@ -169,8 +212,13 @@ export class MembersListComponent implements OnInit, OnDestroy { * @private */ retrieveMembers(page: number): void { +<<<<<<< HEAD this.unsubFrom(SubKey.MembersDTO); this.subs.set(SubKey.MembersDTO, +======= + this.unsubFrom(SubKey.Members); + this.subs.set(SubKey.Members, +>>>>>>> dspace-7.6.1 this.paginationService.getCurrentPagination(this.config.id, this.config).pipe( switchMap((currentPagination) => { return this.ePersonDataService.findListByHref(this.groupBeingEdited._links.epersons.href, { @@ -187,6 +235,7 @@ export class MembersListComponent implements OnInit, OnDestroy { return rd; } }), +<<<<<<< HEAD switchMap((epersonListRD: RemoteData>) => { const dtos$ = observableCombineLatest([...epersonListRD.payload.page.map((member: EPerson) => { const dto$: Observable = observableCombineLatest( @@ -228,6 +277,12 @@ export class MembersListComponent implements OnInit, OnDestroy { return observableOf(false); } })); +======= + getRemoteDataPayload()) + .subscribe((paginatedListOfEPersons: PaginatedList) => { + this.ePeopleMembersOfGroup.next(paginatedListOfEPersons); + })); +>>>>>>> dspace-7.6.1 } /** @@ -246,13 +301,23 @@ 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) { +<<<<<<< HEAD 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}); + } +>>>>>>> dspace-7.6.1 } else { this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup')); } @@ -261,14 +326,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')); } @@ -276,37 +345,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) => { @@ -316,6 +373,7 @@ export class MembersListComponent implements OnInit, OnDestroy { return rd; } }), +<<<<<<< HEAD switchMap((epersonListRD: RemoteData>) => { const dtos$ = observableCombineLatest([...epersonListRD.payload.page.map((member: EPerson) => { const dto$: Observable = observableCombineLatest( @@ -333,6 +391,11 @@ export class MembersListComponent implements OnInit, OnDestroy { })) .subscribe((paginatedListOfDTOs: PaginatedList) => { this.ePeopleSearchDtos.next(paginatedListOfDTOs); +======= + getRemoteDataPayload()) + .subscribe((paginatedListOfEPersons: PaginatedList) => { + this.ePeopleSearch.next(paginatedListOfEPersons); +>>>>>>> dspace-7.6.1 })); } 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..301319941f1 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,58 @@

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

    +<<<<<<< HEAD +======= +

    {{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)}} +
    + +
    +
    +
    +
    + + + +>>>>>>> dspace-7.6.1
    {{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 +132,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 1ca6c88c5f7..d6f539e94b8 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'; describe('SubgroupsListComponent', () => { let component: SubgroupsListComponent; @@ -45,44 +38,79 @@ 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; }, +<<<<<<< HEAD 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())); +>>>>>>> dspace-7.6.1 }, 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() { @@ -91,12 +119,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')); } }; @@ -105,7 +136,7 @@ describe('SubgroupsListComponent', () => { translateService = getMockTranslateService(); paginationService = new PaginationServiceStub(); - TestBed.configureTestingModule({ + return TestBed.configureTestingModule({ imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, TranslateModule.forRoot({ loader: { @@ -116,6 +147,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 }, @@ -133,6 +165,7 @@ describe('SubgroupsListComponent', () => { }); afterEach(fakeAsync(() => { fixture.destroy(); + fixture.debugElement.nativeElement.remove(); flush(); component = null; })); @@ -141,86 +174,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 5f1700e07d5..187df6ddc40 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'; /** * Keys to keep track of specific subscriptions @@ -86,9 +86,11 @@ 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) { + private router: Router, + public dsoNameService: DSONameService, + ) { this.currentSearchQuery = ''; } @@ -100,6 +102,7 @@ export class SubgroupsListComponent implements OnInit, OnDestroy { if (activeGroup != null) { this.groupBeingEdited = activeGroup; this.retrieveSubGroups(); + this.search({query: ''}); } })); } @@ -129,6 +132,7 @@ export class SubgroupsListComponent implements OnInit, OnDestroy { } /** +<<<<<<< HEAD * 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 */ @@ -170,6 +174,8 @@ export class SubgroupsListComponent implements OnInit, OnDestroy { } /** +======= +>>>>>>> dspace-7.6.1 * 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 */ @@ -177,7 +183,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')); } @@ -193,7 +204,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')); } @@ -204,28 +220,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..6d1ea7879a0 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 239939e70d8..1c5d4a2aa63 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,8 +32,13 @@ 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'; +<<<<<<< HEAD +======= +import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; +import { DSONameServiceMock, UNDEFINED_NAME } from '../../shared/mocks/dso-name.service.mock'; +>>>>>>> dspace-7.6.1 -describe('GroupRegistryComponent', () => { +describe('GroupsRegistryComponent', () => { let component: GroupsRegistryComponent; let fixture: ComponentFixture; let ePersonDataServiceStub: any; @@ -160,7 +165,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: { @@ -171,6 +176,7 @@ describe('GroupRegistryComponent', () => { ], declarations: [GroupsRegistryComponent], providers: [GroupsRegistryComponent, + { provide: DSONameService, useValue: new DSONameServiceMock() }, { provide: EPersonDataService, useValue: ePersonDataServiceStub }, { provide: GroupDataService, useValue: groupsDataServiceStub }, { provide: DSpaceObjectDataService, useValue: dsoDataServiceStub }, @@ -208,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 70c9b22852f..9a0937f261e 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'; @Component({ selector: 'ds-groups-registry', @@ -99,12 +100,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, - public requestService: RequestService) { + public requestService: RequestService, + public dsoNameService: DSONameService, + ) { this.currentSearchQuery = ''; this.searchForm = this.formBuilder.group(({ query: this.currentSearchQuery, @@ -201,10 +204,14 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy { .subscribe((rd: RemoteData) => { if (rd.hasSucceeded) { this.deletedGroupsIds = [...this.deletedGroupsIds, group.group.id]; +<<<<<<< HEAD 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) })); +>>>>>>> dspace-7.6.1 } 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 })); } }); @@ -213,18 +220,36 @@ 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>> { +<<<<<<< HEAD return this.ePersonDataService.findListByHref(group._links.epersons.href).pipe(getFirstSucceededRemoteData()); +======= + return this.ePersonDataService.findListByHref(group._links.epersons.href, { + currentPage: 1, + elementsPerPage: 1, + }).pipe(getFirstSucceededRemoteData()); +>>>>>>> dspace-7.6.1 } /** * 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>> { +<<<<<<< HEAD return this.groupService.findListByHref(group._links.subgroups.href).pipe(getFirstSucceededRemoteData()); +======= + return this.groupService.findListByHref(group._links.subgroups.href, { + currentPage: 1, + elementsPerPage: 1, + }).pipe(getFirstSucceededRemoteData()); +>>>>>>> dspace-7.6.1 } /** 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..7af3128bce1 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,36 @@ +<<<<<<< HEAD + + {{'admin.batch-import.page.toggle.help' | translate}} + + + + >>>>>> dspace-7.6.1 (onFileAdded)="setFile($event)" [dropMessageLabel]="'admin.batch-import.page.dropMsg'" [dropMessageLabelReplacement]="'admin.batch-import.page.dropMsgReplace'"> +<<<<<<< HEAD +======= +
    + +
    + +>>>>>>> dspace-7.6.1
    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..094b2feceec 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,24 @@ describe('BatchImportPageComponent', () => { let fileMock: File; beforeEach(() => { +<<<<<<< HEAD +======= + component.isUpload = true; +>>>>>>> dspace-7.6.1 fileMock = new File([''], 'filename.zip', { type: 'application/zip' }); component.setFile(fileMock); }); +<<<<<<< HEAD +======= + 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(); + }); + +>>>>>>> dspace-7.6.1 describe('if proceed button is pressed without validate only', () => { beforeEach(fakeAsync(() => { component.validateOnly = false; @@ -99,9 +113,15 @@ describe('BatchImportPageComponent', () => { })); it('metadata-import script is invoked with --zip fileName and the mockFile', () => { const parameterValues: ProcessParameter[] = [ +<<<<<<< HEAD Object.assign(new ProcessParameter(), { name: '--zip', value: 'filename.zip' }), ]; parameterValues.push(Object.assign(new ProcessParameter(), { name: '--add' })); +======= + Object.assign(new ProcessParameter(), { name: '--add' }), + Object.assign(new ProcessParameter(), { name: '--zip', value: 'filename.zip' }) + ]; +>>>>>>> dspace-7.6.1 expect(scriptService.invoke).toHaveBeenCalledWith(BATCH_IMPORT_SCRIPT_NAME, parameterValues, [fileMock]); }); it('success notification is shown', () => { @@ -121,8 +141,13 @@ describe('BatchImportPageComponent', () => { })); it('metadata-import script is invoked with --zip fileName and the mockFile and -v validate-only', () => { const parameterValues: ProcessParameter[] = [ +<<<<<<< HEAD Object.assign(new ProcessParameter(), { name: '--zip', value: 'filename.zip' }), Object.assign(new ProcessParameter(), { name: '--add' }), +======= + Object.assign(new ProcessParameter(), { name: '--add' }), + Object.assign(new ProcessParameter(), { name: '--zip', value: 'filename.zip' }), +>>>>>>> dspace-7.6.1 Object.assign(new ProcessParameter(), { name: '-v', value: true }), ]; expect(scriptService.invoke).toHaveBeenCalledWith(BATCH_IMPORT_SCRIPT_NAME, parameterValues, [fileMock]); @@ -148,4 +173,80 @@ describe('BatchImportPageComponent', () => { }); }); }); +<<<<<<< HEAD +======= + + 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(); + }); + }); + }); +>>>>>>> dspace-7.6.1 }); 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..6a4deee5dce 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,11 @@ 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'; +<<<<<<< HEAD import { isNotEmpty } from '../../shared/empty.util'; +======= +import { isEmpty, isNotEmpty } from '../../shared/empty.util'; +>>>>>>> dspace-7.6.1 import { getProcessDetailRoute } from '../../process-page/process-page-routing.paths'; import { ImportBatchSelectorComponent @@ -32,11 +36,28 @@ export class BatchImportPageComponent { * The validate only flag */ validateOnly = true; +<<<<<<< HEAD +======= + +>>>>>>> dspace-7.6.1 /** * dso object for community or collection */ dso: DSpaceObject = null; +<<<<<<< HEAD +======= + /** + * The flag between upload and url + */ + isUpload = true; + + /** + * File URL when flag is for url + */ + fileURL: string; + +>>>>>>> dspace-7.6.1 public constructor(private location: Location, protected translate: TranslateService, protected notificationsService: NotificationsService, @@ -72,6 +93,7 @@ export class BatchImportPageComponent { * Starts import-metadata script with --zip fileName (and the selected file) */ public importMetadata() { +<<<<<<< HEAD if (this.fileObject == null) { this.notificationsService.error(this.translate.get('admin.metadata-import.page.error.addFile')); } else { @@ -79,6 +101,24 @@ export class BatchImportPageComponent { Object.assign(new ProcessParameter(), { name: '--zip', value: this.fileObject.name }), Object.assign(new ProcessParameter(), { name: '--add' }) ]; +======= + 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: '--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 })); + } +>>>>>>> dspace-7.6.1 if (this.dso) { parameterValues.push(Object.assign(new ProcessParameter(), { name: '--collection', value: this.dso.uuid })); } @@ -97,9 +137,21 @@ export class BatchImportPageComponent { this.router.navigateByUrl(getProcessDetailRoute(rd.payload.processId)); } } else { +<<<<<<< HEAD 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); + } +>>>>>>> dspace-7.6.1 } }); } @@ -121,4 +173,14 @@ export class BatchImportPageComponent { removeDspaceObject(): void { this.dso = null; } +<<<<<<< HEAD +======= + + /** + * toggle the flag between upload and url + */ + toggleUpload() { + this.isUpload = !this.isUpload; + } +>>>>>>> dspace-7.6.1 } 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..f1993f38d6a 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,11 @@
    +<<<<<<< HEAD
    -
    - {{field.id}}{{schema?.prefix}}.{{field.element}}{{field.qualifier}}{{schema?.prefix}}.{{field.element}}{{field.qualifier ? '.' + field.qualifier : ''}}{{field.scopeNote}}
    + + + + + + + + + +
    {{mdEntry.key}}{{mdValue.value}}{{mdValue.language}}
    +
    + + + +
    +
    + +
    +>>>>>>> dspace-7.6.1

    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 3ab1b7b051d..75d08ec3021 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, TranslateService } 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'; @@ -11,7 +11,11 @@ import { ActivatedRouteStub } from '../../shared/testing/active-router.stub'; import { VarDirective } from '../../shared/utils/var.directive'; import { RouterTestingModule } from '@angular/router/testing'; import { Item } from '../../core/shared/item.model'; +<<<<<<< HEAD import { BehaviorSubject, of, of as observableOf } from 'rxjs'; +======= +import { BehaviorSubject, of as observableOf } from 'rxjs'; +>>>>>>> dspace-7.6.1 import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { By } from '@angular/platform-browser'; import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; @@ -20,6 +24,7 @@ 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'; +<<<<<<< HEAD import { RegistryService } from 'src/app/core/registry/registry.service'; import { Store } from '@ngrx/store'; import { NotificationsService } from 'src/app/shared/notifications/notifications.service'; @@ -30,6 +35,11 @@ import { getMockTranslateService } from 'src/app/shared/mocks/translate.service. import { ConfigurationProperty } from '../../core/shared/configuration-property.model'; import { HALEndpointService } from '../../core/shared/hal-endpoint.service'; import { cold } from 'jasmine-marbles'; +======= +import { ServerResponseService } from '../../core/services/server-response.service'; +import { SignpostingDataService } from '../../core/data/signposting-data.service'; +import { LinkHeadService } from '../../core/services/link-head.service'; +>>>>>>> dspace-7.6.1 const mockItem: Item = Object.assign(new Item(), { bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])), @@ -66,6 +76,24 @@ describe('FullItemPageComponent', () => { let routeStub: ActivatedRouteStub; let routeData; let authorizationDataService: AuthorizationDataService; +<<<<<<< HEAD +======= + 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' + }; +>>>>>>> dspace-7.6.1 beforeEach(waitForAsync(() => { authService = jasmine.createSpyObj('authService', { @@ -85,6 +113,7 @@ describe('FullItemPageComponent', () => { isAuthorized: observableOf(false), }); +<<<<<<< HEAD const mockMetadataBitstreamDataService = { searchByHandleParams: () => of({}) // Returns a mock Observable }; @@ -105,6 +134,21 @@ describe('FullItemPageComponent', () => { translateService = getMockTranslateService(); +======= + 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'), + }); + +>>>>>>> dspace-7.6.1 TestBed.configureTestingModule({ imports: [TranslateModule.forRoot({ loader: { @@ -119,6 +163,7 @@ describe('FullItemPageComponent', () => { { provide: MetadataService, useValue: metadataServiceStub }, { provide: AuthService, useValue: authService }, { provide: AuthorizationDataService, useValue: authorizationDataService }, +<<<<<<< HEAD { provide: MetadataBitstreamDataService, useValue: mockMetadataBitstreamDataService }, { provide: Store, useValue: {} }, { provide: NotificationsService, useValue: {} }, @@ -126,8 +171,13 @@ describe('FullItemPageComponent', () => { { provide: MetadataFieldDataService, useValue: {} }, { provide: HALEndpointService, useValue: halService }, RegistryService +======= + { provide: ServerResponseService, useValue: serverResponseService }, + { provide: SignpostingDataService, useValue: signpostingDataService }, + { provide: LinkHeadService, useValue: linkHeadService }, + { provide: PLATFORM_ID, useValue: 'server' } +>>>>>>> dspace-7.6.1 ], - schemas: [NO_ERRORS_SCHEMA] }).overrideComponent(FullItemPageComponent, { set: { changeDetection: ChangeDetectionStrategy.Default } @@ -141,24 +191,35 @@ 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); } }); it('should show simple view button when not originated from workflow item', () => { +<<<<<<< HEAD waitForAsync(() => { expect(comp.fromSubmissionObject).toBe(false); const simpleViewBtn = fixture.debugElement.query(By.css('.simple-view-link')); expect(simpleViewBtn).toBeTruthy(); }); +======= + expect(comp.fromSubmissionObject).toBe(false); + const simpleViewBtn = fixture.debugElement.query(By.css('.simple-view-link')); + expect(simpleViewBtn).toBeTruthy(); +>>>>>>> dspace-7.6.1 }); it('should not show simple view button when originated from workflow', fakeAsync(() => { routeData.wfi = createSuccessfulRemoteDataObject$({ id: 'wfiId'}); comp.ngOnInit(); +<<<<<<< HEAD waitForAsync(() => { fixture.detectChanges(); fixture.whenStable().then(() => { @@ -166,6 +227,13 @@ describe('FullItemPageComponent', () => { const simpleViewBtn = fixture.debugElement.query(By.css('.simple-view-link')); expect(simpleViewBtn).toBeFalsy(); }); +======= + fixture.detectChanges(); + fixture.whenStable().then(() => { + expect(comp.fromSubmissionObject).toBe(true); + const simpleViewBtn = fixture.debugElement.query(By.css('.simple-view-link')); + expect(simpleViewBtn).toBeFalsy(); +>>>>>>> dspace-7.6.1 }); })); @@ -178,7 +246,16 @@ describe('FullItemPageComponent', () => { it('should display the item', () => { const objectLoader = fixture.debugElement.query(By.css('.full-item-info')); +<<<<<<< HEAD expect(objectLoader.nativeElement).toBeDefined(); +======= + expect(objectLoader.nativeElement).not.toBeNull(); + }); + + it('should add the signposting links', () => { + expect(serverResponseService.setHeader).toHaveBeenCalled(); + expect(linkHeadService.addTag).toHaveBeenCalledTimes(2); +>>>>>>> dspace-7.6.1 }); }); describe('when the item is withdrawn and the user is not an admin', () => { @@ -202,7 +279,16 @@ describe('FullItemPageComponent', () => { it('should display the item', () => { const objectLoader = fixture.debugElement.query(By.css('.full-item-info')); +<<<<<<< HEAD expect(objectLoader.nativeElement).toBeDefined(); +======= + expect(objectLoader).not.toBeNull(); + }); + + it('should add the signposting links', () => { + expect(serverResponseService.setHeader).toHaveBeenCalled(); + expect(linkHeadService.addTag).toHaveBeenCalledTimes(2); +>>>>>>> dspace-7.6.1 }); }); @@ -214,7 +300,16 @@ describe('FullItemPageComponent', () => { it('should display the item', () => { const objectLoader = fixture.debugElement.query(By.css('.full-item-info')); +<<<<<<< HEAD expect(objectLoader.nativeElement).toBeDefined(); +======= + expect(objectLoader).not.toBeNull(); + }); + + it('should add the signposting links', () => { + expect(serverResponseService.setHeader).toHaveBeenCalled(); + expect(linkHeadService.addTag).toHaveBeenCalledTimes(2); +>>>>>>> dspace-7.6.1 }); }); }); 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 2b60fb67003..5ca1d5c146f 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,9 +16,15 @@ 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'; +<<<<<<< HEAD import { RegistryService } from 'src/app/core/registry/registry.service'; import { HALEndpointService } from '../../core/shared/hal-endpoint.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'; +>>>>>>> dspace-7.6.1 /** * This component renders a full item page. @@ -45,6 +51,7 @@ export class FullItemPageComponent extends ItemPageComponent implements OnInit, subs = []; +<<<<<<< HEAD constructor(protected route: ActivatedRoute, router: Router, items: ItemDataService, @@ -54,6 +61,21 @@ export class FullItemPageComponent extends ItemPageComponent implements OnInit, private _location: Location, protected halService: HALEndpointService,) { super(route, router, items, authService, authorizationService, registryService, halService); +======= + 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); +>>>>>>> dspace-7.6.1 } /*** AoT inheritance fix, will hopefully be resolved in the near future **/ diff --git a/src/app/item-page/item-page-routing-paths.ts b/src/app/item-page/item-page-routing-paths.ts index e1167766425..835016fb93c 100644 --- a/src/app/item-page/item-page-routing-paths.ts +++ b/src/app/item-page/item-page-routing-paths.ts @@ -57,5 +57,8 @@ export const ITEM_EDIT_VERSIONHISTORY_PATH = 'versionhistory'; export const ITEM_VERSION_PATH = 'version'; export const UPLOAD_BITSTREAM_PATH = 'bitstreams/new'; export const ORCID_PATH = 'orcid'; +<<<<<<< HEAD export const MATOMO_STATISTICS_PATH = 'matomo-statistics'; +======= +>>>>>>> dspace-7.6.1 diff --git a/src/app/item-page/item-page-routing.module.ts b/src/app/item-page/item-page-routing.module.ts index ce3080f94c1..db44fe036b2 100644 --- a/src/app/item-page/item-page-routing.module.ts +++ b/src/app/item-page/item-page-routing.module.ts @@ -7,28 +7,38 @@ import { VersionResolver } from './version-page/version.resolver'; import { DSOBreadcrumbsService } from '../core/breadcrumbs/dso-breadcrumbs.service'; import { LinkService } from '../core/cache/builders/link.service'; import { UploadBitstreamComponent } from './bitstreams/upload/upload-bitstream.component'; +<<<<<<< HEAD import { ITEM_EDIT_PATH, MATOMO_STATISTICS_PATH, ORCID_PATH, TOMBSTONE_ITEM_PATH, UPLOAD_BITSTREAM_PATH } from './item-page-routing-paths'; +======= +import { ITEM_EDIT_PATH, ORCID_PATH, UPLOAD_BITSTREAM_PATH } from './item-page-routing-paths'; +>>>>>>> dspace-7.6.1 import { ItemPageAdministratorGuard } from './item-page-administrator.guard'; import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model'; import { ThemedItemPageComponent } from './simple/themed-item-page.component'; import { ThemedFullItemPageComponent } from './full/themed-full-item-page.component'; import { MenuItemType } from '../shared/menu/menu-item-type.model'; import { VersionPageComponent } from './version-page/version-page/version-page.component'; +<<<<<<< HEAD import {TombstoneComponent} from './tombstone/tombstone.component'; import { ClarinMatomoStatisticsComponent } from './clarin-matomo-statistics/clarin-matomo-statistics.component'; +======= +>>>>>>> dspace-7.6.1 import { BitstreamRequestACopyPageComponent } from './bitstreams/request-a-copy/bitstream-request-a-copy-page.component'; import { REQUEST_COPY_MODULE_PATH } from '../app-routing-paths'; import { OrcidPageComponent } from './orcid-page/orcid-page.component'; import { OrcidPageGuard } from './orcid-page/orcid-page.guard'; import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver'; +<<<<<<< HEAD import { ClarinZipDownloadPageComponent } from '../bitstream-page/clarin-zip-download-page/clarin-zip-download-page.component'; +======= +>>>>>>> dspace-7.6.1 @NgModule({ imports: [ @@ -66,6 +76,7 @@ import { component: BitstreamRequestACopyPageComponent, }, { +<<<<<<< HEAD path: TOMBSTONE_ITEM_PATH, component: TombstoneComponent }, @@ -91,6 +102,12 @@ import { zipDownloadLink: 'This is download link' } }, +======= + path: ORCID_PATH, + component: OrcidPageComponent, + canActivate: [AuthenticatedGuard, OrcidPageGuard] + } +>>>>>>> dspace-7.6.1 ], data: { menu: { diff --git a/src/app/item-page/item-page.module.ts b/src/app/item-page/item-page.module.ts index 6e87e4594a9..d0d5b5fd2ce 100644 --- a/src/app/item-page/item-page.module.ts +++ b/src/app/item-page/item-page.module.ts @@ -10,9 +10,12 @@ import { ItemPageAuthorFieldComponent } from './simple/field-components/specific-field/author/item-page-author-field.component'; import { +<<<<<<< HEAD ItemPageCitationFieldComponent } from './simple/field-components/specific-field/citation/item-page-citation.component'; import { +======= +>>>>>>> dspace-7.6.1 ItemPageDateFieldComponent } from './simple/field-components/specific-field/date/item-page-date-field.component'; import { @@ -37,12 +40,16 @@ 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 { MiradorViewerComponent } from './mirador-viewer/mirador-viewer.component'; import { VersionPageComponent } from './version-page/version-page/version-page.component'; import { ThemedFileSectionComponent } from './simple/field-components/file-section/themed-file-section.component'; +<<<<<<< HEAD import { TombstoneComponent } from './tombstone/tombstone.component'; import { ReplacedTombstoneComponent } from './tombstone/replaced-tombstone/replaced-tombstone.component'; import { WithdrawnTombstoneComponent } from './tombstone/withdrawn-tombstone/withdrawn-tombstone.component'; @@ -57,6 +64,8 @@ import { ChartsModule } from 'ng2-charts'; import { ClarinGenericItemFieldComponent } from './simple/field-components/clarin-generic-item-field/clarin-generic-item-field.component'; import { ClarinCollectionsItemFieldComponent } from './simple/field-components/clarin-collections-item-field/clarin-collections-item-field.component'; import { ClarinFilesItemFieldComponent } from './simple/field-components/clarin-files-item-field/clarin-files-item-field.component'; +======= +>>>>>>> dspace-7.6.1 import { OrcidAuthComponent } from './orcid-page/orcid-auth/orcid-auth.component'; import { OrcidPageComponent } from './orcid-page/orcid-page.component'; import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap'; @@ -70,6 +79,7 @@ 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'; +<<<<<<< HEAD import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import {PreviewSectionComponent} from './simple/field-components/preview-section/preview-section.component'; import { @@ -78,6 +88,12 @@ import { import { FileTreeViewComponent } from './simple/field-components/preview-section/file-description/file-tree-view/file-tree-view.component'; +======= +import { ThemedItemAlertsComponent } from './alerts/themed-item-alerts.component'; +import { + ThemedFullFileSectionComponent +} from './full/field-components/file-section/themed-full-file-section.component'; +>>>>>>> dspace-7.6.1 const ENTRY_COMPONENTS = [ // put only entry components that use custom decorator @@ -101,14 +117,18 @@ const DECLARATIONS = [ ItemPageFieldComponent, CollectionsComponent, FullFileSectionComponent, + ThemedFullFileSectionComponent, PublicationComponent, UntypedItemComponent, ItemComponent, UploadBitstreamComponent, AbstractIncrementalListComponent, MediaViewerComponent, + ThemedMediaViewerComponent, MediaViewerVideoComponent, + ThemedMediaViewerVideoComponent, MediaViewerImageComponent, + ThemedMediaViewerImageComponent, MiradorViewerComponent, VersionPageComponent, OrcidPageComponent, @@ -116,6 +136,7 @@ const DECLARATIONS = [ OrcidSyncSettingsComponent, OrcidQueueComponent, ItemAlertsComponent, +<<<<<<< HEAD BitstreamRequestACopyPageComponent, TombstoneComponent, ReplacedTombstoneComponent, @@ -133,6 +154,10 @@ const DECLARATIONS = [ PreviewSectionComponent, FileDescriptionComponent, FileTreeViewComponent, +======= + ThemedItemAlertsComponent, + BitstreamRequestACopyPageComponent, +>>>>>>> dspace-7.6.1 ]; @NgModule({ @@ -151,15 +176,18 @@ const DECLARATIONS = [ ResultsBackButtonModule, UploadModule, DsoPageModule, +<<<<<<< HEAD ChartsModule, NgbModule +======= +>>>>>>> dspace-7.6.1 ], declarations: [ ...DECLARATIONS, ], exports: [ - ...DECLARATIONS + ...DECLARATIONS, ] }) export class ItemPageModule { diff --git a/src/app/item-page/item-shared.module.ts b/src/app/item-page/item-shared.module.ts index 0249e3cf22a..2342de400c1 100644 --- a/src/app/item-page/item-shared.module.ts +++ b/src/app/item-page/item-shared.module.ts @@ -13,6 +13,12 @@ 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'; +<<<<<<< HEAD +======= +import { + ThemedMetadataRepresentationListComponent +} from './simple/metadata-representation-list/themed-metadata-representation-list.component'; +>>>>>>> dspace-7.6.1 const ENTRY_COMPONENTS = [ ItemVersionsDeleteModalComponent, @@ -27,6 +33,10 @@ const COMPONENTS = [ MetadataValuesComponent, GenericItemPageFieldComponent, MetadataRepresentationListComponent, +<<<<<<< HEAD +======= + ThemedMetadataRepresentationListComponent, +>>>>>>> dspace-7.6.1 RelatedItemsComponent, ]; 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..a2d9a6fdb02 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,35 @@ +<<<<<<< HEAD
    +======= +
    +>>>>>>> dspace-7.6.1
    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..bc25a28fc18 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,30 @@ -import { Component, Input, OnInit } from '@angular/core'; +import { Component, Input } from '@angular/core'; import { MediaViewerItem } from '../../../core/shared/media-viewer-item.model'; +<<<<<<< HEAD import { languageHelper } from './language-helper'; import { CaptionInfo} from './caption-info'; +======= +import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; +import { languageHelper } from './language-helper'; +import { CaptionInfo } from './caption-info'; +import { Bitstream } from 'src/app/core/shared/bitstream.model'; +>>>>>>> dspace-7.6.1 /** - * 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,8 +32,27 @@ export class MediaViewerVideoComponent implements OnInit { audio: './assets/images/replacement_audio.svg', }; - replacementThumbnail: string; + constructor( + public dsoNameService: DSONameService, + ) { + } + + /** + * This method check if there is caption file for the media + * The caption file name is the media name plus "-" following two letter + * language code and .vtt suffix + * + * html5 video only support WEBVTT format + * + * Two letter language code reference + * https://www.w3schools.com/tags/ref_language_codes.asp + */ + 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()); +<<<<<<< HEAD ngOnInit() { this.isCollapsed = false; this.filteredMedias = this.medias.filter((media) => media.format === 'audio' || media.format === 'video'); @@ -58,12 +85,21 @@ export class MediaViewerVideoComponent implements OnInit { 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], + )); +>>>>>>> dspace-7.6.1 } 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 +107,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..9ee08edd7a3 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,21 @@

    {{ '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 6079287f711..13d358eaad2 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,11 @@ 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'; +<<<<<<< HEAD import { AlertType } from '../../../shared/alert/aletr-type'; +======= +import { AlertType } from '../../../shared/alert/alert-type'; +>>>>>>> dspace-7.6.1 import { Item } from '../../../core/shared/item.model'; import { OrcidAuthService } from '../../../core/orcid/orcid-auth.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..668dad69ee4 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,10 @@ import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +<<<<<<< HEAD import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'; +======= +import { UntypedFormControl, UntypedFormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'; +>>>>>>> dspace-7.6.1 import { RouterTestingModule } from '@angular/router/testing'; import { By } from '@angular/platform-browser'; @@ -26,7 +30,11 @@ describe('OrcidSyncSettingsComponent test suite', () => { let scheduler: TestScheduler; let researcherProfileService: jasmine.SpyObj; let notificationsService; +<<<<<<< HEAD let formGroup: FormGroup; +======= + let formGroup: UntypedFormGroup; +>>>>>>> dspace-7.6.1 const mockResearcherProfile: ResearcherProfile = Object.assign(new ResearcherProfile(), { id: 'test-id', @@ -186,12 +194,21 @@ describe('OrcidSyncSettingsComponent test suite', () => { beforeEach(() => { scheduler = getTestScheduler(); notificationsService = (comp as any).notificationsService; +<<<<<<< HEAD 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), +>>>>>>> dspace-7.6.1 }); 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 494075c0f07..0e0e07a17e7 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,9 @@ import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +<<<<<<< HEAD import { FormGroup } from '@angular/forms'; +======= +import { UntypedFormGroup } from '@angular/forms'; +>>>>>>> dspace-7.6.1 import { TranslateService } from '@ngx-translate/core'; import { Operation } from 'fast-json-patch'; @@ -127,7 +131,11 @@ export class OrcidSyncSettingsComponent implements OnInit { * * @param form The form group */ +<<<<<<< HEAD onSubmit(form: FormGroup): void { +======= + onSubmit(form: UntypedFormGroup): void { +>>>>>>> dspace-7.6.1 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..6b9eef38852 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,28 @@
    +<<<<<<< HEAD {{file?.name}} ({{(file?.sizeBytes) | dsFileSize }}) +======= + {{ dsoNameService.getName(file) }} + ({{(file?.sizeBytes) | dsFileSize }}) +>>>>>>> dspace-7.6.1
    {{'item.page.bitstreams.collapse' | translate}} +======= + +
    +
    + +>>>>>>> dspace-7.6.1
    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..ccd65655b8f 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,10 @@ 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'; +<<<<<<< HEAD +======= +import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; +>>>>>>> dspace-7.6.1 /** * This component renders the file section of the item @@ -42,6 +46,10 @@ export class FileSectionComponent implements OnInit { protected bitstreamDataService: BitstreamDataService, protected notificationsService: NotificationsService, protected translateService: TranslateService, +<<<<<<< HEAD +======= + public dsoNameService: DSONameService, +>>>>>>> dspace-7.6.1 @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..9f92fa27be8 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,10 @@ describe('ItemPageAbstractFieldComponent', () => { })); it('should render a ds-metadata-values', () => { +<<<<<<< HEAD expect(fixture.debugElement.query(By.css('ds-metadata-values'))).toBeDefined(); +======= + expect(fixture.debugElement.query(By.css('ds-metadata-values'))).not.toBeNull(); +>>>>>>> dspace-7.6.1 }); }); 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 15960bdc9d7..17967c526be 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,6 +1,14 @@ +<<<<<<< HEAD

    +======= +

    +>>>>>>> dspace-7.6.1
    {{ type.toLowerCase() + '.page.titleprefix' | translate }}
    {{ dsoNameService.getName(item) }} +<<<<<<< HEAD

    +======= + +>>>>>>> dspace-7.6.1 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/field-components/specific-field/uri/item-page-uri-field.component.spec.ts b/src/app/item-page/simple/field-components/specific-field/uri/item-page-uri-field.component.spec.ts index 4116296b960..aefa1f8a244 100644 --- a/src/app/item-page/simple/field-components/specific-field/uri/item-page-uri-field.component.spec.ts +++ b/src/app/item-page/simple/field-components/specific-field/uri/item-page-uri-field.component.spec.ts @@ -9,9 +9,12 @@ import { environment } from '../../../../../../environments/environment'; import { APP_CONFIG } from '../../../../../../config/app-config.interface'; import { BrowseDefinitionDataService } from '../../../../../core/browse/browse-definition-data.service'; import { BrowseDefinitionDataServiceStub } from '../../../../../shared/testing/browse-definition-data-service.stub'; +<<<<<<< HEAD import { ConfigurationDataService } from '../../../../../core/data/configuration-data.service'; import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils'; import { ConfigurationProperty } from '../../../../../core/shared/configuration-property.model'; +======= +>>>>>>> dspace-7.6.1 let comp: ItemPageUriFieldComponent; let fixture: ComponentFixture; @@ -42,8 +45,12 @@ describe('ItemPageUriFieldComponent', () => { })], providers: [ { provide: APP_CONFIG, useValue: environment }, +<<<<<<< HEAD { provide: BrowseDefinitionDataService, useValue: BrowseDefinitionDataServiceStub }, { provide: ConfigurationDataService, useValue: mockConfigurationDataService } +======= + { provide: BrowseDefinitionDataService, useValue: BrowseDefinitionDataServiceStub } +>>>>>>> dspace-7.6.1 ], declarations: [ItemPageUriFieldComponent, MetadataUriValuesComponent], schemas: [NO_ERRORS_SCHEMA] diff --git a/src/app/item-page/simple/item-page.component.html b/src/app/item-page/simple/item-page.component.html index 362a819e57c..2870e4cbd86 100644 --- a/src/app/item-page/simple/item-page.component.html +++ b/src/app/item-page/simple/item-page.component.html @@ -1,3 +1,4 @@ +<<<<<<< HEAD
    @@ -6,6 +7,12 @@
    +======= +
    +
    +
    + +>>>>>>> dspace-7.6.1 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 049a8b7494b..5fb2dc47c7b 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, TranslateService } 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,7 @@ 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'; +<<<<<<< HEAD import { RegistryService } from 'src/app/core/registry/registry.service'; import { Store } from '@ngrx/store'; import { NotificationsService } from 'src/app/shared/notifications/notifications.service'; @@ -32,6 +33,12 @@ import { getMockTranslateService } from 'src/app/shared/mocks/translate.service. import { ConfigurationProperty } from '../../core/shared/configuration-property.model'; import { HALEndpointService } from '../../core/shared/hal-endpoint.service'; import { MetadataValue } from '../../core/shared/metadata.models'; +======= +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'; +>>>>>>> dspace-7.6.1 const mockItem: Item = Object.assign(new Item(), { bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])), @@ -51,10 +58,28 @@ const mockWithdrawnItem: Item = Object.assign(new Item(), { isWithdrawn: true }); +<<<<<<< HEAD +======= +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]; + +>>>>>>> dspace-7.6.1 describe('ItemPageComponent', () => { let comp: ItemPageComponent; let fixture: ComponentFixture; let authService: AuthService; +<<<<<<< HEAD let translateService: TranslateService; let registryService: RegistryService; let halService: HALEndpointService; @@ -62,6 +87,12 @@ describe('ItemPageComponent', () => { 'isAuthorized', ]); let authorizationDataService: AuthorizationDataService; +======= + let authorizationDataService: AuthorizationDataService; + let serverResponseService: jasmine.SpyObj; + let signpostingDataService: jasmine.SpyObj; + let linkHeadService: jasmine.SpyObj; +>>>>>>> dspace-7.6.1 const mockMetadataService = { /* eslint-disable no-empty,@typescript-eslint/no-empty-function */ @@ -82,6 +113,21 @@ describe('ItemPageComponent', () => { isAuthenticated: observableOf(true), setRedirectUrl: {} }); + 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'), + }); translateService = getMockTranslateService(); authorizationDataService = jasmine.createSpyObj('authorizationDataService', { @@ -115,6 +161,7 @@ describe('ItemPageComponent', () => { { provide: MetadataService, useValue: mockMetadataService }, { provide: Router, useValue: {} }, { provide: AuthService, useValue: authService }, +<<<<<<< HEAD { provide: AuthorizationDataService, useValue: authorizationService }, { provide: Store, useValue: {} }, { provide: NotificationsService, useValue: {} }, @@ -124,6 +171,13 @@ describe('ItemPageComponent', () => { RegistryService, { provide: AuthorizationDataService, useValue: authorizationDataService }, { provide: HALEndpointService, useValue: halService } +======= + { provide: AuthorizationDataService, useValue: authorizationDataService }, + { provide: ServerResponseService, useValue: serverResponseService }, + { provide: SignpostingDataService, useValue: signpostingDataService }, + { provide: LinkHeadService, useValue: linkHeadService }, + { provide: PLATFORM_ID, useValue: 'server' }, +>>>>>>> dspace-7.6.1 ], schemas: [NO_ERRORS_SCHEMA] @@ -175,6 +229,36 @@ describe('ItemPageComponent', () => { const objectLoader = fixture.debugElement.query(By.css('ds-listable-object-component-loader')); expect(objectLoader.nativeElement).toBeDefined(); }); +<<<<<<< HEAD +======= + + 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" '); + }); + +>>>>>>> dspace-7.6.1 }); describe('when the item is withdrawn and the user is not an admin', () => { beforeEach(() => { @@ -199,6 +283,14 @@ describe('ItemPageComponent', () => { const objectLoader = fixture.debugElement.query(By.css('ds-listable-object-component-loader')); expect(objectLoader.nativeElement).toBeDefined(); }); +<<<<<<< HEAD +======= + + it('should add the signposting links', () => { + expect(serverResponseService.setHeader).toHaveBeenCalled(); + expect(linkHeadService.addTag).toHaveBeenCalledTimes(2); + }); +>>>>>>> dspace-7.6.1 }); describe('when the item is not withdrawn and the user is not an admin', () => { @@ -211,6 +303,7 @@ describe('ItemPageComponent', () => { const objectLoader = fixture.debugElement.query(By.css('ds-listable-object-component-loader')); expect(objectLoader.nativeElement).toBeDefined(); }); +<<<<<<< HEAD }); describe('when the item has the file', () => { @@ -223,6 +316,13 @@ describe('ItemPageComponent', () => { expect(objectLoader.nativeElement).toBeDefined(); }); })); +======= + + it('should add the signposting links', () => { + expect(serverResponseService.setHeader).toHaveBeenCalled(); + expect(linkHeadService.addTag).toHaveBeenCalledTimes(2); + }); +>>>>>>> dspace-7.6.1 }); }); diff --git a/src/app/item-page/simple/item-page.component.ts b/src/app/item-page/simple/item-page.component.ts index 99abb4b0493..eeda6ee0e47 100644 --- a/src/app/item-page/simple/item-page.component.ts +++ b/src/app/item-page/simple/item-page.component.ts @@ -1,10 +1,21 @@ +<<<<<<< HEAD import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { map, take } from 'rxjs/operators'; +======= +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, take } from 'rxjs/operators'; + +>>>>>>> dspace-7.6.1 import { ItemDataService } from '../../core/data/item-data.service'; import { RemoteData } from '../../core/data/remote-data'; import { Item } from '../../core/shared/item.model'; import { fadeInOut } from '../../shared/animations/fade'; +<<<<<<< HEAD import { getAllSucceededRemoteDataPayload, getAllSucceededRemoteListPayload, @@ -20,6 +31,20 @@ import { RegistryService } from 'src/app/core/registry/registry.service'; import { MetadataBitstream } from 'src/app/core/metadata/metadata-bitstream.model'; import { BehaviorSubject, Observable } from 'rxjs'; import { HALEndpointService } from '../../core/shared/hal-endpoint.service'; +======= +import { getAllSucceededRemoteDataPayload } from '../../core/shared/operators'; +import { ViewMode } from '../../core/shared/view-mode.model'; +import { AuthService } from '../../core/auth/auth.service'; +import { getItemPageRoute } from '../item-page-routing-paths'; +import { 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'; +>>>>>>> dspace-7.6.1 /** * This component renders a simple item page. @@ -33,7 +58,7 @@ import { HALEndpointService } from '../../core/shared/hal-endpoint.service'; changeDetection: ChangeDetectionStrategy.OnPush, animations: [fadeInOut] }) -export class ItemPageComponent implements OnInit { +export class ItemPageComponent implements OnInit, OnDestroy { /** * The item's id @@ -108,8 +133,21 @@ export class ItemPageComponent implements OnInit { */ hasFiles: BehaviorSubject = new BehaviorSubject(false); + /** + * Whether the current user is an admin or not + */ + isAdmin$: Observable; + + itemUrl: string; + + /** + * Contains a list of SignpostingLink related to the item + */ + signpostingLinks: SignpostingLink[] = []; + constructor( protected route: ActivatedRoute, +<<<<<<< HEAD private router: Router, private items: ItemDataService, private authService: AuthService, @@ -117,6 +155,18 @@ export class ItemPageComponent implements OnInit { protected registryService: RegistryService, protected halService: HALEndpointService, ) { +======= + 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(); +>>>>>>> dspace-7.6.1 } /** @@ -132,6 +182,7 @@ export class ItemPageComponent implements OnInit { map((item) => getItemPageRoute(item)) ); +<<<<<<< HEAD this.processItem(); this.registryService @@ -250,5 +301,47 @@ export class ItemPageComponent implements OnInit { take(1), getAllSucceededRemoteDataPayload()) .subscribe((item: Item) => void this.router.navigate([getItemPageRoute(item), 'download'])); +======= + 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}'`); + }); +>>>>>>> dspace-7.6.1 } } 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 c6f00dafe22..7d2771f9b40 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 @@ -1,5 +1,9 @@ +<<<<<<< HEAD
    +======= +
    +>>>>>>> dspace-7.6.1
    +<<<<<<< HEAD @@ -93,6 +98,86 @@ +
    +
    + + + + + +
    + +
    + + + + + + + + + + + + +
    +
    + + + + + + + + + + + + + + + + + + diff --git a/src/app/item-page/simple/item-types/publication/publication.component.scss b/src/app/item-page/simple/item-types/publication/publication.component.scss index d21979ab1bb..06657be514c 100644 --- a/src/app/item-page/simple/item-types/publication/publication.component.scss +++ b/src/app/item-page/simple/item-types/publication/publication.component.scss @@ -1,2 +1,6 @@ @import '../../../../../styles/variables.scss'; +<<<<<<< HEAD @import "../../item-page.component"; +======= + +>>>>>>> dspace-7.6.1 diff --git a/src/app/item-page/simple/item-types/publication/publication.component.spec.ts b/src/app/item-page/simple/item-types/publication/publication.component.spec.ts index f590c4fbd93..fbbd7b44a76 100644 --- a/src/app/item-page/simple/item-types/publication/publication.component.spec.ts +++ b/src/app/item-page/simple/item-types/publication/publication.component.spec.ts @@ -151,9 +151,14 @@ describe('PublicationComponent', () => { beforeEach(waitForAsync(() => { const iiifEnabledMap: MetadataMap = { 'dspace.iiif.enabled': [getIIIFEnabled(true)], +<<<<<<< HEAD 'iiif.search.enabled': [getIIIFSearchEnabled(true)], }; TestBed.overrideProvider(RouteService, {useValue: localMockRouteService}); +======= + 'iiif.search.enabled': [getIIIFSearchEnabled(false)], + }; +>>>>>>> dspace-7.6.1 TestBed.compileComponents(); fixture = TestBed.createComponent(PublicationComponent); comp = fixture.componentInstance; @@ -165,7 +170,57 @@ describe('PublicationComponent', () => { const fields = fixture.debugElement.queryAll(By.css('ds-mirador-viewer')); expect(fields.length).toBeGreaterThanOrEqual(1); }); + it('should not retrieve the query term for previous route', fakeAsync((): void => { + //tick(10) + expect(comp.iiifQuery$).toBeFalsy(); + })); + + it('should retrieve the query term for previous route', fakeAsync((): void => { + expect(comp.iiifQuery$.subscribe(result => expect(result).toEqual('test query'))); + })); + + }); +<<<<<<< HEAD + describe('with IIIF viewer and search but no previous search query', () => { + const localMockRouteService = { + getPreviousUrl(): Observable { + return of('/item'); +======= + const localMockRouteService = { + getPreviousUrl(): Observable { + return of('/search?query=test%20query&fakeParam=true'); +>>>>>>> dspace-7.6.1 + } + }; + beforeEach(waitForAsync(() => { + const iiifEnabledMap: MetadataMap = { + 'dspace.iiif.enabled': [getIIIFEnabled(true)], + 'iiif.search.enabled': [getIIIFSearchEnabled(true)], + }; + TestBed.overrideProvider(RouteService, {useValue: localMockRouteService}); + TestBed.compileComponents(); + fixture = TestBed.createComponent(PublicationComponent); + comp = fixture.componentInstance; + comp.object = getItem(iiifEnabledMap); + fixture.detectChanges(); + })); + + it('should contain an iiif viewer component', () => { + const fields = fixture.debugElement.queryAll(By.css('ds-mirador-viewer')); + expect(fields.length).toBeGreaterThanOrEqual(1); + }); + +<<<<<<< HEAD + it('should not retrieve the query term for previous route', fakeAsync( () => { + let emitted; + comp.iiifQuery$.subscribe(result => emitted = result); + tick(10); + expect(emitted).toBeUndefined(); + })); + + }); +======= it('should retrieve the query term for previous route', fakeAsync((): void => { expect(comp.iiifQuery$.subscribe(result => expect(result).toEqual('test query'))); })); @@ -204,4 +259,5 @@ describe('PublicationComponent', () => { })); }); +>>>>>>> dspace-7.6.1 }); 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 b4c3da2cdc3..0c4e82178f5 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 @@ -5,8 +5,7 @@ import { RemoteData } from '../../../../core/data/remote-data'; import { Relationship } from '../../../../core/shared/item-relationships/relationship.model'; import { Item } from '../../../../core/shared/item.model'; import { - getFirstSucceededRemoteDataPayload, - getFirstSucceededRemoteData + getFirstCompletedRemoteData } from '../../../../core/shared/operators'; import { hasValue } from '../../../../shared/empty.util'; import { InjectionToken } from '@angular/core'; @@ -77,24 +76,42 @@ export const relationsToItems = (thisId: string) => * @param {string} thisId The item's id of which the relations belong to * @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 17f3675f944..251c0f13487 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 @@ -1,5 +1,9 @@ +<<<<<<< HEAD
    +======= +
    +>>>>>>> dspace-7.6.1
    +<<<<<<< HEAD @@ -99,6 +104,71 @@ +
    +
    + + + + + +
    + +
    + + + + + + + + + + + + +
    +
    + + + + + + + + + + + + diff --git a/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.scss b/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.scss index ae2660b26c8..635fe1e75a7 100644 --- a/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.scss +++ b/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.scss @@ -1,3 +1,6 @@ @import '../../../../../styles/variables.scss'; +<<<<<<< HEAD @import "../../item-page.component"; +======= +>>>>>>> dspace-7.6.1 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..a3675932eda 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,21 @@
    {{'item.page.related-items.view-less' | translate:{ amount: representations?.length } }} +======= + +
    +
    + +>>>>>>> dspace-7.6.1
    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..2cfafe57ad4 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,15 @@ export class MetadataRepresentationListComponent extends AbstractIncrementalList */ total: number; +<<<<<<< HEAD constructor(public relationshipService: RelationshipDataService, private browseDefinitionDataService: BrowseDefinitionDataService) { +======= + constructor( + public relationshipService: RelationshipDataService, + protected browseDefinitionDataService: BrowseDefinitionDataService, + ) { +>>>>>>> dspace-7.6.1 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..3b2825096d9 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,21 @@
    {{'item.page.related-items.view-less' | translate:{ amount: itemsRD?.payload?.page?.length } }} +======= + +
    +
    + +>>>>>>> dspace-7.6.1
    diff --git a/src/app/item-page/versions/item-versions.component.html b/src/app/item-page/versions/item-versions.component.html index b03900a694a..176c1f0cde6 100644 --- a/src/app/item-page/versions/item-versions.component.html +++ b/src/app/item-page/versions/item-versions.component.html @@ -1,149 +1,145 @@ -
    - -
    - -
    -
    +
    +
    +

    {{"item.version.history.head" | translate}}

    - - - - - - - - - - - - - -
    {{'item.version.history.table.name' | translate}}{{'item.version.history.table.handle' | translate}}
    {{versionFromMetadata.name}}{{versionFromMetadata.handle}}
    -
    -
    - - -
    -
    -
    -

    {{"item.version.history.head" | translate}}

    - - {{ "item.version.history.selected.alert" | translate : {version: itemVersion.version} }} - - - - - - - - - - - - - - - - - - - - + + + + + +
    {{"item.version.history.table.version" | translate}}{{'item.version.history.table.name' | translate}}{{'item.version.history.table.handle' | translate}}
    {{getNameFromHandle(relationNameHandle.handle)}}{{relationNameHandle.handle}}
    - - - + + {{ "item.version.history.selected.alert" | translate : {version: itemVersion.version} }} + + + + + + + + + + + + + + - - - - - - - - - - -
    {{"item.version.history.table.version" | translate}}{{"item.version.history.table.editor" | translate}}{{"item.version.history.table.date" | translate}}{{"item.version.history.table.summary" | translate}}
    + + + -
    +
    - - - {{versionWithRelations?.version.version}} - - - {{versionWithRelations?.version.version}} - - * + + + {{version.version}} + + + {{version.version}} + + * - + {{ "item.version.history.table.workspaceItem" | translate }} - + {{ "item.version.history.table.workflowItem" | translate }} -
    +
    -
    +
    -
    - - + + + - - - - - - - - -
    - + + + + +
    - +
    +
    -
    - - - {{getItemNameFromVersion(versionWithRelations?.version) | async }} - - - - {{itemHandle}} -
    {{getNameFromHandle(relationNameHandle.handle)}}{{relationNameHandle.handle}}
    -
    * {{"item.version.history.selected" | translate}}
    -
    - - +
    + {{version?.submitterName}} + + {{version?.created | date : 'yyyy-MM-dd HH:mm:ss'}} + +
    + + {{version?.summary}} + + + +
    + +
    + + + + + + + + + +
    + + +
    +
    * {{"item.version.history.selected" | translate}}
    +
    +
    - - +
    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 a23c43dea59..92e7990c67b 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,11 @@ 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'; +<<<<<<< HEAD:src/app/item-page/versions/item-versions.component.spec.ts import { FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms'; +======= +import { UntypedFormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms'; +>>>>>>> dspace-7.6.1:src/app/shared/item/item-versions/item-versions.component.spec.ts 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'; @@ -140,7 +144,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 fbba2ca541f..43d8172f498 100644 --- a/src/app/item-page/versions/item-versions.component.ts +++ b/src/app/item-page/versions/item-versions.component.ts @@ -1,5 +1,20 @@ +<<<<<<< HEAD:src/app/item-page/versions/item-versions.component.ts import { Component, Input, OnInit } from '@angular/core'; import { BehaviorSubject, combineLatest, Observable, of, Subscription } from 'rxjs'; +======= +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'; +import { + BehaviorSubject, + combineLatest, + Observable, + of, + Subscription, +} from 'rxjs'; +import { VersionHistory } from '../../core/shared/version-history.model'; +>>>>>>> dspace-7.6.1:src/app/shared/item/item-versions/item-versions.component.ts import { getAllSucceededRemoteData, getAllSucceededRemoteDataPayload, @@ -8,13 +23,29 @@ import { getFirstSucceededRemoteDataPayload, getFirstSucceededRemoteListPayload, getRemoteDataPayload } from '../../core/shared/operators'; +<<<<<<< HEAD:src/app/item-page/versions/item-versions.component.ts import { map, mergeMap, startWith, switchMap, take, tap} from 'rxjs/operators'; +======= +import { map, mergeMap, startWith, switchMap, take, tap } from 'rxjs/operators'; +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/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'; +>>>>>>> dspace-7.6.1:src/app/shared/item/item-versions/item-versions.component.ts import { getItemEditVersionhistoryRoute, getItemPageRoute, getItemVersionRoute } from '../item-page-routing-paths'; +<<<<<<< HEAD:src/app/item-page/versions/item-versions.component.ts import { FormBuilder } from '@angular/forms'; +======= +import { UntypedFormBuilder } from '@angular/forms'; +>>>>>>> dspace-7.6.1:src/app/shared/item/item-versions/item-versions.component.ts 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'; @@ -30,6 +61,7 @@ import { WorkspaceItem } from '../../core/submission/models/workspaceitem.model' import { WorkspaceitemDataService } from '../../core/submission/workspaceitem-data.service'; import { WorkflowItemDataService } from '../../core/submission/workflowitem-data.service'; import { ConfigurationDataService } from '../../core/data/configuration-data.service'; +<<<<<<< HEAD:src/app/item-page/versions/item-versions.component.ts import { Item } from 'src/app/core/shared/item.model'; import { AlertType } from 'src/app/shared/alert/aletr-type'; import { RemoteData } from 'src/app/core/data/remote-data'; @@ -46,6 +78,8 @@ import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import isEqual from 'lodash/isEqual'; import { RequestParam } from '../../core/cache/models/request-param.model'; import { FindListOptions } from '../../core/data/find-list-options.model'; +======= +>>>>>>> dspace-7.6.1:src/app/shared/item/item-versions/item-versions.component.ts @Component({ selector: 'ds-item-versions', @@ -56,7 +90,7 @@ import { FindListOptions } from '../../core/data/find-list-options.model'; /** * 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 @@ -188,7 +222,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..a759318fb10 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,11 @@ import { } from '../../../core/shared/operators'; import { map, startWith, switchMap } from 'rxjs/operators'; import { VersionHistoryDataService } from '../../../core/data/version-history-data.service'; +<<<<<<< HEAD:src/app/item-page/versions/notice/item-versions-notice.component.ts import { AlertType } from '../../../shared/alert/aletr-type'; +======= +import { AlertType } from '../../../shared/alert/alert-type'; +>>>>>>> dspace-7.6.1:src/app/shared/item/item-versions/notice/item-versions-notice.component.ts 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 ea6dba6a316..3b9cd4484eb 100644 --- a/src/app/login-page/login-page.component.html +++ b/src/app/login-page/login-page.component.html @@ -6,8 +6,8 @@

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

    - +
    diff --git a/src/app/menu.resolver.spec.ts b/src/app/menu.resolver.spec.ts index eef5c2d5af4..0b5b3cc6213 100644 --- a/src/app/menu.resolver.spec.ts +++ b/src/app/menu.resolver.spec.ts @@ -41,6 +41,10 @@ describe('MenuResolver', () => { beforeEach(waitForAsync(() => { menuService = new MenuServiceStub(); spyOn(menuService, 'getMenu').and.returnValue(observableOf(MENU_STATE)); +<<<<<<< HEAD +======= + spyOn(menuService, 'addSection'); +>>>>>>> dspace-7.6.1 browseService = jasmine.createSpyObj('browseService', { getBrowseDefinitions: createSuccessfulRemoteDataObject$(createPaginatedList(BROWSE_DEFINITIONS)) @@ -70,8 +74,11 @@ describe('MenuResolver', () => { schemas: [NO_ERRORS_SCHEMA] }); resolver = TestBed.inject(MenuResolver); +<<<<<<< HEAD spyOn(menuService, 'addSection'); +======= +>>>>>>> dspace-7.6.1 })); it('should be created', () => { diff --git a/src/app/menu.resolver.ts b/src/app/menu.resolver.ts index 14714f3c561..6214ee8c304 100644 --- a/src/app/menu.resolver.ts +++ b/src/app/menu.resolver.ts @@ -47,7 +47,10 @@ import { import { ExportBatchSelectorComponent } from './shared/dso-selector/modal-wrappers/export-batch-selector/export-batch-selector.component'; +<<<<<<< HEAD import { getLicensesManageTablePath, getLicensesModulePath } from './app-routing-paths'; +======= +>>>>>>> dspace-7.6.1 /** * Creates all of the app's menus @@ -360,6 +363,7 @@ export class MenuResolver implements Resolve { link: '/health' } as LinkMenuItemModel, icon: 'heartbeat', +<<<<<<< HEAD index: 13 }, /* Workflow tasks */ { @@ -400,6 +404,10 @@ export class MenuResolver implements Resolve { icon: 'scroll', index: 12 }, +======= + index: 11 + }, +>>>>>>> dspace-7.6.1 ]; menuList.forEach((menuSection) => this.menuService.addSection(MenuID.ADMIN, Object.assign(menuSection, { shouldPersistOnRouteChange: true @@ -656,7 +664,11 @@ export class MenuResolver implements Resolve { link: '/admin/system-wide-alert' } as LinkMenuItemModel, icon: 'exclamation-circle', +<<<<<<< HEAD index: 14 +======= + index: 12 +>>>>>>> dspace-7.6.1 }, ]; @@ -698,6 +710,20 @@ export class MenuResolver implements Resolve { link: '/access-control/groups' } as LinkMenuItemModel, }, +<<<<<<< HEAD +======= + { + 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, + }, +>>>>>>> dspace-7.6.1 // 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-new-submission/my-dspace-new-submission.component.spec.ts b/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.spec.ts index c85b5166c37..6fecf32b0e1 100644 --- a/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.spec.ts +++ b/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.spec.ts @@ -26,8 +26,11 @@ import { CookieServiceMock } from '../../shared/mocks/cookie.service.mock'; import { HttpXsrfTokenExtractorMock } from '../../shared/mocks/http-xsrf-token-extractor.mock'; import { getMockEntityTypeService } from './my-dspace-new-submission-dropdown/my-dspace-new-submission-dropdown.component.spec'; import { EntityTypeDataService } from '../../core/data/entity-type-data.service'; +<<<<<<< HEAD import { of } from 'rxjs'; import { ConfigurationDataService } from '../../core/data/configuration-data.service'; +======= +>>>>>>> dspace-7.6.1 describe('MyDSpaceNewSubmissionComponent test', () => { @@ -70,7 +73,10 @@ describe('MyDSpaceNewSubmissionComponent test', () => { { provide: CookieService, useValue: new CookieServiceMock() }, { provide: HostWindowService, useValue: new HostWindowServiceStub(800) }, { provide: EntityTypeDataService, useValue: getMockEntityTypeService() }, +<<<<<<< HEAD { provide: ConfigurationDataService, useValue: configurationServiceSpy }, +======= +>>>>>>> dspace-7.6.1 ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); 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 f3775214d5e..26da829adf4 100644 --- a/src/app/my-dspace-page/my-dspace-search.module.ts +++ b/src/app/my-dspace-page/my-dspace-search.module.ts @@ -23,7 +23,10 @@ import { ItemDetailPreviewComponent } from '../shared/object-detail/my-dspace-re 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'; +<<<<<<< HEAD import { MyDSpaceItemStatusComponent } from '../shared/object-collection/shared/mydspace-item-status/my-dspace-item-status.component'; +======= +>>>>>>> dspace-7.6.1 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'; @@ -52,7 +55,10 @@ const DECLARATIONS = [ ItemDetailPreviewFieldComponent, ItemListPreviewComponent, ThemedItemListPreviewComponent, +<<<<<<< HEAD MyDSpaceItemStatusComponent, +======= +>>>>>>> dspace-7.6.1 ]; @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..904c5097bd5 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,10 @@ 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'; +<<<<<<< HEAD import { rendersSectionForMenu } from '../../shared/menu/menu-section.decorator'; +======= +>>>>>>> dspace-7.6.1 import { MenuID } from '../../shared/menu/menu-id.model'; /** @@ -16,7 +19,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..4a054c73a86 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,12 @@ import { MenuID } from '../../shared/menu/menu-id.model'; * Themed wrapper for ExpandableNavbarSectionComponent */ @Component({ +<<<<<<< HEAD /* eslint-disable @angular-eslint/component-selector */ selector: 'li[ds-themed-expandable-navbar-section]', +======= + selector: 'ds-themed-expandable-navbar-section', +>>>>>>> dspace-7.6.1 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..5d5eb2a941d 100644 --- a/src/app/navbar/navbar-section/navbar-section.component.ts +++ b/src/app/navbar/navbar-section/navbar-section.component.ts @@ -8,8 +8,12 @@ import { MenuID } from '../../shared/menu/menu-id.model'; * Represents a non-expandable section in the navbar */ @Component({ +<<<<<<< HEAD /* eslint-disable @angular-eslint/component-selector */ selector: 'li[ds-navbar-section]', +======= + selector: 'ds-navbar-section', +>>>>>>> dspace-7.6.1 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..16a91ffa304 100644 --- a/src/app/navbar/navbar.component.html +++ b/src/app/navbar/navbar.component.html @@ -6,11 +6,19 @@
    diff --git a/src/app/navbar/navbar.component.scss b/src/app/navbar/navbar.component.scss index 441ee82c968..88e83d40c9d 100644 --- a/src/app/navbar/navbar.component.scss +++ b/src/app/navbar/navbar.component.scss @@ -1,5 +1,9 @@ nav.navbar { +<<<<<<< HEAD border-bottom: 1px var(--ds-header-navbar-border-bottom-color) solid; +======= + background-color: var(--ds-navbar-bg); +>>>>>>> dspace-7.6.1 align-items: baseline; } @@ -11,9 +15,14 @@ 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 +<<<<<<< HEAD +======= + border-bottom: 1px var(--ds-header-navbar-border-bottom-color) solid; // open navbar covers header-navbar-wrapper border +>>>>>>> dspace-7.6.1 } } } @@ -38,8 +47,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 ada9be9d0bc..1ee3a9da5ab 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'; @@ -28,6 +27,12 @@ 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'; +<<<<<<< HEAD +======= +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'; +>>>>>>> dspace-7.6.1 let comp: NavbarComponent; let fixture: ComponentFixture; @@ -66,30 +71,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/navbar/navbar.module.ts b/src/app/navbar/navbar.module.ts index 9f2c2fff5d1..d68b6666f69 100644 --- a/src/app/navbar/navbar.module.ts +++ b/src/app/navbar/navbar.module.ts @@ -24,7 +24,10 @@ const ENTRY_COMPONENTS = [ NavbarSectionComponent, ExpandableNavbarSectionComponent, ThemedExpandableNavbarSectionComponent, +<<<<<<< HEAD ClarinNavbarTopComponent +======= +>>>>>>> dspace-7.6.1 ]; @NgModule({ @@ -45,8 +48,12 @@ const ENTRY_COMPONENTS = [ exports: [ ThemedNavbarComponent, NavbarSectionComponent, +<<<<<<< HEAD ThemedExpandableNavbarSectionComponent, ClarinNavbarTopComponent +======= + ThemedExpandableNavbarSectionComponent +>>>>>>> dspace-7.6.1 ] }) diff --git a/src/app/page-error/page-error.component.html b/src/app/page-error/page-error.component.html index d1a46602d50..33833a183e0 100644 --- a/src/app/page-error/page-error.component.html +++ b/src/app/page-error/page-error.component.html @@ -5,6 +5,10 @@

    {{"error-page.description." + status | translate}}

    {{"error-page." + code | translate}}


    +<<<<<<< HEAD {{ status + ".link.home-page" | translate}} +======= + {{ status + ".link.home-page" | translate}} +>>>>>>> dspace-7.6.1

    diff --git a/src/app/page-internal-server-error/page-internal-server-error.component.html b/src/app/page-internal-server-error/page-internal-server-error.component.html index 3b4fa351d65..d2f0258d5b5 100644 --- a/src/app/page-internal-server-error/page-internal-server-error.component.html +++ b/src/app/page-internal-server-error/page-internal-server-error.component.html @@ -7,6 +7,10 @@

    {{"500.help" | translate}}


    +<<<<<<< HEAD {{"500.link.home-page" | translate}} +======= + {{"500.link.home-page" | translate}} +>>>>>>> dspace-7.6.1

    diff --git a/src/app/process-page/detail/process-detail.component.html b/src/app/process-page/detail/process-detail.component.html index 29cbfc113ff..7ab81790858 100644 --- a/src/app/process-page/detail/process-detail.component.html +++ b/src/app/process-page/detail/process-detail.component.html @@ -1,10 +1,24 @@ +<<<<<<< HEAD

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

    +======= +
    +
    +
    +

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

    +
    +
    + Refreshing in {{ seconds }}s +
    +>>>>>>> dspace-7.6.1
    +
    {{ process?.scriptName }}
    @@ -17,10 +31,19 @@

    {{'process.detail.title' | translate:{
    +<<<<<<< HEAD {{getFileName(file)}} ({{(file?.sizeBytes) | dsFileSize }}) +======= +
    + + {{getFileName(file)}} + ({{(file?.sizeBytes) | dsFileSize }}) + +
    +>>>>>>> dspace-7.6.1
    @@ -70,7 +93,11 @@

    {{'process.detail.title' | translate:{ +<<<<<<< HEAD
    +======= +
    +>>>>>>> dspace-7.6.1 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..fc8b147da73 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,11 @@ 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'; +<<<<<<< HEAD +======= +import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service'; +import { DSONameServiceMock } from '../../../../mocks/dso-name.service.mock'; +>>>>>>> dspace-7.6.1 describe('ComcolRoleComponent', () => { @@ -41,6 +46,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..e9b7902ab7c 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,10 @@ 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'; +<<<<<<< HEAD +======= +import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service'; +>>>>>>> dspace-7.6.1 /** * Component for managing a community or collection role. @@ -76,6 +80,10 @@ export class ComcolRoleComponent implements OnInit { protected groupService: GroupDataService, protected notificationsService: NotificationsService, protected translateService: TranslateService, +<<<<<<< HEAD +======= + public dsoNameService: DSONameService, +>>>>>>> dspace-7.6.1 ) { } 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/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..bca21d3ebaf 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,11 @@
    +<<<<<<< HEAD {{elem.text}} +======= + {{elem.text}} +>>>>>>> dspace-7.6.1 {{ elem }} diff --git a/src/app/shared/cookies/klaro-configuration.ts b/src/app/shared/cookies/klaro-configuration.ts index fb72b174c2a..c6138586dab 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/dso-page/dso-edit-menu.resolver.spec.ts b/src/app/shared/dso-page/dso-edit-menu.resolver.spec.ts index abfe618174b..405ba91420f 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,10 @@ import { TestBed, waitForAsync } from '@angular/core/testing'; import { MenuServiceStub } from '../testing/menu-service.stub'; +<<<<<<< HEAD import { of as observableOf } from 'rxjs'; +======= +import { combineLatest, map, of as observableOf } from 'rxjs'; +>>>>>>> dspace-7.6.1 import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { RouterTestingModule } from '@angular/router/testing'; @@ -16,10 +20,20 @@ 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'; +<<<<<<< HEAD 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 { 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'; +>>>>>>> dspace-7.6.1 describe('DSOEditMenuResolver', () => { @@ -37,6 +51,7 @@ describe('DSOEditMenuResolver', () => { let notificationsService; let translate; +<<<<<<< HEAD const route = { data: { menu: { @@ -49,13 +64,50 @@ describe('DSOEditMenuResolver', () => { } }, 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}, + }; +>>>>>>> dspace-7.6.1 }; const state = { url: 'test-url' }; +<<<<<<< HEAD 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; +>>>>>>> dspace-7.6.1 const dummySections1 = [{ id: 'dummy-1', @@ -90,6 +142,13 @@ describe('DSOEditMenuResolver', () => { }]; beforeEach(waitForAsync(() => { +<<<<<<< HEAD +======= + // test with Items unless specified otherwise + testObject = testItem; + route = dsoRoute(testItem); + +>>>>>>> dspace-7.6.1 menuService = new MenuServiceStub(); spyOn(menuService, 'getMenu').and.returnValue(observableOf(MENU_STATE)); @@ -154,16 +213,29 @@ describe('DSOEditMenuResolver', () => { { ...route.data.menu, [MenuID.DSO_EDIT]: [ +<<<<<<< HEAD ...dummySections1.map((menu) => Object.assign(menu, {id: menu.id + '-test-uuid'})), ...dummySections2.map((menu) => Object.assign(menu, {id: menu.id + '-test-uuid'})) ] } ); expect(dSpaceObjectDataService.findById).toHaveBeenCalledWith('test-uuid', true, false); +======= + ...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-item-uuid', true, false); +>>>>>>> dspace-7.6.1 expect(resolver.getDsoMenus).toHaveBeenCalled(); done(); }); }); +<<<<<<< HEAD +======= + +>>>>>>> dspace-7.6.1 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 +270,10 @@ describe('DSOEditMenuResolver', () => { done(); }); }); +<<<<<<< HEAD +======= + +>>>>>>> dspace-7.6.1 it('should return the statistics menu when no dso is found', (done) => { (dSpaceObjectDataService.findById as jasmine.Spy).and.returnValue(createFailedRemoteDataObject$()); @@ -211,6 +287,7 @@ describe('DSOEditMenuResolver', () => { }); }); }); +<<<<<<< HEAD describe('getDsoMenus', () => { it('should return as first part the item version, orcid and claim list ', (done) => { const result = resolver.getDsoMenus(testObject, route, state); @@ -254,6 +331,167 @@ describe('DSOEditMenuResolver', () => { done(); }); +======= + + describe('getDsoMenus', () => { + 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(); + }); + }); + }); + + 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(); + }); + }); +>>>>>>> dspace-7.6.1 }); }); }); 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 749d5580a43..149c8a35344 100644 --- a/src/app/shared/dso-page/dso-edit-menu.resolver.ts +++ b/src/app/shared/dso-page/dso-edit-menu.resolver.ts @@ -21,6 +21,12 @@ 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'; +<<<<<<< HEAD +======= +import { SubscriptionModalComponent } from '../subscriptions/subscription-modal/subscription-modal.component'; +import { Community } from '../../core/shared/community.model'; +import { Collection } from '../../core/shared/collection.model'; +>>>>>>> dspace-7.6.1 /** * Creates the menus for the dspace object pages @@ -50,6 +56,7 @@ export class DSOEditMenuResolver implements Resolve<{ [key: string]: MenuSection if (hasNoValue(id) && hasValue(route.queryParams.scope)) { id = route.queryParams.scope; } +<<<<<<< HEAD return this.dSpaceObjectDataService.findById(id, true, false).pipe( getFirstCompletedRemoteData(), switchMap((dsoRD) => { @@ -71,6 +78,34 @@ export class DSOEditMenuResolver implements Resolve<{ [key: string]: MenuSection } }) ); +======= + 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) { + 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}); + } + }) + ); + } +>>>>>>> dspace-7.6.1 } /** @@ -79,6 +114,10 @@ export class DSOEditMenuResolver implements Resolve<{ [key: string]: MenuSection getDsoMenus(dso, route, state): Observable[] { return [ this.getItemMenu(dso), +<<<<<<< HEAD +======= + this.getComColMenu(dso), +>>>>>>> dspace-7.6.1 this.getCommonMenu(dso, state) ]; } @@ -174,6 +213,42 @@ export class DSOEditMenuResolver implements Resolve<{ [key: string]: MenuSection } /** +<<<<<<< HEAD +======= + * 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([]); + } + } + + /** +>>>>>>> dspace-7.6.1 * 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..eda37da1c37 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,10 @@ import { hasValue } from '../../../empty.util'; * Represents an expandable section in the dso edit menus */ @Component({ +<<<<<<< HEAD /* tslint:disable:component-selector */ +======= +>>>>>>> dspace-7.6.1 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..6f0681ab34a 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,10 @@ import { MenuSection } from '../../../menu/menu-section.model'; * Represents a non-expandable section in the dso edit menus */ @Component({ +<<<<<<< HEAD /* tslint:disable:component-selector */ +======= +>>>>>>> dspace-7.6.1 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.module.ts b/src/app/shared/dso-page/dso-page.module.ts index 6820e8eb53c..42b29135e35 100644 --- a/src/app/shared/dso-page/dso-page.module.ts +++ b/src/app/shared/dso-page/dso-page.module.ts @@ -9,7 +9,11 @@ import { import { DsoEditMenuExpandableSectionComponent } from '../dso-page/dso-edit-menu/dso-edit-expandable-menu-section/dso-edit-menu-expandable-section.component'; +<<<<<<< HEAD import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; +======= +import { NgbDropdownModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; +>>>>>>> dspace-7.6.1 const COMPONENTS = [ DsoEditMenuComponent, @@ -25,6 +29,10 @@ const MODULES = [ RouterModule, CommonModule, NgbTooltipModule, +<<<<<<< HEAD +======= + NgbDropdownModule, +>>>>>>> dspace-7.6.1 ]; const PROVIDERS = [ 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 c8d11891baa..474392a806b 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'; @@ -69,6 +70,11 @@ export class DSOSelectorComponent implements OnInit, OnDestroy { */ @Input() types: DSpaceObjectType[]; + /** + * The sorting options + */ + @Input() sort: SortOptions; + // list of allowed selectable dsoTypes typesString: string; @@ -80,7 +86,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 @@ -221,13 +227,23 @@ export class DSOSelectorComponent implements OnInit, OnDestroy { * @param useCache Whether or not to use the cache */ search(query: string, page: number, useCache: boolean = true): Observable>>> { +<<<<<<< HEAD +======= + // default sort is only used when there is not query + let efectiveSort = query ? null : this.sort; +>>>>>>> dspace-7.6.1 return this.searchService.search( new PaginatedSearchOptions({ query: query, dsoTypes: this.types, pagination: Object.assign({}, this.defaultPagination, { currentPage: page +<<<<<<< HEAD }) +======= + }), + sort: efectiveSort +>>>>>>> dspace-7.6.1 }), 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 8b38b623786..e0b7c1675b8 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); constructor(protected activeModal: NgbActiveModal, protected route: ActivatedRoute, private router: Router) { super(activeModal, route); 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 4a226729888..a13be638803 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 a7f583df50f..77458d98022 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); constructor(protected activeModal: NgbActiveModal, protected route: ActivatedRoute, private router: Router) { super(activeModal, route); 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 85d8797e660..54044f5d796 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 113ca518fdc..3f81687c9f8 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', @@ -49,6 +50,11 @@ export abstract class DSOSelectorModalWrapperComponent implements OnInit { */ action: SelectorActionType; + /** + * Default DSO ordering + */ + defaultSort: SortOptions; + constructor(protected activeModal: NgbActiveModal, protected route: ActivatedRoute) { } 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 cfc2ea282da..fd54cd44ed2 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); constructor(protected activeModal: NgbActiveModal, protected route: ActivatedRoute, private router: Router) { super(activeModal, route); 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 d73a7b48c5f..cf2f97c6d36 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); constructor(protected activeModal: NgbActiveModal, protected route: ActivatedRoute, private router: Router) { super(activeModal, route); 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/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/file-download-link/file-download-link.component.html b/src/app/shared/file-download-link/file-download-link.component.html index ba81ee3d203..8ebe622a5ba 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,5 +1,5 @@ - + 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 2381ada66d1..9e1f1d48aa1 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 @@ -16,7 +16,7 @@

    -
    { const testModel = mockInputWithTypeBindModel; testModel.typeBindRelations = getTypeBindRelations(['boundType']); +<<<<<<< HEAD const dcTypeControl = new FormControl(); +======= + const dcTypeControl = new UntypedFormControl(); +>>>>>>> dspace-7.6.1 dcTypeControl.setValue('boundType'); let subscriptions = service.subscribeRelations(testModel, dcTypeControl); expect(subscriptions).toHaveSize(1); @@ -96,7 +104,11 @@ describe('DSDynamicTypeBindRelationService test suite', () => { it('Expect hasMatch to be true (ie. this should be hidden)', () => { const testModel = mockInputWithTypeBindModel; testModel.typeBindRelations = getTypeBindRelations(['boundType']); +<<<<<<< HEAD const dcTypeControl = new FormControl(); +======= + const dcTypeControl = new UntypedFormControl(); +>>>>>>> dspace-7.6.1 dcTypeControl.setValue('boundType'); testModel.typeBindRelations[0].when[0].value = 'anotherType'; const relation = dynamicFormRelationService.findRelationByMatcher((testModel as any).typeBindRelations, HIDDEN_MATCHER); @@ -111,7 +123,11 @@ describe('DSDynamicTypeBindRelationService test suite', () => { it('Expect hasMatch to be false (ie. this should NOT be hidden)', () => { const testModel = mockInputWithTypeBindModel; testModel.typeBindRelations = getTypeBindRelations(['boundType']); +<<<<<<< HEAD const dcTypeControl = new FormControl(); +======= + const dcTypeControl = new UntypedFormControl(); +>>>>>>> dspace-7.6.1 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..f977c9632fa 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,9 @@ import { Inject, Injectable, Injector, Optional } from '@angular/core'; +<<<<<<< HEAD import { FormControl } from '@angular/forms'; +======= +import { UntypedFormControl } from '@angular/forms'; +>>>>>>> dspace-7.6.1 import { Subscription } from 'rxjs'; import { startWith } from 'rxjs/operators'; @@ -172,7 +176,11 @@ export class DsDynamicTypeBindRelationService { * @param model * @param control */ +<<<<<<< HEAD subscribeRelations(model: DynamicFormControlModel, control: FormControl): Subscription[] { +======= + subscribeRelations(model: DynamicFormControlModel, control: UntypedFormControl): Subscription[] { +>>>>>>> dspace-7.6.1 const relatedModels = this.getRelatedFormModel(model); const subscriptions: Subscription[] = []; @@ -183,7 +191,12 @@ export class DsDynamicTypeBindRelationService { const initValue = (hasNoValue(relatedModel.value) || typeof relatedModel.value === 'string') ? relatedModel.value : (Array.isArray(relatedModel.value) ? relatedModel.value : relatedModel.value.value); +<<<<<<< HEAD const valueChanges = relatedModel.valueChanges.pipe( +======= + const updateSubject = (relatedModel.type === 'CHECKBOX_GROUP' ? relatedModel.valueUpdates : relatedModel.valueChanges); + const valueChanges = updateSubject.pipe( +>>>>>>> dspace-7.6.1 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 01bba74cc84..9d48bdac216 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 f23b6c1e7b3..2eb6e9291c4 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 { NgbDatepicker, NgbDatepickerConfig } from '@ng-bootstrap/ng-bootstrap'; import { DynamicDatePickerModel, @@ -16,7 +16,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.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.html index 1046dd6b2d3..26803f3c67b 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.html @@ -1,6 +1,6 @@
    - + {{model.placeholder}} * { let dateFixture: ComponentFixture; let html; + const renderer2: Renderer2 = { + selectRootElement: jasmine.createSpy('selectRootElement'), + querySelector: jasmine.createSpy('querySelector'), + } as unknown as Renderer2; + // waitForAsync beforeEach beforeEach(waitForAsync(() => { @@ -54,7 +61,8 @@ describe('DsDatePickerComponent test suite', () => { ChangeDetectorRef, DsDatePickerComponent, { provide: DynamicFormLayoutService, useValue: mockDynamicFormLayoutService }, - { provide: DynamicFormValidationService, useValue: mockDynamicFormValidationService } + { provide: DynamicFormValidationService, useValue: mockDynamicFormValidationService }, + { provide: Renderer2, useValue: renderer2 }, ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }); @@ -233,6 +241,102 @@ describe('DsDatePickerComponent test suite', () => { expect(dateComp.disabledMonth).toBeFalsy(); expect(dateComp.disabledDay).toBeFalsy(); }); + + it('should move focus on month field when on year field and tab pressed', fakeAsync(() => { + const event = { + field: 'day', + value: null + }; + const event1 = { + field: 'month', + value: null + }; + dateComp.onChange(event); + dateComp.onChange(event1); + + const yearElement = dateFixture.debugElement.query(By.css(`#${dateComp.model.id}_year`)); + const monthElement = dateFixture.debugElement.query(By.css(`#${dateComp.model.id}_month`)); + + yearElement.nativeElement.focus(); + dateFixture.detectChanges(); + + expect(document.activeElement).toBe(yearElement.nativeElement); + + dateFixture.nativeElement.dispatchEvent(new KeyboardEvent('keydown', { key: 'tab' })); + dateFixture.detectChanges(); + + tick(200); + dateFixture.detectChanges(); + + expect(document.activeElement).toBe(monthElement.nativeElement); + })); + + it('should move focus on day field when on month field and tab pressed', fakeAsync(() => { + const event = { + field: 'day', + value: null + }; + dateComp.onChange(event); + + const monthElement = dateFixture.debugElement.query(By.css(`#${dateComp.model.id}_month`)); + const dayElement = dateFixture.debugElement.query(By.css(`#${dateComp.model.id}_day`)); + + monthElement.nativeElement.focus(); + dateFixture.detectChanges(); + + expect(document.activeElement).toBe(monthElement.nativeElement); + + dateFixture.nativeElement.dispatchEvent(new KeyboardEvent('keydown', { key: 'tab' })); + dateFixture.detectChanges(); + + tick(200); + dateFixture.detectChanges(); + + expect(document.activeElement).toBe(dayElement.nativeElement); + })); + + it('should move focus on month field when on day field and shift tab pressed', fakeAsync(() => { + const event = { + field: 'day', + value: null + }; + dateComp.onChange(event); + + const monthElement = dateFixture.debugElement.query(By.css(`#${dateComp.model.id}_month`)); + const dayElement = dateFixture.debugElement.query(By.css(`#${dateComp.model.id}_day`)); + + dayElement.nativeElement.focus(); + dateFixture.detectChanges(); + + expect(document.activeElement).toBe(dayElement.nativeElement); + + dateFixture.nativeElement.dispatchEvent(new KeyboardEvent('keydown', { key: 'shift.tab' })); + dateFixture.detectChanges(); + + tick(200); + dateFixture.detectChanges(); + + expect(document.activeElement).toBe(monthElement.nativeElement); + })); + + it('should move focus on year field when on month field and shift tab pressed', fakeAsync(() => { + const yearElement = dateFixture.debugElement.query(By.css(`#${dateComp.model.id}_year`)); + const monthElement = dateFixture.debugElement.query(By.css(`#${dateComp.model.id}_month`)); + + monthElement.nativeElement.focus(); + dateFixture.detectChanges(); + + expect(document.activeElement).toBe(monthElement.nativeElement); + + dateFixture.nativeElement.dispatchEvent(new KeyboardEvent('keydown', { key: 'shift.tab' })); + dateFixture.detectChanges(); + + tick(200); + dateFixture.detectChanges(); + + expect(document.activeElement).toBe(yearElement.nativeElement); + })); + }); }); 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 78f99358298..404e8514933 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, Input, OnInit, Output } from '@angular/core'; -import { FormGroup } from '@angular/forms'; +import { UntypedFormGroup } from '@angular/forms'; +import { Component, EventEmitter, HostListener, Inject, Input, OnInit, Output, Renderer2 } from '@angular/core'; import { DynamicDsDatePickerModel } from './date-picker.model'; import { hasValue } from '../../../../../empty.util'; import { @@ -7,6 +7,11 @@ import { DynamicFormLayoutService, DynamicFormValidationService } from '@ng-dynamic-forms/core'; +import { DOCUMENT } from '@angular/common'; +import isEqual from 'lodash/isEqual'; + + +export type DatePickerFieldType = '_year' | '_month' | '_day'; export const DS_DATE_PICKER_SEPARATOR = '-'; @@ -18,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; @@ -50,8 +55,12 @@ export class DsDatePickerComponent extends DynamicFormControlComponent implement disabledMonth = true; disabledDay = true; + private readonly fields: DatePickerFieldType[] = ['_year', '_month', '_day']; + constructor(protected layoutService: DynamicFormLayoutService, - protected validationService: DynamicFormValidationService + protected validationService: DynamicFormValidationService, + private renderer: Renderer2, + @Inject(DOCUMENT) private _document: Document ) { super(layoutService, validationService); } @@ -80,9 +89,8 @@ export class DsDatePickerComponent extends DynamicFormControlComponent implement } } - this.maxYear = this.initialYear + 100; - - } + this.maxYear = now.getUTCFullYear() + 100; + } onBlur(event) { this.blur.emit(); @@ -166,6 +174,67 @@ export class DsDatePickerComponent extends DynamicFormControlComponent implement this.change.emit(value); } + /** + * Listen to keydown Tab event. + * Get the active element and blur it, in order to focus the next input field. + */ + @HostListener('keydown.tab', ['$event']) + onTabKeydown(event: KeyboardEvent) { + event.preventDefault(); + const activeElement: Element = this._document.activeElement; + (activeElement as any).blur(); + const index = this.selectedFieldIndex(activeElement); + if (index < 0) { + return; + } + let fieldToFocusOn = index + 1; + if (fieldToFocusOn < this.fields.length) { + this.focusInput(this.fields[fieldToFocusOn]); + } + } + + @HostListener('keydown.shift.tab', ['$event']) + onShiftTabKeyDown(event: KeyboardEvent) { + event.preventDefault(); + const activeElement: Element = this._document.activeElement; + (activeElement as any).blur(); + const index = this.selectedFieldIndex(activeElement); + let fieldToFocusOn = index - 1; + if (fieldToFocusOn >= 0) { + this.focusInput(this.fields[fieldToFocusOn]); + } + } + + private selectedFieldIndex(activeElement: Element): number { + return this.fields.findIndex(field => isEqual(activeElement.id, this.model.id.concat(field))); + } + + /** + * Focus the input field for the given type + * based on the model id. + * Used to focus the next input field + * in case of a disabled field. + * @param type DatePickerFieldType + */ + focusInput(type: DatePickerFieldType) { + const field = this._document.getElementById(this.model.id.concat(type)); + if (field) { + + if (hasValue(this.year) && isEqual(type, '_year')) { + this.disabledMonth = true; + this.disabledDay = true; + } + if (hasValue(this.year) && isEqual(type, '_month')) { + this.disabledMonth = false; + } else if (hasValue(this.month) && isEqual(type, '_day')) { + this.disabledDay = false; + } + setTimeout(() => { + this.renderer.selectRootElement(field).focus(); + }, 100); + } + } + onFocus(event) { this.focus.emit(event); } 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 5af9b2bd323..0a45b587dc3 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,10 @@ export const DYNAMIC_FORM_CONTROL_TYPE_DSDATEPICKER = 'DATE'; export interface DynamicDsDateControlModelConfig extends DynamicDatePickerModelConfig { legend?: string; typeBindRelations?: DynamicFormControlRelation[]; +<<<<<<< HEAD +======= + repeatable: boolean; +>>>>>>> dspace-7.6.1 } /** @@ -37,7 +41,11 @@ export class DynamicDsDatePickerModel extends DynamicDateControlModel { this.metadataValue = (config as any).metadataValue; this.typeBindRelations = config.typeBindRelations ? config.typeBindRelations : []; this.hiddenUpdates = new BehaviorSubject(this.hidden); +<<<<<<< HEAD +======= + this.repeatable = config.repeatable; +>>>>>>> dspace-7.6.1 // This was a subscription, then an async setTimeout, but it seems unnecessary const parentModel = this.getRootParent(this); if (parentModel && isNotUndefined(parentModel.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 1d6037a4097..7f61e9812f7 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 @@ -46,6 +46,7 @@ export class DynamicConcatModel extends DynamicFormGroupModel { @serializable() submissionId: string; @serializable() hasSelectableMetadata: boolean; @serializable() metadataValue: MetadataValue; + @serializable() readOnly?: boolean; isCustomGroup = true; valueUpdates: Subject; @@ -65,6 +66,10 @@ export class DynamicConcatModel extends DynamicFormGroupModel { this.valueUpdates = new Subject(); this.valueUpdates.subscribe((value: string) => this.value = value); this.typeBindRelations = config.typeBindRelations ? config.typeBindRelations : []; +<<<<<<< HEAD +======= + this.readOnly = config.disabled; +>>>>>>> dspace-7.6.1 } get value() { 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 bab0c8607cf..3c6abaa851a 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 @@ -9,7 +9,7 @@ import {Subject} from 'rxjs'; import { LanguageCode } from '../../models/form-field-language-value.model'; import { VocabularyOptions } from '../../../../../core/submission/vocabularies/models/vocabulary-options.model'; -import { hasValue } from '../../../../empty.util'; +import {hasValue} from '../../../../empty.util'; import { FormFieldMetadataValueObject } from '../../models/form-field-metadata-value.model'; import { RelationshipOptions } from '../../models/relationship-options.model'; @@ -55,6 +55,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 4c244436336..f19b6602955 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, @@ -25,7 +25,7 @@ import { PageInfo } from '../../../../../core/shared/page-info.model'; }) export abstract class DsDynamicVocabularyComponent extends DynamicFormControlComponent { - @Input() abstract group: FormGroup; + @Input() abstract group: UntypedFormGroup; @Input() abstract model: DsDynamicInputModel; @Output() abstract blur: EventEmitter; @@ -53,7 +53,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.model.value.hasAuthority()) { 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 9d8d73eab50..cfd1bc293c6 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,5 +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, @@ -22,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 7cffdfe801a..8ab103383ab 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,8 +15,15 @@ export interface DynamicListCheckboxGroupModelConfig extends DynamicFormGroupMod vocabularyOptions: VocabularyOptions; groupLength?: number; repeatable: boolean; +<<<<<<< HEAD value?: any; typeBindRelations?: DynamicFormControlRelation[]; +======= + value?: VocabularyEntry[]; + typeBindRelations?: DynamicFormControlRelation[]; + required: boolean; + hint?: string; +>>>>>>> dspace-7.6.1 } export class DynamicListCheckboxGroupModel extends DynamicCheckboxGroupModel { @@ -26,6 +33,11 @@ export class DynamicListCheckboxGroupModel extends DynamicCheckboxGroupModel { @serializable() groupLength: number; @serializable() _value: VocabularyEntry[]; @serializable() typeBindRelations: DynamicFormControlRelation[]; +<<<<<<< HEAD +======= + @serializable() required: boolean; + @serializable() hint: string; +>>>>>>> dspace-7.6.1 isListGroup = true; valueUpdates: Subject; @@ -36,6 +48,8 @@ export class DynamicListCheckboxGroupModel extends DynamicCheckboxGroupModel { this.groupLength = config.groupLength || 5; this._value = []; this.repeatable = config.repeatable; + this.required = config.required; + this.hint = config.hint; this.valueUpdates = new Subject(); this.valueUpdates.subscribe((value: VocabularyEntry | VocabularyEntry[]) => this.value = value); @@ -56,9 +70,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 7e5cad353be..728c59aa460 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]="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 b4a02457dcf..51ce584bb1d 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'; @@ -136,9 +136,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(); @@ -184,9 +184,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 })]; @@ -224,9 +224,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(); @@ -260,9 +260,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 }); @@ -291,9 +291,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 d44d24f8b8f..16c46fc26b6 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,6 +1,5 @@ import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; -import { FormGroup } from '@angular/forms'; - +import { UntypedFormGroup, ValidatorFn, ValidationErrors, AbstractControl } from '@angular/forms'; import { DynamicCheckboxModel, DynamicFormControlComponent, @@ -36,7 +35,7 @@ export interface ListItem { }) export class DsDynamicListComponent extends DynamicFormControlComponent implements OnInit { - @Input() group: FormGroup; + @Input() group: UntypedFormGroup; @Input() model: any; @Output() blur: EventEmitter = new EventEmitter(); @@ -109,7 +108,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); @@ -121,14 +123,14 @@ 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; const checked: boolean = isNotEmpty(findKey( this.model.value, (v) => v.value === option.value)); const item: ListItem = { - id: value, + id: `${this.model.id}_${value}`, label: option.display, value: checked, index: key @@ -156,4 +158,13 @@ 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; + }; + } + } 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 27029ff2be8..2fea4fc9856 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 } 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'; @@ -71,9 +71,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() }); describe('Dynamic Lookup component', () => { @@ -122,9 +122,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() }); } @@ -564,7 +564,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 39937048955..63545f45d2f 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'; @@ -30,7 +30,7 @@ import { DsDynamicVocabularyComponent } from '../dynamic-vocabulary.component'; }) 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 e6b0cf508f5..3c19ecda13f 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 @@ -39,6 +39,7 @@ [ngbTypeahead]="search" [placeholder]="model.placeholder" [readonly]="model.readOnly" + [disabled]="model.readOnly" [resultTemplate]="rt" [type]="model.inputType" [(ngModel)]="currentValue" @@ -63,6 +64,7 @@ [name]="model.name" [placeholder]="model.placeholder" [readonly]="true" + [disabled]="model.readOnly" [type]="model.inputType" [value]="currentValue?.display" (focus)="onFocus($event)" 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 cf417145a70..69520aba633 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 } 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'; @@ -47,8 +47,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 = { @@ -457,7 +457,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 008328bf734..db7d177d66a 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 { DynamicFormLayoutService, DynamicFormValidationService } from '@ng-dynamic-forms/core'; import { @@ -30,8 +30,12 @@ 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'; +<<<<<<< HEAD import { VocabularyTreeviewComponent } from '../../../../vocabulary-treeview/vocabulary-treeview.component'; +======= +>>>>>>> dspace-7.6.1 import { VocabularyEntryDetail } from '../../../../../../core/submission/vocabularies/models/vocabulary-entry-detail.model'; +import { VocabularyTreeviewModalComponent } from '../../../../vocabulary-treeview-modal/vocabulary-treeview-modal.component'; /** * Component representing a onebox input field. @@ -44,7 +48,7 @@ import { VocabularyEntryDetail } from '../../../../../../core/submission/vocabul }) export class DsDynamicOneboxComponent extends DsDynamicVocabularyComponent implements OnInit { - @Input() group: FormGroup; + @Input() group: UntypedFormGroup; @Input() model: DynamicOneboxModel; @Output() blur: EventEmitter = new EventEmitter(); @@ -216,16 +220,19 @@ export class DsDynamicOneboxComponent extends DsDynamicVocabularyComponent imple * @param event The click event fired */ openTree(event) { + if (this.model.readOnly) { + return; + } event.preventDefault(); event.stopImmediatePropagation(); this.subs.push(this.vocabulary$.pipe( map((vocabulary: Vocabulary) => vocabulary.preloadLevel), take(1) ).subscribe((preloadLevel) => { - const modalRef: NgbModalRef = this.modalService.open(VocabularyTreeviewComponent, { size: 'lg', windowClass: 'treeview' }); + 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.value] : []; modalRef.result.then((result: VocabularyEntryDetail) => { 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 733758fd27a..feca7f95c61 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 } 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'; @@ -83,8 +83,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(), }); } @@ -96,9 +96,9 @@ describe('DsDynamicRelationGroupComponent test suite', () => { let groupFixture: ComponentFixture; let modelValue: any; let html; - let control1: FormControl; + let control1: UntypedFormControl; let model1: DsDynamicInputModel; - let control2: FormControl; + let control2: UntypedFormControl; let model2: DsDynamicInputModel; // waitForAsync beforeEach @@ -170,9 +170,9 @@ describe('DsDynamicRelationGroupComponent test suite', () => { groupComp.group = FORM_GROUP_TEST_GROUP; groupComp.model = new DynamicRelationGroupModel(FORM_GROUP_TEST_MODEL_CONFIG); groupFixture.detectChanges(); - control1 = service.getFormControlById('dc_contributor_author', (groupComp as any).formRef.formGroup, groupComp.formModel) as FormControl; + control1 = service.getFormControlById('dc_contributor_author', (groupComp as any).formRef.formGroup, groupComp.formModel) as UntypedFormControl; model1 = service.findById('dc_contributor_author', groupComp.formModel) as DsDynamicInputModel; - control2 = service.getFormControlById('local_contributor_affiliation', (groupComp as any).formRef.formGroup, groupComp.formModel) as FormControl; + control2 = service.getFormControlById('local_contributor_affiliation', (groupComp as any).formRef.formGroup, groupComp.formModel) as UntypedFormControl; model2 = service.findById('local_contributor_affiliation', groupComp.formModel) as DsDynamicInputModel; // spyOn(store, 'dispatch'); @@ -272,7 +272,7 @@ describe('DsDynamicRelationGroupComponent test suite', () => { groupComp.onChipSelected(0); groupFixture.detectChanges(); - control1 = service.getFormControlById('dc_contributor_author', (groupComp as any).formRef.formGroup, groupComp.formModel) as FormControl; + control1 = service.getFormControlById('dc_contributor_author', (groupComp as any).formRef.formGroup, groupComp.formModel) as UntypedFormControl; model1 = service.findById('dc_contributor_author', groupComp.formModel) as DsDynamicInputModel; control1.setValue('test author modify'); 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 fd111e44c2a..7fdfb61b74e 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, ViewChild } 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 } from 'rxjs/operators'; @@ -43,7 +43,7 @@ import { VocabularyEntryDetail } from '../../../../../../core/submission/vocabul export class DsDynamicRelationGroupComponent extends DynamicFormControlComponent implements OnDestroy, OnInit { @Input() formId: string; - @Input() group: FormGroup; + @Input() group: UntypedFormGroup; @Input() model: DynamicRelationGroupModel; @Output() blur: EventEmitter = new EventEmitter(); 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 8a4d502287f..1ac38e9943c 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 @@ -3,8 +3,10 @@ role="combobox" [attr.aria-label]="model.label" [attr.aria-owns]="'combobox_' + id + '_listbox'"> - + + + (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..a9b5dc9cd1e 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,22 @@ export class DsDynamicLookupRelationExternalSourceTabComponent implements OnInit */ relatedEntityType: ItemType; +<<<<<<< HEAD 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, +>>>>>>> dspace-7.6.1 ) { } @@ -148,8 +164,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..5cd26c71092 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,17 @@ 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, +<<<<<<< HEAD private relationshipService: RelationshipDataService, private paginationService: PaginationService +======= + protected relationshipService: RelationshipDataService, + protected paginationService: PaginationService, +>>>>>>> dspace-7.6.1 ) { } 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 b68963e5ad5..5e045c88ed5 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 @@ -235,10 +235,16 @@ describe('FormBuilderService test suite', () => { new DynamicListCheckboxGroupModel({ id: 'testCheckboxList', vocabularyOptions: vocabularyOptions, - repeatable: true + repeatable: true, + required: false, }), - new DynamicListRadioGroupModel({ id: 'testRadioList', vocabularyOptions: vocabularyOptions, repeatable: false }), + new DynamicListRadioGroupModel({ + id: 'testRadioList', + vocabularyOptions: vocabularyOptions, + repeatable: false, + required: false, + }), new DynamicRelationGroupModel({ submissionId, @@ -284,7 +290,7 @@ describe('FormBuilderService test suite', () => { hasSelectableMetadata: true }), - new DynamicDsDatePickerModel({ id: 'testDate' }), + new DynamicDsDatePickerModel({ id: 'testDate', repeatable: false}), new DynamicLookupModel({ id: 'testLookup', @@ -677,21 +683,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', () => { @@ -714,7 +720,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' }); @@ -725,14 +731,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' }); @@ -744,7 +750,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']); }); @@ -763,14 +769,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(); @@ -804,7 +810,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); }); @@ -835,8 +841,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'; @@ -845,8 +851,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'); @@ -859,8 +865,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'; @@ -869,8 +875,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 35c6b9d0778..87a6d8214ef 100644 --- a/src/app/shared/form/builder/form-builder.service.ts +++ b/src/app/shared/form/builder/form-builder.service.ts @@ -1,5 +1,9 @@ import {Injectable, Optional} from '@angular/core'; +<<<<<<< HEAD import { AbstractControl, FormControl, FormGroup } from '@angular/forms'; +======= +import { AbstractControl, UntypedFormControl, UntypedFormGroup } from '@angular/forms'; +>>>>>>> dspace-7.6.1 import { DYNAMIC_FORM_CONTROL_TYPE_ARRAY, @@ -45,10 +49,13 @@ import { CONCAT_GROUP_SUFFIX, DynamicConcatModel } from './ds-dynamic-form-ui/mo import { VIRTUAL_METADATA_PREFIX } from '../../../core/shared/metadata.models'; import { ConfigurationDataService } from '../../../core/data/configuration-data.service'; import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; +<<<<<<< HEAD import { COMPLEX_GROUP_SUFFIX, DynamicComplexModel } from './ds-dynamic-form-ui/models/ds-dynamic-complex.model'; +======= +>>>>>>> dspace-7.6.1 @Injectable() export class FormBuilderService extends DynamicFormService { @@ -63,7 +70,11 @@ export class FormBuilderService extends DynamicFormService { /** * This map contains the active forms control groups */ +<<<<<<< HEAD private formGroups: Map; +======= + private formGroups: Map; +>>>>>>> dspace-7.6.1 /** * This is the field to use for type binding @@ -87,7 +98,11 @@ export class FormBuilderService extends DynamicFormService { } +<<<<<<< HEAD createDynamicFormControlEvent(control: FormControl, group: FormGroup, model: DynamicFormControlModel, type: string): DynamicFormControlEvent { +======= + createDynamicFormControlEvent(control: UntypedFormControl, group: UntypedFormGroup, model: DynamicFormControlModel, type: string): DynamicFormControlEvent { +>>>>>>> dspace-7.6.1 const $event = { value: (model as any).value, autoSave: false @@ -124,6 +139,7 @@ export class FormBuilderService extends DynamicFormService { if (this.isConcatGroup(controlModel)) { if (controlModel.id.match(new RegExp(findId + CONCAT_GROUP_SUFFIX))) { result = (controlModel as DynamicConcatModel); +<<<<<<< HEAD break; } } @@ -132,6 +148,8 @@ export class FormBuilderService extends DynamicFormService { const regex = new RegExp(findId + COMPLEX_GROUP_SUFFIX); if (controlModel.id.match(regex)) { result = (controlModel as DynamicComplexModel); +======= +>>>>>>> dspace-7.6.1 break; } } @@ -385,12 +403,16 @@ 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; } +<<<<<<< HEAD getFormControlByModel(formGroup: FormGroup, fieldModel: DynamicFormControlModel): AbstractControl { +======= + getFormControlByModel(formGroup: UntypedFormGroup, fieldModel: DynamicFormControlModel): AbstractControl { +>>>>>>> dspace-7.6.1 return isNotEmpty(fieldModel) ? formGroup.get(this.getPath(fieldModel)) : null; } @@ -428,7 +450,11 @@ export class FormBuilderService extends DynamicFormService { * @param id id of model * @param formGroup FormGroup */ +<<<<<<< HEAD addFormGroups(id: string, formGroup: FormGroup): void { +======= + addFormGroups(id: string, formGroup: UntypedFormGroup): void { +>>>>>>> dspace-7.6.1 this.formGroups.set(id, formGroup); } 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 b58844a5379..9b18084e402 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 @@ -11,6 +11,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; @@ -58,6 +62,13 @@ export class FormFieldMetadataValueObject implements MetadataValueInterface { return isNotEmpty(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 ea3a8c363f4..196f3e46c6d 100644 --- a/src/app/shared/form/builder/models/form-field.model.ts +++ b/src/app/shared/form/builder/models/form-field.model.ts @@ -1,3 +1,4 @@ +import { SectionVisibility } from './../../../../submission/objects/section-visibility.model'; import { autoserialize } from 'cerialize'; import { LanguageCode } from './form-field-language-value.model'; @@ -125,9 +126,14 @@ export class FormFieldModel { @autoserialize value: any; +<<<<<<< HEAD /** * Containing the definition of the complex input types - multiple inputs in one row */ @autoserialize complexDefinition: string; +======= + @autoserialize + visibility: SectionVisibility; +>>>>>>> dspace-7.6.1 } 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 a2076484f43..e86de70c816 100644 --- a/src/app/shared/form/builder/parsers/concat-field-parser.ts +++ b/src/app/shared/form/builder/parsers/concat-field-parser.ts @@ -83,6 +83,8 @@ export class ConcatFieldParser extends FieldParser { input1ModelConfig.required = true; } + concatGroup.disabled = input1ModelConfig.readOnly; + if (isNotEmpty(this.firstPlaceholder)) { input1ModelConfig.placeholder = this.firstPlaceholder; } diff --git a/src/app/shared/form/builder/parsers/field-parser.ts b/src/app/shared/form/builder/parsers/field-parser.ts index 6dc734c96a9..c9c5fcbaf86 100644 --- a/src/app/shared/form/builder/parsers/field-parser.ts +++ b/src/app/shared/form/builder/parsers/field-parser.ts @@ -1,3 +1,5 @@ +import { SectionVisibility } from './../../../../submission/objects/section-visibility.model'; +import { VisibilityType } from './../../../../submission/sections/visibility-type'; import { Inject, InjectionToken } from '@angular/core'; import uniqueId from 'lodash/uniqueId'; @@ -22,6 +24,7 @@ import { RelationshipOptions } from '../models/relationship-options.model'; import { VocabularyOptions } from '../../../../core/submission/vocabularies/models/vocabulary-options.model'; import { ParserType } from './parser-type'; import { isNgbDateStruct } from '../../../date.util'; +import { SubmissionScopeType } from '../../../../core/submission/submission-scope-type'; export const SUBMISSION_ID: InjectionToken = new InjectionToken('submissionId'); export const CONFIG_DATA: InjectionToken = new InjectionToken('configData'); @@ -289,8 +292,8 @@ export abstract class FieldParser { controlModel.id = (this.fieldId).replace(/\./g, '_'); // Set read only option - controlModel.readOnly = this.parserOptions.readOnly; - controlModel.disabled = this.parserOptions.readOnly; + controlModel.readOnly = this.parserOptions.readOnly || this.isFieldReadOnly(this.configData.visibility, this.configData.scope, this.parserOptions.submissionScope); + controlModel.disabled = controlModel.readOnly; if (hasValue(this.configData.selectableRelationship)) { controlModel.relationship = Object.assign(new RelationshipOptions(), this.configData.selectableRelationship); } @@ -329,6 +332,31 @@ export abstract class FieldParser { } /** +<<<<<<< HEAD +======= + * Checks if a field is read-only with the given scope. + * The field is readonly when submissionScope is WORKSPACE and the main visibility is READONLY + * or when submissionScope is WORKFLOW and the other visibility is READONLY + * @param visibility + * @param submissionScope + */ + private isFieldReadOnly(visibility: SectionVisibility, fieldScope: string, submissionScope: string) { + return isNotEmpty(submissionScope) + && isNotEmpty(fieldScope) + && isNotEmpty(visibility) + && (( + submissionScope === SubmissionScopeType.WorkspaceItem + && visibility.main === VisibilityType.READONLY + ) + || + (visibility.other === VisibilityType.READONLY + && submissionScope === SubmissionScopeType.WorkflowItem + ) + ); + } + + /** +>>>>>>> dspace-7.6.1 * Get the type bind values from the REST data for a specific field * The return value is any[] in the method signature but in reality it's * returning the 'relation' that'll be used for a dynamic matcher when filtering 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 3dafa4eebd7..606c41ea748 100644 --- a/src/app/shared/form/builder/parsers/onebox-field-parser.ts +++ b/src/app/shared/form/builder/parsers/onebox-field-parser.ts @@ -59,19 +59,20 @@ export class OneboxFieldParser extends FieldParser { this.setLabel(inputSelectGroup, label); inputSelectGroup.required = isNotEmpty(this.configData.mandatory); + const inputModelConfig: DsDynamicInputModelConfig = this.initModel(newId + QUALDROP_VALUE_SUFFIX, label, false, false); + inputModelConfig.hint = null; + this.setValues(inputModelConfig, fieldValue); + const selectModelConfig: DynamicSelectModelConfig = this.initModel(newId + QUALDROP_METADATA_SUFFIX, label, false, false); selectModelConfig.hint = null; this.setOptions(selectModelConfig); if (isNotEmpty(fieldValue)) { selectModelConfig.value = fieldValue.metadata; } - inputSelectGroup.group.push(new DynamicSelectModel(selectModelConfig, clsSelect)); - - const inputModelConfig: DsDynamicInputModelConfig = this.initModel(newId + QUALDROP_VALUE_SUFFIX, label, false, false); - inputModelConfig.hint = null; - this.setValues(inputModelConfig, fieldValue); + selectModelConfig.disabled = inputModelConfig.readOnly; inputSelectGroup.readOnly = selectModelConfig.disabled && inputModelConfig.readOnly; + 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/row-parser.ts b/src/app/shared/form/builder/parsers/row-parser.ts index 2818e37b25f..3f5b4a04c61 100644 --- a/src/app/shared/form/builder/parsers/row-parser.ts +++ b/src/app/shared/form/builder/parsers/row-parser.ts @@ -1,9 +1,11 @@ +import { SubmissionFieldScopeType } from './../../../../core/submission/submission-field-scope-type'; +import { SectionVisibility } from './../../../../submission/objects/section-visibility.model'; import { Injectable, Injector } from '@angular/core'; import { DYNAMIC_FORM_CONTROL_TYPE_ARRAY, DynamicFormGroupModelConfig } from '@ng-dynamic-forms/core'; import uniqueId from 'lodash/uniqueId'; -import { isEmpty } from '../../../empty.util'; +import { isEmpty, isNotEmpty } from '../../../empty.util'; import { DynamicRowGroupModel } from '../ds-dynamic-form-ui/models/ds-dynamic-row-group-model'; import { FormFieldModel } from '../models/form-field.model'; import { CONFIG_DATA, FieldParser, INIT_FORM_VALUES, PARSER_OPTIONS, SUBMISSION_ID } from './field-parser'; @@ -12,6 +14,7 @@ import { ParserOptions } from './parser-options'; import { ParserType } from './parser-type'; import { setLayout } from './parser.utils'; import { DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP } from '../ds-dynamic-form-ui/ds-dynamic-form-constants'; +import { SubmissionScopeType } from '../../../../core/submission/submission-scope-type'; export const ROW_ID_PREFIX = 'df-row-group-config-'; @@ -118,15 +121,37 @@ export class RowParser { return parsedResult; } - checksFieldScope(fieldScope, submissionScope) { - return (isEmpty(fieldScope) || isEmpty(submissionScope) || fieldScope === submissionScope); + checksFieldScope(fieldScope, submissionScope, visibility: SectionVisibility) { + return (isEmpty(fieldScope) || !this.isHidden(visibility, fieldScope, submissionScope)); + } + + /** + * Check if the field is hidden or not. + * It is hidden when we do have the scope, + * but we do not have the visibility, + * also the field scope should be different from the submissionScope. + * @param visibility The visibility of the field + * @param scope the scope of the field + * @param submissionScope the scope of the submission + * @returns If the field is hidden or not + */ + private isHidden(visibility: SectionVisibility, scope: string, submissionScope: string): boolean { + return isNotEmpty(scope) + && ( + isEmpty(visibility) + && ( + submissionScope === SubmissionScopeType.WorkspaceItem && scope !== SubmissionFieldScopeType.WorkspaceItem + || + submissionScope === SubmissionScopeType.WorkflowItem && scope !== SubmissionFieldScopeType.WorkflowItem + ) + ); } filterScopedFields(fields: FormFieldModel[], submissionScope): FormFieldModel[] { const filteredFields: FormFieldModel[] = []; fields.forEach((field: FormFieldModel) => { // Whether field scope doesn't match the submission scope, skip it - if (this.checksFieldScope(field.scope, submissionScope)) { + if (this.checksFieldScope(field.scope, submissionScope, field.visibility)) { filteredFields.push(field); } }); diff --git a/src/app/shared/form/chips/chips.component.html b/src/app/shared/form/chips/chips.component.html index 2233c1bd16d..a6b90b23aaa 100644 --- a/src/app/shared/form/chips/chips.component.html +++ b/src/app/shared/form/chips/chips.component.html @@ -1,5 +1,5 @@
    - +
    diff --git a/src/app/shared/form/chips/chips.component.spec.ts b/src/app/shared/form/chips/chips.component.spec.ts index 2b8a469bd17..050950ed4d5 100644 --- a/src/app/shared/form/chips/chips.component.spec.ts +++ b/src/app/shared/form/chips/chips.component.spec.ts @@ -122,7 +122,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); @@ -131,7 +131,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); @@ -158,7 +158,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); @@ -166,7 +166,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); diff --git a/src/app/shared/form/form.component.html b/src/app/shared/form/form.component.html index 0410cfb5dd1..1b27c9d308d 100644 --- a/src/app/shared/form/form.component.html +++ b/src/app/shared/form/form.component.html @@ -13,22 +13,20 @@ (ngbEvent)="onCustomEvent($event)"> -
    -
    +
    diff --git a/src/app/shared/form/form.component.ts b/src/app/shared/form/form.component.ts index 9c16a0a401c..e00cb740eeb 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, UntypedFormArray, UntypedFormControl, UntypedFormGroup } from '@angular/forms'; import { Observable, Subscription } from 'rxjs'; import { @@ -69,7 +69,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; /* eslint-disable @angular-eslint/no-output-rename */ @@ -123,9 +123,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; @@ -185,7 +185,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); } @@ -224,7 +224,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); } @@ -271,7 +271,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); @@ -297,7 +297,7 @@ export class FormComponent implements OnDestroy, OnInit { this.change.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.id, fieldIndex); @@ -331,7 +331,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)) { // In case of qualdrop value remove event must be dispatched before removing the control from array @@ -346,7 +346,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); @@ -361,17 +361,17 @@ export class FormComponent implements OnDestroy, OnInit { protected getEvent($event: any, arrayContext: DynamicFormArrayModel, index: number, type: string): DynamicFormControlEvent { const context = arrayContext.groups[index]; const itemGroupModel = context.context; - let group = this.formGroup.get(itemGroupModel.id) as FormGroup; + let group = 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 7cbc95a66d7..ec3c17bfe8f 100644 --- a/src/app/shared/form/form.module.ts +++ b/src/app/shared/form/form.module.ts @@ -32,7 +32,11 @@ 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'; +<<<<<<< HEAD import { VocabularyTreeviewService } from './vocabulary-treeview/vocabulary-treeview.service'; +======= +import { VocabularyTreeviewModalComponent } from './vocabulary-treeview-modal/vocabulary-treeview-modal.component'; +>>>>>>> dspace-7.6.1 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,9 +44,14 @@ 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'; +<<<<<<< HEAD import { DsDynamicAutocompleteComponent } from './builder/ds-dynamic-form-ui/models/autocomplete/ds-dynamic-autocomplete.component'; import { DsDynamicSponsorAutocompleteComponent } from './builder/ds-dynamic-form-ui/models/sponsor-autocomplete/ds-dynamic-sponsor-autocomplete.component'; import { DsDynamicSponsorScrollableDropdownComponent } from './builder/ds-dynamic-form-ui/models/sponsor-scrollable-dropdown/dynamic-sponsor-scrollable-dropdown.component'; +======= +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'; +>>>>>>> dspace-7.6.1 const COMPONENTS = [ CustomSwitchComponent, @@ -51,8 +60,10 @@ const COMPONENTS = [ DsDynamicListComponent, DsDynamicLookupComponent, DsDynamicLookupRelationSearchTabComponent, + ThemedDynamicLookupRelationSearchTabComponent, DsDynamicLookupRelationSelectionTabComponent, DsDynamicLookupRelationExternalSourceTabComponent, + ThemedDynamicLookupRelationExternalSourceTabComponent, DsDynamicDisabledComponent, DsDynamicLookupRelationModalComponent, DsDynamicScrollableDropdownComponent, @@ -73,7 +84,12 @@ const COMPONENTS = [ ChipsComponent, NumberPickerComponent, VocabularyTreeviewComponent, +<<<<<<< HEAD ThemedExternalSourceEntryImportModalComponent +======= + VocabularyTreeviewModalComponent, + ThemedExternalSourceEntryImportModalComponent, +>>>>>>> dspace-7.6.1 ]; const DIRECTIVES = [ @@ -107,7 +123,10 @@ const DIRECTIVES = [ provide: DYNAMIC_FORM_CONTROL_MAP_FN, useValue: dsDynamicFormControlMapFn }, +<<<<<<< HEAD VocabularyTreeviewService, +======= +>>>>>>> dspace-7.6.1 DynamicFormLayoutService, DynamicFormService, DynamicFormValidationService, diff --git a/src/app/shared/form/form.service.spec.ts b/src/app/shared/form/form.service.spec.ts index e565c8f219d..e25951da22f 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, UntypedFormControl, UntypedFormGroup, Validators } from '@angular/forms'; import { DynamicFormControlModel, DynamicFormGroupModel, DynamicInputModel } from '@ng-dynamic-forms/core'; @@ -22,7 +22,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' }), @@ -104,6 +104,7 @@ describe('FormService test suite', () => { .subscribe((state) => { state.forms = formState; }); +<<<<<<< HEAD const author: AbstractControl = new FormControl('test'); const title: AbstractControl = new FormControl(undefined, Validators.required); const date: AbstractControl = new FormControl(undefined); @@ -116,6 +117,20 @@ describe('FormService test suite', () => { }); formGroup = new FormGroup({ author, title, date, description, addressLocation }); +======= + 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), + }); + + formGroup = new UntypedFormGroup({ author, title, date, description, addressLocation }); +>>>>>>> dspace-7.6.1 controls = { author, title, date, description , addressLocation }; service = new FormService(builderService, store); }) diff --git a/src/app/shared/form/form.service.ts b/src/app/shared/form/form.service.ts index 2dbf78f5658..8a216d85905 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) { @@ -162,7 +162,11 @@ export class FormService { } // if the field in question is a concat group, pass down the error to its fields +<<<<<<< HEAD if (field instanceof FormGroup && model instanceof DynamicFormGroupModel && this.formBuilderService.isConcatGroup(model)) { +======= + if (field instanceof UntypedFormGroup && model instanceof DynamicFormGroupModel && this.formBuilderService.isConcatGroup(model)) { +>>>>>>> dspace-7.6.1 model.group.forEach((subModel) => { const subField = field.controls[subModel.id]; @@ -183,7 +187,11 @@ export class FormService { } // if the field in question is a concat group, clear the error from its fields +<<<<<<< HEAD if (field instanceof FormGroup && model instanceof DynamicFormGroupModel && this.formBuilderService.isConcatGroup(model)) { +======= + if (field instanceof UntypedFormGroup && model instanceof DynamicFormGroupModel && this.formBuilderService.isConcatGroup(model)) { +>>>>>>> dspace-7.6.1 model.group.forEach((subModel) => { const subField = field.controls[subModel.id]; @@ -194,7 +202,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 0df1e050cd3..65c1d86e659 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,9 @@ import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output, SimpleChanges, } from '@angular/core'; +<<<<<<< HEAD:src/app/shared/form/number-picker/number-picker.component.ts import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR } from '@angular/forms'; +======= +import { ControlValueAccessor, UntypedFormBuilder, NG_VALUE_ACCESSOR } from '@angular/forms'; +>>>>>>> dspace-7.6.1:src/app/shared/number-picker/number-picker.component.ts import { isEmpty } from '../../empty.util'; @Component({ @@ -31,7 +35,7 @@ export class NumberPickerComponent implements OnInit, ControlValueAccessor { startValue: number; - constructor(private fb: FormBuilder, private cd: ChangeDetectorRef) { + constructor(private fb: UntypedFormBuilder, private cd: ChangeDetectorRef) { } ngOnInit() { @@ -103,13 +107,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..71eb8e14765 --- /dev/null +++ b/src/app/shared/form/vocabulary-treeview-modal/vocabulary-treeview-modal.component.html @@ -0,0 +1,16 @@ + + 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..590c69a1596 --- /dev/null +++ b/src/app/shared/form/vocabulary-treeview-modal/vocabulary-treeview-modal.component.spec.ts @@ -0,0 +1,33 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { VocabularyTreeviewModalComponent } from './vocabulary-treeview-modal.component'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateModule } from '@ngx-translate/core'; + +describe('VocabularyTreeviewModalComponent', () => { + let component: VocabularyTreeviewModalComponent; + let fixture: ComponentFixture; + + const modalStub = jasmine.createSpyObj('modalStub', ['close']); + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ TranslateModule.forRoot() ], + declarations: [ VocabularyTreeviewModalComponent ], + providers: [ + { provide: NgbActiveModal, useValue: modalStub }, + ], + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(VocabularyTreeviewModalComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); 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..c6b0bf20feb --- /dev/null +++ b/src/app/shared/form/vocabulary-treeview-modal/vocabulary-treeview-modal.component.ts @@ -0,0 +1,51 @@ +import { Component, Input } from '@angular/core'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +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 { + + /** + * 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: string[] = []; + + /** + * Whether to allow selecting multiple values with checkboxes + */ + @Input() multiSelect = false; + + /** + * Initialize instance variables + * + * @param {NgbActiveModal} activeModal + */ + constructor( + public activeModal: NgbActiveModal, + ) { } + + /** + * Method called on entry select + */ + onSelect(item: VocabularyEntryDetail) { + this.activeModal.close(item); + } +} 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.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 642d81bbd17..e5847b006d2 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 @@ -12,7 +12,11 @@ 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'; +<<<<<<< HEAD:src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.spec.ts import { TreeviewFlatNode } from './vocabulary-treeview-node.model'; +======= +import { TreeviewFlatNode, TreeviewNode } from './vocabulary-treeview-node.model'; +>>>>>>> dspace-7.6.1:src/app/shared/vocabulary-treeview/vocabulary-treeview.component.spec.ts 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'; @@ -20,6 +24,11 @@ import { VocabularyEntry } from '../../../core/submission/vocabularies/models/vo import { AuthTokenInfo } from '../../../core/auth/models/auth-token-info.model'; import { authReducer } from '../../../core/auth/auth.reducer'; import { storeModuleConfig } from '../../../app.reducer'; +<<<<<<< HEAD:src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.spec.ts +======= +import { By } from '@angular/platform-browser'; +import { VocabularyService } from '../../../core/submission/vocabularies/vocabulary.service'; +>>>>>>> dspace-7.6.1:src/app/shared/vocabulary-treeview/vocabulary-treeview.component.spec.ts describe('VocabularyTreeviewComponent test suite', () => { @@ -27,6 +36,7 @@ describe('VocabularyTreeviewComponent test suite', () => { let compAsAny: any; let fixture: ComponentFixture; let initialState; + let de; const item = new VocabularyEntryDetail(); item.id = 'node1'; @@ -47,6 +57,14 @@ describe('VocabularyTreeviewComponent test suite', () => { restoreNodes: jasmine.createSpy('restoreNodes'), cleanTree: jasmine.createSpy('cleanTree'), }); + 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') + }); initialState = { core: { @@ -75,6 +93,7 @@ describe('VocabularyTreeviewComponent test suite', () => { ], providers: [ { provide: VocabularyTreeviewService, useValue: vocabularyTreeviewServiceStub }, + { provide: VocabularyService, useValue: vocabularyServiceStub }, { provide: NgbActiveModal, useValue: modalStub }, provideMockStore({ initialState }), ChangeDetectorRef, @@ -117,13 +136,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 +151,14 @@ describe('VocabularyTreeviewComponent test suite', () => { currentValue.otherInformation = { id: 'entryID' }; - comp.selectedItem = currentValue; + comp.selectedItems = [currentValue.value]; fixture.detectChanges(); expect(comp.dataSource.data).toEqual([]); +<<<<<<< HEAD:src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.spec.ts expect(vocabularyTreeviewServiceStub.initialize).toHaveBeenCalledWith(comp.vocabularyOptions, new PageInfo(), null); +======= + expect(vocabularyTreeviewServiceStub.initialize).toHaveBeenCalledWith(comp.vocabularyOptions, new PageInfo(), ['testValue'], null); +>>>>>>> dspace-7.6.1:src/app/shared/vocabulary-treeview/vocabulary-treeview.component.spec.ts }); it('should should init component properly with init value as VocabularyEntry', () => { @@ -150,30 +167,34 @@ describe('VocabularyTreeviewComponent test suite', () => { currentValue.otherInformation = { id: 'entryID' }; - comp.selectedItem = currentValue; + comp.selectedItems = [currentValue.value]; fixture.detectChanges(); expect(comp.dataSource.data).toEqual([]); +<<<<<<< HEAD:src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.spec.ts expect(vocabularyTreeviewServiceStub.initialize).toHaveBeenCalledWith(comp.vocabularyOptions, new PageInfo(), null); +======= + expect(vocabularyTreeviewServiceStub.initialize).toHaveBeenCalledWith(comp.vocabularyOptions, new PageInfo(), ['testValue'], null); +>>>>>>> dspace-7.6.1:src/app/shared/vocabulary-treeview/vocabulary-treeview.component.spec.ts }); 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 +209,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 +220,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 +250,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 56909090c77..12a000de8bc 100644 --- a/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.ts +++ b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.ts @@ -1,8 +1,11 @@ import { FlatTreeControl } from '@angular/cdk/tree'; -import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; +import { Component, EventEmitter, Input, OnDestroy, OnInit, Output, OnChanges, SimpleChanges } from '@angular/core'; +<<<<<<< HEAD:src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.ts import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { map } from 'rxjs/operators'; +======= +>>>>>>> dspace-7.6.1:src/app/shared/vocabulary-treeview/vocabulary-treeview.component.ts import { Observable, Subscription } from 'rxjs'; import { Store } from '@ngrx/store'; import { TranslateService } from '@ngx-translate/core'; @@ -17,17 +20,22 @@ import { VocabularyEntry } from '../../../core/submission/vocabularies/models/vo import { VocabularyTreeFlattener } from './vocabulary-tree-flattener'; import { VocabularyTreeFlatDataSource } from './vocabulary-tree-flat-data-source'; import { CoreState } from '../../../core/core-state.model'; +<<<<<<< HEAD:src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.ts import { lowerCase } from 'lodash/string'; +======= +import { VocabularyService } from '../../../core/submission/vocabularies/vocabulary.service'; +import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators'; +>>>>>>> dspace-7.6.1:src/app/shared/vocabulary-treeview/vocabulary-treeview.component.ts /** - * 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 @@ -40,14 +48,14 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit { @Input() preloadLevel = 2; /** - * The vocabulary entry already selected, if any + * The vocabulary entries already selected, if any */ - @Input() selectedItem: any = null; + @Input() selectedItems: string[] = []; /** - * Contain a descriptive message for this vocabulary retrieved from i18n files + * Whether to allow selecting multiple values with checkboxes */ - description: Observable; + @Input() multiSelect = false; /** * A map containing the current node showed by the tree @@ -90,6 +98,12 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit { */ @Output() select: EventEmitter = new EventEmitter(null); + /** + * An event fired when a vocabulary entry is deselected. + * Event's payload equals to {@link VocabularyEntryDetail} deselected. + */ + @Output() deselect: EventEmitter = new EventEmitter(null); + /** * A boolean representing if user is authenticated */ @@ -103,14 +117,14 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit { /** * Initialize instance variables * - * @param {NgbActiveModal} activeModal * @param {VocabularyTreeviewService} vocabularyTreeviewService + * @param {vocabularyService} vocabularyService * @param {Store} store * @param {TranslateService} translate */ constructor( - public activeModal: NgbActiveModal, private vocabularyTreeviewService: VocabularyTreeviewService, + private vocabularyService: VocabularyService, private store: Store, private translate: TranslateService ) { @@ -148,7 +162,8 @@ 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); @@ -203,6 +218,7 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit { }) ); +<<<<<<< HEAD:src/app/shared/form/vocabulary-treeview/vocabulary-treeview.component.ts this.translate.get(`search.filters.filter.${this.vocabularyOptions.name}.head`).pipe( map((type) => lowerCase(type)), ).subscribe( @@ -212,6 +228,11 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit { this.loading = this.vocabularyTreeviewService.isLoading(); this.vocabularyTreeviewService.initialize(this.vocabularyOptions, new PageInfo(), null); +======= + this.loading = this.vocabularyTreeviewService.isLoading(); + + this.vocabularyTreeviewService.initialize(this.vocabularyOptions, new PageInfo(), this.selectedItems, null); +>>>>>>> dspace-7.6.1:src/app/shared/vocabulary-treeview/vocabulary-treeview.component.ts } /** @@ -219,7 +240,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.selectedItems); } /** @@ -227,7 +248,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.selectedItems); } /** @@ -235,16 +256,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.selectedItems, true); } /** - * Method called on entry select - * Emit a new select Event + * Method called on entry select/deselect */ onSelect(item: VocabularyEntryDetail) { - this.select.emit(item); - this.activeModal.close(item); + if (!this.selectedItems.includes(item.id)) { + this.selectedItems.push(item.id); + this.select.emit(item); + } else { + this.selectedItems = this.selectedItems.filter((detail: string) => { return detail !== item.id; }); + this.deselect.emit(item); + } } /** @@ -256,7 +281,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.selectedItems); } } @@ -271,15 +296,33 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit { * Reset tree resulting from a previous search */ reset() { + this.searchText = ''; + for (const item of this.selectedItems) { + this.subs.push(this.vocabularyService.findEntryDetailById(item, this.vocabularyOptions.name, true, true, false).pipe( + getFirstSucceededRemoteDataPayload(), + ).subscribe((detail: VocabularyEntryDetail) => { + this.deselect.emit(detail); + })); + this.nodeMap.get(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 = { + value: this.searchText, + display: this.searchText, + } as VocabularyEntryDetail; + this.select.emit(userVocabularyEntry); } + /** * Unsubscribe from all subscriptions */ @@ -296,4 +339,9 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit { private getEntryId(entry: VocabularyEntry): string { return entry.authority || entry.otherInformation.id || undefined; } + + ngOnChanges(changes: SimpleChanges): void { + this.reset(); + this.vocabularyTreeviewService.initialize(this.vocabularyOptions, new PageInfo(), this.selectedItems, 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 752ef10fee9..97be15a0418 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 8804716927d..f524af4c0e2 100644 --- a/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.service.ts +++ b/src/app/shared/form/vocabulary-treeview/vocabulary-treeview.service.ts @@ -25,7 +25,9 @@ import { VocabularyEntryDetail } from '../../../core/submission/vocabularies/mod /** * A service that provides methods to deal with vocabulary tree */ -@Injectable() +@Injectable({ + providedIn: 'root' +}) export class VocabularyTreeviewService { /** @@ -101,21 +103,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 +132,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; } @@ -154,7 +159,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)); children.push(...newNodes); if ((list.pageInfo.currentPage + 1) <= list.pageInfo.totalPages) { @@ -183,7 +188,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; @@ -200,7 +205,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; @@ -231,11 +236,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)!; @@ -243,13 +249,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(entry.id)); const result = new TreeviewNode( entry, hasChildren, pageInfo, null, isSearchNode, - isInInitValueHierarchy); + isInInitValueHierarchy, + isSelected); if (toStore) { this.nodeMap.set(entryId, result); @@ -260,12 +268,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)) ); } @@ -306,13 +315,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) { @@ -334,15 +344,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 @@ -357,7 +368,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); @@ -368,15 +379,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..9b8af19f1eb 100644 --- a/src/app/shared/handle.service.spec.ts +++ b/src/app/shared/handle.service.spec.ts @@ -1,8 +1,18 @@ +<<<<<<< HEAD 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'; +>>>>>>> dspace-7.6.1 describe('HandleService', () => { let service: HandleService; +<<<<<<< HEAD beforeEach(() => { service = new HandleService(); }); @@ -42,6 +52,83 @@ describe('HandleService', () => { input = 'something completely different'; output = service.normalizeHandle(input); expect(output).toBeNull(); +======= + let configurationService: ConfigurationDataServiceStub; + + beforeEach(() => { + configurationService = new ConfigurationDataServiceStub(); + + TestBed.configureTestingModule({ + providers: [ + { provide: ConfigurationDataService, useValue: configurationService }, + ], + }); + service = TestBed.inject(HandleService); + }); + + describe(`normalizeHandle`, () => { + 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/'], + })); + + service.normalizeHandle('https://hdl.handle.net/123456789/123456').subscribe((handle: string | null) => { + expect(handle).toBe('123456789/123456'); + done(); + }); + }); + + 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/'], + })); + + 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(); + }); + }); + + 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 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(); + }); + }); + + 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(); + }); + }); + + 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(); + }); +>>>>>>> dspace-7.6.1 }); }); }); diff --git a/src/app/shared/handle.service.ts b/src/app/shared/handle.service.ts index da0f17f7de3..8a012e80c23 100644 --- a/src/app/shared/handle.service.ts +++ b/src/app/shared/handle.service.ts @@ -1,7 +1,24 @@ import { Injectable } from '@angular/core'; +<<<<<<< HEAD import { isNotEmpty, isEmpty } from './empty.util'; const PREFIX_REGEX = /handle\/([^\/]+\/[^\/]+)$/; +======= +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'; + +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)\/([^\/]+\/[^\/]+)$`); +}; +>>>>>>> dspace-7.6.1 const NO_PREFIX_REGEX = /^([^\/]+\/[^\/]+)$/; @Injectable({ @@ -9,10 +26,18 @@ const NO_PREFIX_REGEX = /^([^\/]+\/[^\/]+)$/; }) export class HandleService { +<<<<<<< HEAD +======= + constructor( + protected configurationService: ConfigurationDataService, + ) { + } +>>>>>>> dspace-7.6.1 /** * Turns a handle string into the default 123456789/12345 format * +<<<<<<< HEAD * @param handle the input handle * * normalizeHandle('123456789/123456') // '123456789/123456' @@ -36,6 +61,51 @@ export class HandleService { } else { return matches[1]; } +======= + * When the handle.canonical.prefix doesn't end with handle, be sure to expose the variable so that the + * frontend can find the handle + * + * @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): 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[]; + + matches = handle.match(PREFIX_REGEX(prefix)); + + 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), + ); +>>>>>>> dspace-7.6.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 @@ -
    +>>>>>>> dspace-7.6.1 diff --git a/src/app/shared/object-collection/shared/mydspace-item-collection/item-collection.component.ts b/src/app/shared/object-collection/shared/mydspace-item-collection/item-collection.component.ts index 2f2a20ede36..f3e70a9d7a8 100644 --- a/src/app/shared/object-collection/shared/mydspace-item-collection/item-collection.component.ts +++ b/src/app/shared/object-collection/shared/mydspace-item-collection/item-collection.component.ts @@ -10,6 +10,10 @@ import { Collection } from '../../../../core/shared/collection.model'; import { getFirstCompletedRemoteData } from '../../../../core/shared/operators'; import { LinkService } from '../../../../core/cache/builders/link.service'; import { followLink } from '../../../utils/follow-link-config.model'; +<<<<<<< HEAD +======= +import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; +>>>>>>> dspace-7.6.1 /** * This component represents a badge with collection information. @@ -31,8 +35,15 @@ export class ItemCollectionComponent implements OnInit { */ collection$: Observable; +<<<<<<< HEAD public constructor(protected linkService: LinkService) { +======= + public constructor( + protected linkService: LinkService, + public dsoNameService: DSONameService, + ) { +>>>>>>> dspace-7.6.1 } /** diff --git a/src/app/shared/object-collection/shared/mydspace-item-submitter/item-submitter.component.html b/src/app/shared/object-collection/shared/mydspace-item-submitter/item-submitter.component.html index db38f98b04b..02c504d6545 100644 --- a/src/app/shared/object-collection/shared/mydspace-item-submitter/item-submitter.component.html +++ b/src/app/shared/object-collection/shared/mydspace-item-submitter/item-submitter.component.html @@ -1,3 +1,18 @@ +<<<<<<< HEAD
    {{'submission.workflow.tasks.generic.submitter' | translate}}: {{(submitter$ | async)?.name}} +======= +
    + + {{'submission.workflow.tasks.generic.submitter' | translate}}: + + + {{ dsoNameService.getName(submitter) }} + + + {{ 'submitter.empty' | translate }} + + + +>>>>>>> dspace-7.6.1
    diff --git a/src/app/shared/object-collection/shared/mydspace-item-submitter/item-submitter.component.spec.ts b/src/app/shared/object-collection/shared/mydspace-item-submitter/item-submitter.component.spec.ts index 227db9ec822..abaf029155a 100644 --- a/src/app/shared/object-collection/shared/mydspace-item-submitter/item-submitter.component.spec.ts +++ b/src/app/shared/object-collection/shared/mydspace-item-submitter/item-submitter.component.spec.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; +import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA, DebugElement } from '@angular/core'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { of as observableOf } from 'rxjs'; @@ -60,10 +60,18 @@ describe('ItemSubmitterComponent', () => { })); }); + it('should show N/A when submitter is null', () => { + component.submitter$ = observableOf(null); + fixture.detectChanges(); + + const badge: DebugElement = fixture.debugElement.query(By.css('.badge')); + + expect(badge.nativeElement.innerText).toBe('submitter.empty'); + }); + it('should show a badge with submitter name', () => { - const badge = fixture.debugElement.query(By.css('.badge')); + const badge: DebugElement = fixture.debugElement.query(By.css('.badge')); - expect(badge).toBeDefined(); - expect(badge.nativeElement.innerHTML).toBe(EPersonMock.name); + expect(badge.nativeElement.innerText).toBe(EPersonMock.name); }); }); diff --git a/src/app/shared/object-collection/shared/mydspace-item-submitter/item-submitter.component.ts b/src/app/shared/object-collection/shared/mydspace-item-submitter/item-submitter.component.ts index fb644017dbb..33778cca716 100644 --- a/src/app/shared/object-collection/shared/mydspace-item-submitter/item-submitter.component.ts +++ b/src/app/shared/object-collection/shared/mydspace-item-submitter/item-submitter.component.ts @@ -10,6 +10,10 @@ import { WorkflowItem } from '../../../../core/submission/models/workflowitem.mo import { getFirstCompletedRemoteData } from '../../../../core/shared/operators'; import { LinkService } from '../../../../core/cache/builders/link.service'; import { followLink } from '../../../utils/follow-link-config.model'; +<<<<<<< HEAD +======= +import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; +>>>>>>> dspace-7.6.1 /** * This component represents a badge with submitter information. @@ -31,8 +35,15 @@ export class ItemSubmitterComponent implements OnInit { */ submitter$: Observable; +<<<<<<< HEAD public constructor(protected linkService: LinkService) { +======= + public constructor( + public dsoNameService: DSONameService, + protected linkService: LinkService, + ) { +>>>>>>> dspace-7.6.1 } /** diff --git a/src/app/shared/object-collection/shared/object-collection-element/abstract-listable-element.component.ts b/src/app/shared/object-collection/shared/object-collection-element/abstract-listable-element.component.ts index 7d4e107b2b3..4614adcf434 100644 --- a/src/app/shared/object-collection/shared/object-collection-element/abstract-listable-element.component.ts +++ b/src/app/shared/object-collection/shared/object-collection-element/abstract-listable-element.component.ts @@ -4,6 +4,7 @@ import { CollectionElementLinkType } from '../../collection-element-link.type'; import { Context } from '../../../../core/shared/context.model'; import { ViewMode } from '../../../../core/shared/view-mode.model'; import { DSpaceObject } from '../../../../core/shared/dspace-object.model'; +import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; @Component({ selector: 'ds-abstract-object-element', @@ -41,6 +42,11 @@ export class AbstractListableElementComponent { */ @Input() showLabel = true; + /** + * Whether to show the thumbnail preview + */ + @Input() showThumbnails; + /** * The context we matched on to get this component */ @@ -70,4 +76,10 @@ export class AbstractListableElementComponent { * The available contexts */ contexts = Context; + + constructor( + public dsoNameService: DSONameService, + ) { + } + } diff --git a/src/app/shared/object-detail/my-dspace-result-detail-element/claimed-task-search-result/claimed-task-search-result-detail-element.component.html b/src/app/shared/object-detail/my-dspace-result-detail-element/claimed-task-search-result/claimed-task-search-result-detail-element.component.html index 3695f4714dc..9a80421fbf5 100644 --- a/src/app/shared/object-detail/my-dspace-result-detail-element/claimed-task-search-result/claimed-task-search-result-detail-element.component.html +++ b/src/app/shared/object-detail/my-dspace-result-detail-element/claimed-task-search-result/claimed-task-search-result-detail-element.component.html @@ -2,7 +2,7 @@ + [badgeContext]="badgeContext"> >>>>>> dspace-7.6.1 import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { of as observableOf } from 'rxjs'; @@ -7,9 +18,12 @@ import { of as observableOf } from 'rxjs'; import { Item } from '../../../../core/shared/item.model'; import { ClaimedTaskSearchResultDetailElementComponent } from './claimed-task-search-result-detail-element.component'; import { ClaimedTask } from '../../../../core/tasks/models/claimed-task-object.model'; +<<<<<<< HEAD import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type'; +======= +>>>>>>> dspace-7.6.1 import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model'; import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils'; import { ClaimedTaskSearchResult } from '../../../object-collection/shared/claimed-task-search-result.model'; @@ -18,6 +32,12 @@ import { LinkService } from '../../../../core/cache/builders/link.service'; import { getMockLinkService } from '../../../mocks/link-service.mock'; import { By } from '@angular/platform-browser'; import { ObjectCacheService } from '../../../../core/cache/object-cache.service'; +<<<<<<< HEAD +======= +import { Context } from '../../../../core/shared/context.model'; +import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; +import { DSONameServiceMock } from '../../../mocks/dso-name.service.mock'; +>>>>>>> dspace-7.6.1 let component: ClaimedTaskSearchResultDetailElementComponent; let fixture: ComponentFixture; @@ -71,6 +91,10 @@ describe('ClaimedTaskSearchResultDetailElementComponent', () => { imports: [NoopAnimationsModule], declarations: [ClaimedTaskSearchResultDetailElementComponent, VarDirective], providers: [ +<<<<<<< HEAD +======= + { provide: DSONameService, useValue: new DSONameServiceMock() }, +>>>>>>> dspace-7.6.1 { provide: LinkService, useValue: linkService }, { provide: ObjectCacheService, useValue: objectCacheServiceMock } ], @@ -101,8 +125,8 @@ describe('ClaimedTaskSearchResultDetailElementComponent', () => { expect(component.item$.value).toEqual(item); })); - it('should have properly status', () => { - expect(component.status).toEqual(MyDspaceItemStatusType.VALIDATION); + it('should have the correct badge context', () => { + expect(component.badgeContext).toEqual(Context.MyDSpaceValidation); }); it('should forward claimed-task-actions processComplete event to reloadObject event emitter', fakeAsync(() => { diff --git a/src/app/shared/object-detail/my-dspace-result-detail-element/claimed-task-search-result/claimed-task-search-result-detail-element.component.ts b/src/app/shared/object-detail/my-dspace-result-detail-element/claimed-task-search-result/claimed-task-search-result-detail-element.component.ts index 2ee661ef382..e9340121516 100644 --- a/src/app/shared/object-detail/my-dspace-result-detail-element/claimed-task-search-result/claimed-task-search-result-detail-element.component.ts +++ b/src/app/shared/object-detail/my-dspace-result-detail-element/claimed-task-search-result/claimed-task-search-result-detail-element.component.ts @@ -8,17 +8,27 @@ import { ViewMode } from '../../../../core/shared/view-mode.model'; import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model'; import { ClaimedTask } from '../../../../core/tasks/models/claimed-task-object.model'; import { SearchResultDetailElementComponent } from '../search-result-detail-element.component'; +<<<<<<< HEAD import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type'; +======= +>>>>>>> dspace-7.6.1 import { listableObjectComponent } from '../../../object-collection/shared/listable-object/listable-object.decorator'; import { ClaimedTaskSearchResult } from '../../../object-collection/shared/claimed-task-search-result.model'; import { followLink } from '../../../utils/follow-link-config.model'; import { LinkService } from '../../../../core/cache/builders/link.service'; import { Item } from '../../../../core/shared/item.model'; import { getFirstCompletedRemoteData } from '../../../../core/shared/operators'; +<<<<<<< HEAD import { isNotEmpty } from '../../../empty.util'; import { ObjectCacheService } from '../../../../core/cache/object-cache.service'; +======= +import { isNotEmpty, hasValue } from '../../../empty.util'; +import { ObjectCacheService } from '../../../../core/cache/object-cache.service'; +import { Context } from 'src/app/core/shared/context.model'; +import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; +>>>>>>> dspace-7.6.1 /** * This component renders claimed task object for the search result in the detail view. @@ -43,17 +53,26 @@ export class ClaimedTaskSearchResultDetailElementComponent extends SearchResultD public showSubmitter = true; /** - * Represent item's status + * Represents the badge context */ - public status = MyDspaceItemStatusType.VALIDATION; + public badgeContext = Context.MyDSpaceValidation; /** * The workflowitem object that belonging to the result object */ public workflowitem$: BehaviorSubject = new BehaviorSubject(null); +<<<<<<< HEAD constructor(protected linkService: LinkService, protected objectCache: ObjectCacheService) { super(); +======= + constructor( + public dsoNameService: DSONameService, + protected linkService: LinkService, + protected objectCache: ObjectCacheService, + ) { + super(dsoNameService); +>>>>>>> dspace-7.6.1 } /** @@ -88,7 +107,13 @@ export class ClaimedTaskSearchResultDetailElementComponent extends SearchResultD ngOnDestroy() { // This ensures the object is removed from cache, when action is performed on task +<<<<<<< HEAD this.objectCache.remove(this.dso._links.workflowitem.href); +======= + if (hasValue(this.dso)) { + this.objectCache.remove(this.dso._links.workflowitem.href); + } +>>>>>>> dspace-7.6.1 } } diff --git a/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component.html b/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component.html index c9c2da5c2ee..18c187112c5 100644 --- a/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component.html +++ b/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component.html @@ -1,21 +1,24 @@
    - - - +
    +<<<<<<< HEAD +======= + + +>>>>>>> dspace-7.6.1
    - +
    diff --git a/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component.ts b/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component.ts index 778e4550045..2e290bfffa5 100644 --- a/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component.ts +++ b/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component.ts @@ -6,12 +6,13 @@ import { BitstreamDataService } from '../../../../core/data/bitstream-data.servi import { Item } from '../../../../core/shared/item.model'; import { getFirstSucceededRemoteListPayload } from '../../../../core/shared/operators'; -import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type'; import { fadeInOut } from '../../../animations/fade'; import { Bitstream } from '../../../../core/shared/bitstream.model'; import { FileService } from '../../../../core/shared/file.service'; import { HALEndpointService } from '../../../../core/shared/hal-endpoint.service'; import { SearchResult } from '../../../search/models/search-result.model'; +import { Context } from '../../../../core/shared/context.model'; +import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; /** * This component show metadata for the given item object in the detail view. @@ -23,7 +24,6 @@ import { SearchResult } from '../../../search/models/search-result.model'; animations: [fadeInOut] }) export class ItemDetailPreviewComponent { - /** * The item to display */ @@ -35,9 +35,9 @@ export class ItemDetailPreviewComponent { @Input() object: SearchResult; /** - * Represent item's status + * Represents the badge context */ - @Input() status: MyDspaceItemStatusType; + @Input() badgeContext: Context; /** * A boolean representing if to show submitter information @@ -54,16 +54,12 @@ export class ItemDetailPreviewComponent { */ public separator = ', '; - /** - * Initialize instance variables - * - * @param {FileService} fileService - * @param {HALEndpointService} halService - * @param {BitstreamDataService} bitstreamDataService - */ - constructor(private fileService: FileService, - private halService: HALEndpointService, - private bitstreamDataService: BitstreamDataService) { + constructor( + protected fileService: FileService, + protected halService: HALEndpointService, + protected bitstreamDataService: BitstreamDataService, + public dsoNameService: DSONameService, + ) { } /** diff --git a/src/app/shared/object-detail/my-dspace-result-detail-element/item-search-result/item-search-result-detail-element.component.html b/src/app/shared/object-detail/my-dspace-result-detail-element/item-search-result/item-search-result-detail-element.component.html index 2b687960ab1..1c19201c634 100644 --- a/src/app/shared/object-detail/my-dspace-result-detail-element/item-search-result/item-search-result-detail-element.component.html +++ b/src/app/shared/object-detail/my-dspace-result-detail-element/item-search-result/item-search-result-detail-element.component.html @@ -1,6 +1,6 @@ + [badgeContext]="badgeContext"> diff --git a/src/app/shared/object-detail/my-dspace-result-detail-element/item-search-result/item-search-result-detail-element.component.spec.ts b/src/app/shared/object-detail/my-dspace-result-detail-element/item-search-result/item-search-result-detail-element.component.spec.ts index 8d602d5eb2d..2c8039c9239 100644 --- a/src/app/shared/object-detail/my-dspace-result-detail-element/item-search-result/item-search-result-detail-element.component.spec.ts +++ b/src/app/shared/object-detail/my-dspace-result-detail-element/item-search-result/item-search-result-detail-element.component.spec.ts @@ -6,8 +6,10 @@ import { of as observableOf } from 'rxjs'; import { Item } from '../../../../core/shared/item.model'; import { ItemSearchResultDetailElementComponent } from './item-search-result-detail-element.component'; -import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type'; import { ItemSearchResult } from '../../../object-collection/shared/item-search-result.model'; +import { Context } from 'src/app/core/shared/context.model'; +import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; +import { DSONameServiceMock } from '../../../mocks/dso-name.service.mock'; let component: ItemSearchResultDetailElementComponent; let fixture: ComponentFixture; @@ -52,6 +54,9 @@ describe('ItemSearchResultDetailElementComponent', () => { TestBed.configureTestingModule({ imports: [NoopAnimationsModule], declarations: [ItemSearchResultDetailElementComponent], + providers: [ + { provide: DSONameService, useValue: new DSONameServiceMock() }, + ], schemas: [NO_ERRORS_SCHEMA] }).overrideComponent(ItemSearchResultDetailElementComponent, { set: { changeDetection: ChangeDetectionStrategy.Default } @@ -68,7 +73,7 @@ describe('ItemSearchResultDetailElementComponent', () => { fixture.detectChanges(); }); - it('should have properly status', () => { - expect(component.status).toEqual(MyDspaceItemStatusType.ARCHIVED); + it('should have the correct badge context', () => { + expect(component.badgeContext).toEqual(Context.MyDSpaceArchived); }); }); diff --git a/src/app/shared/object-detail/my-dspace-result-detail-element/item-search-result/item-search-result-detail-element.component.ts b/src/app/shared/object-detail/my-dspace-result-detail-element/item-search-result/item-search-result-detail-element.component.ts index 27a94b0cf58..c1aa535f3f5 100644 --- a/src/app/shared/object-detail/my-dspace-result-detail-element/item-search-result/item-search-result-detail-element.component.ts +++ b/src/app/shared/object-detail/my-dspace-result-detail-element/item-search-result/item-search-result-detail-element.component.ts @@ -3,9 +3,12 @@ import { Component } from '@angular/core'; import { ViewMode } from '../../../../core/shared/view-mode.model'; import { Item } from '../../../../core/shared/item.model'; import { SearchResultDetailElementComponent } from '../search-result-detail-element.component'; +<<<<<<< HEAD import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type'; +======= +>>>>>>> dspace-7.6.1 import { listableObjectComponent } from '../../../object-collection/shared/listable-object/listable-object.decorator'; import { ItemSearchResult } from '../../../object-collection/shared/item-search-result.model'; import { Context } from '../../../../core/shared/context.model'; @@ -24,8 +27,8 @@ import { Context } from '../../../../core/shared/context.model'; export class ItemSearchResultDetailElementComponent extends SearchResultDetailElementComponent { /** - * Represent item's status + * Represents the badge context */ - public status = MyDspaceItemStatusType.ARCHIVED; + public badgeContext = Context.MyDSpaceArchived; } diff --git a/src/app/shared/object-detail/my-dspace-result-detail-element/pool-search-result/pool-search-result-detail-element.component.html b/src/app/shared/object-detail/my-dspace-result-detail-element/pool-search-result/pool-search-result-detail-element.component.html index c9165b416a7..d2a15b016f5 100644 --- a/src/app/shared/object-detail/my-dspace-result-detail-element/pool-search-result/pool-search-result-detail-element.component.html +++ b/src/app/shared/object-detail/my-dspace-result-detail-element/pool-search-result/pool-search-result-detail-element.component.html @@ -2,7 +2,7 @@ + [badgeContext]="badgeContext"> >>>>>> dspace-7.6.1 import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model'; import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils'; import { PoolSearchResultDetailElementComponent } from './pool-search-result-detail-element.component'; @@ -18,6 +21,12 @@ import { LinkService } from '../../../../core/cache/builders/link.service'; import { getMockLinkService } from '../../../mocks/link-service.mock'; import { By } from '@angular/platform-browser'; import { ObjectCacheService } from '../../../../core/cache/object-cache.service'; +<<<<<<< HEAD +======= +import { Context } from 'src/app/core/shared/context.model'; +import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; +import { DSONameServiceMock } from '../../../mocks/dso-name.service.mock'; +>>>>>>> dspace-7.6.1 let component: PoolSearchResultDetailElementComponent; let fixture: ComponentFixture; @@ -71,6 +80,7 @@ describe('PoolSearchResultDetailElementComponent', () => { imports: [NoopAnimationsModule], declarations: [PoolSearchResultDetailElementComponent, VarDirective], providers: [ + { provide: DSONameService, useValue: new DSONameServiceMock() }, { provide: 'objectElementProvider', useValue: (mockResultObject) }, { provide: 'indexElementProvider', useValue: (compIndex) }, { provide: LinkService, useValue: linkService }, @@ -103,8 +113,8 @@ describe('PoolSearchResultDetailElementComponent', () => { expect(component.item$.value).toEqual(item); })); - it('should have properly status', () => { - expect(component.status).toEqual(MyDspaceItemStatusType.WAITING_CONTROLLER); + it('should have the correct badge context', () => { + expect(component.badgeContext).toEqual(Context.MyDSpaceWaitingController); }); it('should forward pool-task-actions processCompleted event to the reloadedObject event emitter', fakeAsync(() => { diff --git a/src/app/shared/object-detail/my-dspace-result-detail-element/pool-search-result/pool-search-result-detail-element.component.ts b/src/app/shared/object-detail/my-dspace-result-detail-element/pool-search-result/pool-search-result-detail-element.component.ts index 6dec14f9cb6..5d6e91433bc 100644 --- a/src/app/shared/object-detail/my-dspace-result-detail-element/pool-search-result/pool-search-result-detail-element.component.ts +++ b/src/app/shared/object-detail/my-dspace-result-detail-element/pool-search-result/pool-search-result-detail-element.component.ts @@ -6,9 +6,12 @@ import { mergeMap, tap } from 'rxjs/operators'; import { RemoteData } from '../../../../core/data/remote-data'; import { PoolTask } from '../../../../core/tasks/models/pool-task-object.model'; import { SearchResultDetailElementComponent } from '../search-result-detail-element.component'; +<<<<<<< HEAD import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type'; +======= +>>>>>>> dspace-7.6.1 import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model'; import { ViewMode } from '../../../../core/shared/view-mode.model'; import { listableObjectComponent } from '../../../object-collection/shared/listable-object/listable-object.decorator'; @@ -17,8 +20,15 @@ import { followLink } from '../../../utils/follow-link-config.model'; import { LinkService } from '../../../../core/cache/builders/link.service'; import { Item } from '../../../../core/shared/item.model'; import { getFirstCompletedRemoteData } from '../../../../core/shared/operators'; +<<<<<<< HEAD import { isNotEmpty } from '../../../empty.util'; import { ObjectCacheService } from '../../../../core/cache/object-cache.service'; +======= +import { isNotEmpty, hasValue } from '../../../empty.util'; +import { ObjectCacheService } from '../../../../core/cache/object-cache.service'; +import { Context } from 'src/app/core/shared/context.model'; +import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; +>>>>>>> dspace-7.6.1 /** * This component renders pool task object for the search result in the detail view. @@ -43,17 +53,26 @@ export class PoolSearchResultDetailElementComponent extends SearchResultDetailEl public showSubmitter = true; /** - * Represent item's status + * Represents the badge context */ - public status = MyDspaceItemStatusType.WAITING_CONTROLLER; + public badgeContext = Context.MyDSpaceWaitingController; /** * The workflowitem object that belonging to the result object */ public workflowitem$: BehaviorSubject = new BehaviorSubject(null); +<<<<<<< HEAD constructor(protected linkService: LinkService, protected objectCache: ObjectCacheService) { super(); +======= + constructor( + public dsoNameService: DSONameService, + protected linkService: LinkService, + protected objectCache: ObjectCacheService, + ) { + super(dsoNameService); +>>>>>>> dspace-7.6.1 } /** @@ -89,7 +108,13 @@ export class PoolSearchResultDetailElementComponent extends SearchResultDetailEl ngOnDestroy() { // This ensures the object is removed from cache, when action is performed on task +<<<<<<< HEAD this.objectCache.remove(this.dso._links.workflowitem.href); +======= + if (hasValue(this.dso)) { + this.objectCache.remove(this.dso._links.workflowitem.href); + } +>>>>>>> dspace-7.6.1 } } diff --git a/src/app/shared/object-detail/my-dspace-result-detail-element/workflow-item-search-result/workflow-item-search-result-detail-element.component.html b/src/app/shared/object-detail/my-dspace-result-detail-element/workflow-item-search-result/workflow-item-search-result-detail-element.component.html index 7ff1a9bf728..ddd6af453c1 100644 --- a/src/app/shared/object-detail/my-dspace-result-detail-element/workflow-item-search-result/workflow-item-search-result-detail-element.component.html +++ b/src/app/shared/object-detail/my-dspace-result-detail-element/workflow-item-search-result/workflow-item-search-result-detail-element.component.html @@ -1,6 +1,6 @@ + [badgeContext]="badgeContext"> diff --git a/src/app/shared/object-detail/my-dspace-result-detail-element/workflow-item-search-result/workflow-item-search-result-detail-element.component.spec.ts b/src/app/shared/object-detail/my-dspace-result-detail-element/workflow-item-search-result/workflow-item-search-result-detail-element.component.spec.ts index 536bd6d0bb2..f4bc6b88faf 100644 --- a/src/app/shared/object-detail/my-dspace-result-detail-element/workflow-item-search-result/workflow-item-search-result-detail-element.component.spec.ts +++ b/src/app/shared/object-detail/my-dspace-result-detail-element/workflow-item-search-result/workflow-item-search-result-detail-element.component.spec.ts @@ -7,13 +7,13 @@ import { of as observableOf } from 'rxjs'; import { Item } from '../../../../core/shared/item.model'; import { WorkflowItemSearchResultDetailElementComponent } from './workflow-item-search-result-detail-element.component'; import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model'; -import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type'; import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils'; import { WorkflowItemSearchResult } from '../../../object-collection/shared/workflow-item-search-result.model'; import { getMockLinkService } from '../../../mocks/link-service.mock'; import { LinkService } from '../../../../core/cache/builders/link.service'; import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; import { DSONameServiceMock } from '../../../mocks/dso-name.service.mock'; +import { Context } from '../../../../core/shared/context.model'; let component: WorkflowItemSearchResultDetailElementComponent; let fixture: ComponentFixture; @@ -87,7 +87,7 @@ describe('WorkflowItemSearchResultDetailElementComponent', () => { expect(component.item).toEqual(item); }); - it('should have properly status', () => { - expect(component.status).toEqual(MyDspaceItemStatusType.WORKFLOW); + it('should have the correct badge context', () => { + expect(component.badgeContext).toEqual(Context.MyDSpaceWorkflow); }); }); diff --git a/src/app/shared/object-detail/my-dspace-result-detail-element/workflow-item-search-result/workflow-item-search-result-detail-element.component.ts b/src/app/shared/object-detail/my-dspace-result-detail-element/workflow-item-search-result/workflow-item-search-result-detail-element.component.ts index 8dc510d2aa9..bad0fcee87c 100644 --- a/src/app/shared/object-detail/my-dspace-result-detail-element/workflow-item-search-result/workflow-item-search-result-detail-element.component.ts +++ b/src/app/shared/object-detail/my-dspace-result-detail-element/workflow-item-search-result/workflow-item-search-result-detail-element.component.ts @@ -4,7 +4,6 @@ import { ViewMode } from '../../../../core/shared/view-mode.model'; import { Item } from '../../../../core/shared/item.model'; import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model'; import { SearchResultDetailElementComponent } from '../search-result-detail-element.component'; -import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type'; import { Observable } from 'rxjs'; import { RemoteData } from '../../../../core/data/remote-data'; import { find } from 'rxjs/operators'; @@ -13,6 +12,8 @@ import { listableObjectComponent } from '../../../object-collection/shared/lista import { WorkflowItemSearchResult } from '../../../object-collection/shared/workflow-item-search-result.model'; import { LinkService } from '../../../../core/cache/builders/link.service'; import { followLink } from '../../../utils/follow-link-config.model'; +import { Context } from 'src/app/core/shared/context.model'; +import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; /** * This component renders workflowitem object for the search result in the detail view. @@ -32,14 +33,15 @@ export class WorkflowItemSearchResultDetailElementComponent extends SearchResult public item: Item; /** - * Represent item's status + * Represents the badge context */ - public status = MyDspaceItemStatusType.WORKFLOW; + public badgeContext = Context.MyDSpaceWorkflow; constructor( + public dsoNameService: DSONameService, protected linkService: LinkService ) { - super(); + super(dsoNameService); } /** diff --git a/src/app/shared/object-detail/my-dspace-result-detail-element/workspace-item-search-result/workspace-item-search-result-detail-element.component.html b/src/app/shared/object-detail/my-dspace-result-detail-element/workspace-item-search-result/workspace-item-search-result-detail-element.component.html index 8d4eee6ba92..4414933e02c 100644 --- a/src/app/shared/object-detail/my-dspace-result-detail-element/workspace-item-search-result/workspace-item-search-result-detail-element.component.html +++ b/src/app/shared/object-detail/my-dspace-result-detail-element/workspace-item-search-result/workspace-item-search-result-detail-element.component.html @@ -1,5 +1,5 @@ + [badgeContext]="badgeContext"> diff --git a/src/app/shared/object-detail/my-dspace-result-detail-element/workspace-item-search-result/workspace-item-search-result-detail-element.component.spec.ts b/src/app/shared/object-detail/my-dspace-result-detail-element/workspace-item-search-result/workspace-item-search-result-detail-element.component.spec.ts index 00a20b006a3..e86bfdb5e60 100644 --- a/src/app/shared/object-detail/my-dspace-result-detail-element/workspace-item-search-result/workspace-item-search-result-detail-element.component.spec.ts +++ b/src/app/shared/object-detail/my-dspace-result-detail-element/workspace-item-search-result/workspace-item-search-result-detail-element.component.spec.ts @@ -7,13 +7,13 @@ import { of as observableOf } from 'rxjs'; import { Item } from '../../../../core/shared/item.model'; import { WorkspaceItemSearchResultDetailElementComponent } from './workspace-item-search-result-detail-element.component'; import { WorkspaceItem } from '../../../../core/submission/models/workspaceitem.model'; -import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type'; import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils'; import { WorkflowItemSearchResult } from '../../../object-collection/shared/workflow-item-search-result.model'; import { getMockLinkService } from '../../../mocks/link-service.mock'; import { LinkService } from '../../../../core/cache/builders/link.service'; import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; import { DSONameServiceMock } from '../../../mocks/dso-name.service.mock'; +import { Context } from 'src/app/core/shared/context.model'; let component: WorkspaceItemSearchResultDetailElementComponent; let fixture: ComponentFixture; @@ -87,7 +87,7 @@ describe('WorkspaceItemSearchResultDetailElementComponent', () => { expect(component.item).toEqual(item); }); - it('should have properly status', () => { - expect(component.status).toEqual(MyDspaceItemStatusType.WORKSPACE); + it('should have the correct badge context', () => { + expect(component.badgeContext).toEqual(Context.MyDSpaceWorkspace); }); }); diff --git a/src/app/shared/object-detail/my-dspace-result-detail-element/workspace-item-search-result/workspace-item-search-result-detail-element.component.ts b/src/app/shared/object-detail/my-dspace-result-detail-element/workspace-item-search-result/workspace-item-search-result-detail-element.component.ts index e4781fe0b29..03b41e795c8 100644 --- a/src/app/shared/object-detail/my-dspace-result-detail-element/workspace-item-search-result/workspace-item-search-result-detail-element.component.ts +++ b/src/app/shared/object-detail/my-dspace-result-detail-element/workspace-item-search-result/workspace-item-search-result-detail-element.component.ts @@ -8,12 +8,13 @@ import { Item } from '../../../../core/shared/item.model'; import { RemoteData } from '../../../../core/data/remote-data'; import { isNotUndefined } from '../../../empty.util'; import { SearchResultDetailElementComponent } from '../search-result-detail-element.component'; -import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type'; import { ViewMode } from '../../../../core/shared/view-mode.model'; import { listableObjectComponent } from '../../../object-collection/shared/listable-object/listable-object.decorator'; import { WorkspaceItemSearchResult } from '../../../object-collection/shared/workspace-item-search-result.model'; import { followLink } from '../../../utils/follow-link-config.model'; import { LinkService } from '../../../../core/cache/builders/link.service'; +import { Context } from '../../../../core/shared/context.model'; +import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; /** * This component renders workspace item object for the search result in the detail view. @@ -33,14 +34,15 @@ export class WorkspaceItemSearchResultDetailElementComponent extends SearchResul public item: Item; /** - * Represent item's status + * Represents the badge context */ - status = MyDspaceItemStatusType.WORKSPACE; + public badgeContext = Context.MyDSpaceWorkspace; constructor( + public dsoNameService: DSONameService, protected linkService: LinkService ) { - super(); + super(dsoNameService); } /** diff --git a/src/app/shared/object-detail/object-detail.component.html b/src/app/shared/object-detail/object-detail.component.html index 05b8342ca3a..2ba44ae84a5 100644 --- a/src/app/shared/object-detail/object-detail.component.html +++ b/src/app/shared/object-detail/object-detail.component.html @@ -21,6 +21,10 @@ >>>>>> dspace-7.6.1 (contentChange)="contentChange.emit($event)">
    diff --git a/src/app/shared/object-detail/object-detail.component.ts b/src/app/shared/object-detail/object-detail.component.ts index 15bd5b7bca0..2c591a65e0d 100644 --- a/src/app/shared/object-detail/object-detail.component.ts +++ b/src/app/shared/object-detail/object-detail.component.ts @@ -65,6 +65,14 @@ export class ObjectDetailComponent { @Input() showPaginator = true; /** +<<<<<<< HEAD +======= + * Whether to show the thumbnail preview + */ + @Input() showThumbnails; + + /** +>>>>>>> dspace-7.6.1 * Emit when one of the listed object has changed. */ @Output() contentChange = new EventEmitter(); diff --git a/src/app/shared/object-grid/collection-grid-element/collection-grid-element.component.html b/src/app/shared/object-grid/collection-grid-element/collection-grid-element.component.html index 4d6d5cb1c34..542ad117d49 100644 --- a/src/app/shared/object-grid/collection-grid-element/collection-grid-element.component.html +++ b/src/app/shared/object-grid/collection-grid-element/collection-grid-element.component.html @@ -1,17 +1,27 @@
    +<<<<<<< HEAD +======= + + + +>>>>>>> dspace-7.6.1 - - + +
    -

    {{object.name}}

    +

    {{ dsoNameService.getName(object) }}

    {{object.shortDescription}}

    diff --git a/src/app/shared/object-grid/collection-grid-element/collection-grid-element.component.ts b/src/app/shared/object-grid/collection-grid-element/collection-grid-element.component.ts index b6a7cc056b7..e2ecf7b1aee 100644 --- a/src/app/shared/object-grid/collection-grid-element/collection-grid-element.component.ts +++ b/src/app/shared/object-grid/collection-grid-element/collection-grid-element.component.ts @@ -6,6 +6,7 @@ import { listableObjectComponent } from '../../object-collection/shared/listable import { hasNoValue, hasValue } from '../../empty.util'; import { followLink } from '../../utils/follow-link-config.model'; import { LinkService } from '../../../core/cache/builders/link.service'; +import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; /** * Component representing a grid element for collection @@ -21,8 +22,11 @@ export class CollectionGridElementComponent extends AbstractListableElementCompo > { private _object: Collection; - constructor(private linkService: LinkService) { - super(); + constructor( + public dsoNameService: DSONameService, + private linkService: LinkService, + ) { + super(dsoNameService); } // @ts-ignore diff --git a/src/app/shared/object-grid/community-grid-element/community-grid-element.component.html b/src/app/shared/object-grid/community-grid-element/community-grid-element.component.html index c9833b8829b..564e34014e7 100644 --- a/src/app/shared/object-grid/community-grid-element/community-grid-element.component.html +++ b/src/app/shared/object-grid/community-grid-element/community-grid-element.component.html @@ -1,17 +1,27 @@
    +<<<<<<< HEAD +======= + + + +>>>>>>> dspace-7.6.1 - - + +
    -

    {{object.name}}

    +

    {{ dsoNameService.getName(object) }}

    {{object.shortDescription}}

    diff --git a/src/app/shared/object-grid/community-grid-element/community-grid-element.component.ts b/src/app/shared/object-grid/community-grid-element/community-grid-element.component.ts index 001c3e3080d..59ac0693eb7 100644 --- a/src/app/shared/object-grid/community-grid-element/community-grid-element.component.ts +++ b/src/app/shared/object-grid/community-grid-element/community-grid-element.component.ts @@ -6,6 +6,7 @@ import { listableObjectComponent } from '../../object-collection/shared/listable import { followLink } from '../../utils/follow-link-config.model'; import { LinkService } from '../../../core/cache/builders/link.service'; import { hasNoValue, hasValue } from '../../empty.util'; +import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; /** * Component representing a grid element for a community @@ -20,8 +21,11 @@ import { hasNoValue, hasValue } from '../../empty.util'; export class CommunityGridElementComponent extends AbstractListableElementComponent { private _object: Community; - constructor( private linkService: LinkService) { - super(); + constructor( + public dsoNameService: DSONameService, + private linkService: LinkService, + ) { + super(dsoNameService); } // @ts-ignore diff --git a/src/app/shared/object-grid/item-grid-element/item-types/item/item-grid-element.component.spec.ts b/src/app/shared/object-grid/item-grid-element/item-types/item/item-grid-element.component.spec.ts index 645897ebbf9..b137c2e7083 100644 --- a/src/app/shared/object-grid/item-grid-element/item-types/item/item-grid-element.component.spec.ts +++ b/src/app/shared/object-grid/item-grid-element/item-types/item/item-grid-element.component.spec.ts @@ -10,6 +10,8 @@ import { Item } from '../../../../../core/shared/item.model'; import { createSuccessfulRemoteDataObject$ } from '../../../../remote-data.utils'; import { buildPaginatedList } from '../../../../../core/data/paginated-list.model'; import { PageInfo } from '../../../../../core/shared/page-info.model'; +import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service'; +import { DSONameServiceMock } from '../../../../mocks/dso-name.service.mock'; const mockItem = Object.assign(new Item(), { bundles: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])), @@ -54,6 +56,7 @@ describe('ItemGridElementComponent', () => { imports: [NoopAnimationsModule], declarations: [ItemGridElementComponent, TruncatePipe], providers: [ + { provide: DSONameService, useValue: new DSONameServiceMock() }, { provide: TruncatableService, useValue: truncatableServiceStub }, ], schemas: [NO_ERRORS_SCHEMA] diff --git a/src/app/shared/object-grid/object-grid.component.html b/src/app/shared/object-grid/object-grid.component.html index 4050f93f770..957416ac959 100644 --- a/src/app/shared/object-grid/object-grid.component.html +++ b/src/app/shared/object-grid/object-grid.component.html @@ -19,7 +19,15 @@
    +<<<<<<< HEAD +======= + +>>>>>>> dspace-7.6.1
    diff --git a/src/app/shared/object-grid/object-grid.component.ts b/src/app/shared/object-grid/object-grid.component.ts index 1b5ab075e26..f015e649d81 100644 --- a/src/app/shared/object-grid/object-grid.component.ts +++ b/src/app/shared/object-grid/object-grid.component.ts @@ -1,11 +1,12 @@ -import { combineLatest as observableCombineLatest, BehaviorSubject, Observable } from 'rxjs'; +import { BehaviorSubject, combineLatest as observableCombineLatest, Observable } from 'rxjs'; -import { startWith, distinctUntilChanged, map } from 'rxjs/operators'; +import { distinctUntilChanged, map, startWith } from 'rxjs/operators'; import { ChangeDetectionStrategy, Component, EventEmitter, - Input, OnInit, + Input, + OnInit, Output, ViewEncapsulation } from '@angular/core'; @@ -55,6 +56,14 @@ export class ObjectGridComponent implements OnInit { @Input() showPaginator = true; /** +<<<<<<< HEAD +======= + * Whether to show the thumbnail preview + */ + @Input() showThumbnails; + + /** +>>>>>>> dspace-7.6.1 * The whether or not the gear is hidden */ @Input() hideGear = false; diff --git a/src/app/shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component.html b/src/app/shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component.html index b05bb4f7ba7..23ecedd21a3 100644 --- a/src/app/shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component.html +++ b/src/app/shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component.html @@ -1,18 +1,28 @@
    +<<<<<<< HEAD +======= + + + +>>>>>>> dspace-7.6.1 - - + +
    - -

    {{dso.name}}

    + +

    {{ dsoNameService.getName(dso) }}

    {{dso.shortDescription}}

    diff --git a/src/app/shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component.ts b/src/app/shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component.ts index 9f715b0e794..e4ea2e48f54 100644 --- a/src/app/shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component.ts +++ b/src/app/shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component.ts @@ -9,6 +9,7 @@ import { followLink } from '../../../utils/follow-link-config.model'; import { LinkService } from '../../../../core/cache/builders/link.service'; import { TruncatableService } from '../../../truncatable/truncatable.service'; import { BitstreamDataService } from '../../../../core/data/bitstream-data.service'; +import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; @Component({ selector: 'ds-collection-search-result-grid-element', @@ -23,11 +24,12 @@ export class CollectionSearchResultGridElementComponent extends SearchResultGrid private _dso: Collection; constructor( + public dsoNameService: DSONameService, private linkService: LinkService, protected truncatableService: TruncatableService, protected bitstreamDataService: BitstreamDataService ) { - super(truncatableService, bitstreamDataService); + super(dsoNameService, truncatableService, bitstreamDataService); } // @ts-ignore diff --git a/src/app/shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component.html b/src/app/shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component.html index d915cdb7a42..fdd0102faa9 100644 --- a/src/app/shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component.html +++ b/src/app/shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component.html @@ -1,18 +1,28 @@
    +<<<<<<< HEAD +======= + + + +>>>>>>> dspace-7.6.1 - - + +
    - -

    {{dso.name}}

    + +

    {{ dsoNameService.getName(dso) }}

    {{dso.shortDescription}}

    diff --git a/src/app/shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component.ts b/src/app/shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component.ts index 18994e2cd75..039b01c5676 100644 --- a/src/app/shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component.ts +++ b/src/app/shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component.ts @@ -9,6 +9,7 @@ import { TruncatableService } from '../../../truncatable/truncatable.service'; import { BitstreamDataService } from '../../../../core/data/bitstream-data.service'; import { hasNoValue, hasValue } from '../../../empty.util'; import { followLink } from '../../../utils/follow-link-config.model'; +import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; @Component({ selector: 'ds-community-search-result-grid-element', @@ -26,11 +27,12 @@ export class CommunitySearchResultGridElementComponent extends SearchResultGridE private _dso: Community; constructor( + public dsoNameService: DSONameService, private linkService: LinkService, protected truncatableService: TruncatableService, protected bitstreamDataService: BitstreamDataService ) { - super(truncatableService, bitstreamDataService); + super(dsoNameService, truncatableService, bitstreamDataService); } // @ts-ignore diff --git a/src/app/shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component.html b/src/app/shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component.html index c7e7e2d0566..f4cffab249a 100644 --- a/src/app/shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component.html +++ b/src/app/shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component.html @@ -2,15 +2,24 @@
    +<<<<<<< HEAD
    +======= + +
    + + +>>>>>>> dspace-7.6.1
    +<<<<<<< HEAD
    @@ -18,6 +27,14 @@
    +======= + + +
    +
    + diff --git a/src/app/shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component.ts b/src/app/shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component.ts index 303e4681a28..a18f8fe9a06 100644 --- a/src/app/shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component.ts +++ b/src/app/shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component.ts @@ -32,11 +32,19 @@ export class ItemSearchResultGridElementComponent extends SearchResultGridElemen dsoTitle: string; constructor( +<<<<<<< HEAD protected truncatableService: TruncatableService, protected bitstreamDataService: BitstreamDataService, private dsoNameService: DSONameService, ) { super(truncatableService, bitstreamDataService); +======= + public dsoNameService: DSONameService, + protected truncatableService: TruncatableService, + protected bitstreamDataService: BitstreamDataService, + ) { + super(dsoNameService, truncatableService, bitstreamDataService); +>>>>>>> dspace-7.6.1 } ngOnInit(): void { diff --git a/src/app/shared/object-grid/search-result-grid-element/search-result-grid-element.component.ts b/src/app/shared/object-grid/search-result-grid-element/search-result-grid-element.component.ts index 6e72eaa9424..4c3431bb55d 100644 --- a/src/app/shared/object-grid/search-result-grid-element/search-result-grid-element.component.ts +++ b/src/app/shared/object-grid/search-result-grid-element/search-result-grid-element.component.ts @@ -8,6 +8,7 @@ import { Metadata } from '../../../core/shared/metadata.utils'; import { hasValue } from '../../empty.util'; import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component'; import { TruncatableService } from '../../truncatable/truncatable.service'; +import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; @Component({ selector: 'ds-search-result-grid-element', @@ -25,10 +26,11 @@ export class SearchResultGridElementComponent, K exten isCollapsed$: Observable; public constructor( + public dsoNameService: DSONameService, protected truncatableService: TruncatableService, protected bitstreamDataService: BitstreamDataService ) { - super(); + super(dsoNameService); } /** diff --git a/src/app/shared/object-list/bitstream-list-item/bitstream-list-item.component.html b/src/app/shared/object-list/bitstream-list-item/bitstream-list-item.component.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/app/shared/object-list/bitstream-list-item/bitstream-list-item.component.scss b/src/app/shared/object-list/bitstream-list-item/bitstream-list-item.component.scss new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/app/shared/object-list/bitstream-list-item/bitstream-list-item.component.spec.ts b/src/app/shared/object-list/bitstream-list-item/bitstream-list-item.component.spec.ts new file mode 100644 index 00000000000..906add6578d --- /dev/null +++ b/src/app/shared/object-list/bitstream-list-item/bitstream-list-item.component.spec.ts @@ -0,0 +1,35 @@ +import {CommonModule} from '@angular/common'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {RouterTestingModule} from '@angular/router/testing'; +import {TranslateModule} from '@ngx-translate/core'; +import {SharedModule} from '../../shared.module'; + +import {BitstreamListItemComponent} from './bitstream-list-item.component'; +import {DSONameService} from '../../../core/breadcrumbs/dso-name.service'; +import {DSONameServiceMock} from '../../mocks/dso-name.service.mock'; + +describe('BitstreamListItemComponent', () => { + let component: BitstreamListItemComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ BitstreamListItemComponent ], + imports: [ CommonModule, SharedModule, TranslateModule, RouterTestingModule ], + providers: [{ provide: DSONameService, useValue: new DSONameServiceMock() }] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(BitstreamListItemComponent); + component = fixture.componentInstance; + // @ts-ignore + component.object = {name: 'test'}; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/object-list/bitstream-list-item/bitstream-list-item.component.ts b/src/app/shared/object-list/bitstream-list-item/bitstream-list-item.component.ts new file mode 100644 index 00000000000..f281c0bd77f --- /dev/null +++ b/src/app/shared/object-list/bitstream-list-item/bitstream-list-item.component.ts @@ -0,0 +1,17 @@ +import { Component } from '@angular/core'; +import { listableObjectComponent } from '../../object-collection/shared/listable-object/listable-object.decorator'; +import { ViewMode } from '../../../core/shared/view-mode.model'; +import { + AbstractListableElementComponent +} from '../../object-collection/shared/object-collection-element/abstract-listable-element.component'; +import { Bitstream } from '../../../core/shared/bitstream.model'; +import { Context } from '../../../core/shared/context.model'; + + +@listableObjectComponent(Bitstream, ViewMode.ListElement, Context.Bitstream) +@Component({ + selector: 'ds-bitstream-list-item', + template: ` {{object.name}} `, + styleUrls: ['./bitstream-list-item.component.scss'] +}) +export class BitstreamListItemComponent extends AbstractListableElementComponent{} diff --git a/src/app/shared/object-list/browse-entry-list-element/browse-entry-list-element.component.html b/src/app/shared/object-list/browse-entry-list-element/browse-entry-list-element.component.html index dcbdd77bffa..5aa67ccfeaf 100644 --- a/src/app/shared/object-list/browse-entry-list-element/browse-entry-list-element.component.html +++ b/src/app/shared/object-list/browse-entry-list-element/browse-entry-list-element.component.html @@ -1,5 +1,9 @@
    +<<<<<<< HEAD +======= + +>>>>>>> dspace-7.6.1 {{object.value}} diff --git a/src/app/shared/object-list/browse-entry-list-element/browse-entry-list-element.component.spec.ts b/src/app/shared/object-list/browse-entry-list-element/browse-entry-list-element.component.spec.ts index a4490bd9519..130d57295ca 100644 --- a/src/app/shared/object-list/browse-entry-list-element/browse-entry-list-element.component.spec.ts +++ b/src/app/shared/object-list/browse-entry-list-element/browse-entry-list-element.component.spec.ts @@ -7,6 +7,11 @@ import { BrowseEntry } from '../../../core/shared/browse-entry.model'; import { PaginationService } from '../../../core/pagination/pagination.service'; import { RouteService } from '../../../core/services/route.service'; import { of as observableOf } from 'rxjs'; +<<<<<<< HEAD +======= +import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; +import { DSONameServiceMock } from '../../mocks/dso-name.service.mock'; +>>>>>>> dspace-7.6.1 let browseEntryListElementComponent: BrowseEntryListElementComponent; let fixture: ComponentFixture; @@ -34,6 +39,10 @@ describe('BrowseEntryListElementComponent', () => { TestBed.configureTestingModule({ declarations: [BrowseEntryListElementComponent, TruncatePipe], providers: [ +<<<<<<< HEAD +======= + { provide: DSONameService, useValue: new DSONameServiceMock() }, +>>>>>>> dspace-7.6.1 { provide: 'objectElementProvider', useValue: { mockValue } }, {provide: PaginationService, useValue: paginationService}, {provide: RouteService, useValue: routeService}, diff --git a/src/app/shared/object-list/browse-entry-list-element/browse-entry-list-element.component.ts b/src/app/shared/object-list/browse-entry-list-element/browse-entry-list-element.component.ts index 667da726ed8..d9bf33ed47d 100644 --- a/src/app/shared/object-list/browse-entry-list-element/browse-entry-list-element.component.ts +++ b/src/app/shared/object-list/browse-entry-list-element/browse-entry-list-element.component.ts @@ -10,6 +10,10 @@ import { BBM_PAGINATION_ID } from '../../../browse-by/browse-by-metadata-page/br import { RouteService } from 'src/app/core/services/route.service'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; +<<<<<<< HEAD +======= +import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; +>>>>>>> dspace-7.6.1 @Component({ selector: 'ds-browse-entry-list-element', @@ -27,8 +31,17 @@ export class BrowseEntryListElementComponent extends AbstractListableElementComp */ queryParams$: Observable; +<<<<<<< HEAD constructor(private paginationService: PaginationService, private routeService: RouteService) { super(); +======= + constructor( + public dsoNameService: DSONameService, + protected paginationService: PaginationService, + protected routeService: RouteService, + ) { + super(dsoNameService); +>>>>>>> dspace-7.6.1 } ngOnInit() { diff --git a/src/app/shared/object-list/bundle-list-element/bundle-list-element.component.html b/src/app/shared/object-list/bundle-list-element/bundle-list-element.component.html index dfe08144a83..77ed1e3b212 100644 --- a/src/app/shared/object-list/bundle-list-element/bundle-list-element.component.html +++ b/src/app/shared/object-list/bundle-list-element/bundle-list-element.component.html @@ -1 +1 @@ -
    {{object.name}}
    +
    {{ dsoNameService.getName(object) }}
    diff --git a/src/app/shared/object-list/collection-list-element/collection-list-element.component.html b/src/app/shared/object-list/collection-list-element/collection-list-element.component.html index c61adf5dad5..c50b382495c 100644 --- a/src/app/shared/object-list/collection-list-element/collection-list-element.component.html +++ b/src/app/shared/object-list/collection-list-element/collection-list-element.component.html @@ -1,9 +1,13 @@ - - {{object.name}} - - - {{object.name}} - +
    + + {{ dsoNameService.getName(object) }} + + + {{ dsoNameService.getName(object) }} + +   + {{object.archivedItemsCount}} +
    {{object.shortDescription}}
    diff --git a/src/app/shared/object-list/collection-list-element/collection-list-element.component.spec.ts b/src/app/shared/object-list/collection-list-element/collection-list-element.component.spec.ts index c41d5a73142..97f9eb88c2c 100644 --- a/src/app/shared/object-list/collection-list-element/collection-list-element.component.spec.ts +++ b/src/app/shared/object-list/collection-list-element/collection-list-element.component.spec.ts @@ -3,10 +3,35 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; import { By } from '@angular/platform-browser'; import { Collection } from '../../../core/shared/collection.model'; +import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; +import { DSONameServiceMock } from '../../mocks/dso-name.service.mock'; let collectionListElementComponent: CollectionListElementComponent; let fixture: ComponentFixture; +const mockCollectionWithArchivedItems: Collection = Object.assign(new Collection(), { + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'Test title' + } + ] + }, archivedItemsCount: 1 +}); + +const mockCollectionWithArchivedItemsDisabledAtBackend: Collection = Object.assign(new Collection(), { + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'Test title' + } + ] + }, archivedItemsCount: -1 +}); + + const mockCollectionWithAbstract: Collection = Object.assign(new Collection(), { metadata: { 'dc.description.abstract': [ @@ -15,7 +40,7 @@ const mockCollectionWithAbstract: Collection = Object.assign(new Collection(), { value: 'Short description' } ] - } + }, archivedItemsCount: 1 }); const mockCollectionWithoutAbstract: Collection = Object.assign(new Collection(), { @@ -26,7 +51,7 @@ const mockCollectionWithoutAbstract: Collection = Object.assign(new Collection() value: 'Test title' } ] - } + }, archivedItemsCount: 1 }); describe('CollectionListElementComponent', () => { @@ -34,6 +59,7 @@ describe('CollectionListElementComponent', () => { TestBed.configureTestingModule({ declarations: [CollectionListElementComponent], providers: [ + { provide: DSONameService, useValue: new DSONameServiceMock() }, { provide: 'objectElementProvider', useValue: (mockCollectionWithAbstract) } ], @@ -71,4 +97,29 @@ describe('CollectionListElementComponent', () => { expect(collectionAbstractField).toBeNull(); }); }); + + + describe('When the collection has archived items', () => { + beforeEach(() => { + collectionListElementComponent.object = mockCollectionWithArchivedItems; + fixture.detectChanges(); + }); + + it('should show the archived items paragraph', () => { + const field = fixture.debugElement.query(By.css('span.archived-items-lead')); + expect(field).not.toBeNull(); + }); + }); + + describe('When the collection archived items are disabled at backend', () => { + beforeEach(() => { + collectionListElementComponent.object = mockCollectionWithArchivedItemsDisabledAtBackend; + fixture.detectChanges(); + }); + + it('should not show the archived items paragraph', () => { + const field = fixture.debugElement.query(By.css('span.archived-items-lead')); + expect(field).toBeNull(); + }); + }); }); diff --git a/src/app/shared/object-list/community-list-element/community-list-element.component.html b/src/app/shared/object-list/community-list-element/community-list-element.component.html index af01999ca7c..2101261bdc2 100644 --- a/src/app/shared/object-list/community-list-element/community-list-element.component.html +++ b/src/app/shared/object-list/community-list-element/community-list-element.component.html @@ -1,9 +1,13 @@ - - {{object.name}} - - - {{object.name}} - +
    + + {{ dsoNameService.getName(object) }} + + + {{ dsoNameService.getName(object) }} + +   + {{object.archivedItemsCount}} +
    {{object.shortDescription}}
    diff --git a/src/app/shared/object-list/community-list-element/community-list-element.component.spec.ts b/src/app/shared/object-list/community-list-element/community-list-element.component.spec.ts index cd212e03990..8f7350d860a 100644 --- a/src/app/shared/object-list/community-list-element/community-list-element.component.spec.ts +++ b/src/app/shared/object-list/community-list-element/community-list-element.component.spec.ts @@ -3,6 +3,8 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; import { By } from '@angular/platform-browser'; import { Community } from '../../../core/shared/community.model'; +import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; +import { DSONameServiceMock } from '../../mocks/dso-name.service.mock'; let communityListElementComponent: CommunityListElementComponent; let fixture: ComponentFixture; @@ -34,6 +36,7 @@ describe('CommunityListElementComponent', () => { TestBed.configureTestingModule({ declarations: [CommunityListElementComponent], providers: [ + { provide: DSONameService, useValue: new DSONameServiceMock() }, { provide: 'objectElementProvider', useValue: (mockCommunityWithAbstract) } ], diff --git a/src/app/shared/object-list/community-list-element/community-list-element.component.ts b/src/app/shared/object-list/community-list-element/community-list-element.component.ts index b41cd781078..4a83fd28345 100644 --- a/src/app/shared/object-list/community-list-element/community-list-element.component.ts +++ b/src/app/shared/object-list/community-list-element/community-list-element.component.ts @@ -4,6 +4,7 @@ import { Community } from '../../../core/shared/community.model'; import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component'; import { ViewMode } from '../../../core/shared/view-mode.model'; import { listableObjectComponent } from '../../object-collection/shared/listable-object/listable-object.decorator'; +import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; @Component({ selector: 'ds-community-list-element', @@ -14,4 +15,12 @@ import { listableObjectComponent } from '../../object-collection/shared/listable * Component representing a list element for a community */ @listableObjectComponent(Community, ViewMode.ListElement) -export class CommunityListElementComponent extends AbstractListableElementComponent {} +export class CommunityListElementComponent extends AbstractListableElementComponent { + + constructor( + public dsoNameService: DSONameService, + ) { + super(dsoNameService); + } + +} diff --git a/src/app/shared/object-list/item-list-element/item-types/item/item-list-element.component.spec.ts b/src/app/shared/object-list/item-list-element/item-types/item/item-list-element.component.spec.ts index de30c232163..d75576e8ebc 100644 --- a/src/app/shared/object-list/item-list-element/item-types/item/item-list-element.component.spec.ts +++ b/src/app/shared/object-list/item-list-element/item-types/item/item-list-element.component.spec.ts @@ -6,6 +6,8 @@ import { Item } from '../../../../../core/shared/item.model'; import { TruncatePipe } from '../../../../utils/truncate.pipe'; import { TruncatableService } from '../../../../truncatable/truncatable.service'; import { of as observableOf } from 'rxjs'; +import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service'; +import { DSONameServiceMock } from '../../../../mocks/dso-name.service.mock'; const mockItem: Item = Object.assign(new Item(), { bundles: observableOf({}), @@ -55,6 +57,7 @@ describe('ItemListElementComponent', () => { TestBed.configureTestingModule({ declarations: [ItemListElementComponent, TruncatePipe], providers: [ + { provide: DSONameService, useValue: new DSONameServiceMock() }, { provide: TruncatableService, useValue: truncatableServiceStub }, ], schemas: [NO_ERRORS_SCHEMA] diff --git a/src/app/shared/object-list/metadata-representation-list-element/browse-link/browse-link-metadata-list-element.component.html b/src/app/shared/object-list/metadata-representation-list-element/browse-link/browse-link-metadata-list-element.component.html index 8d3afea273d..eed267c6a8f 100644 --- a/src/app/shared/object-list/metadata-representation-list-element/browse-link/browse-link-metadata-list-element.component.html +++ b/src/app/shared/object-list/metadata-representation-list-element/browse-link/browse-link-metadata-list-element.component.html @@ -1,9 +1,17 @@ diff --git a/src/app/shared/object-list/metadata-representation-list-element/browse-link/browse-link-metadata-list-element.component.spec.ts b/src/app/shared/object-list/metadata-representation-list-element/browse-link/browse-link-metadata-list-element.component.spec.ts index 32919d97581..b4e2d9e3a6f 100644 --- a/src/app/shared/object-list/metadata-representation-list-element/browse-link/browse-link-metadata-list-element.component.spec.ts +++ b/src/app/shared/object-list/metadata-representation-list-element/browse-link/browse-link-metadata-list-element.component.spec.ts @@ -30,7 +30,11 @@ describe('BrowseLinkMetadataListElementComponent', () => { beforeEach(waitForAsync(() => { fixture = TestBed.createComponent(BrowseLinkMetadataListElementComponent); comp = fixture.componentInstance; +<<<<<<< HEAD comp.metadataRepresentation = mockMetadataRepresentation; +======= + comp.mdRepresentation = mockMetadataRepresentation; +>>>>>>> dspace-7.6.1 fixture.detectChanges(); })); @@ -46,7 +50,11 @@ describe('BrowseLinkMetadataListElementComponent', () => { beforeEach(waitForAsync(() => { fixture = TestBed.createComponent(BrowseLinkMetadataListElementComponent); comp = fixture.componentInstance; +<<<<<<< HEAD comp.metadataRepresentation = mockMetadataRepresentationWithUrl; +======= + comp.mdRepresentation = mockMetadataRepresentationWithUrl; +>>>>>>> dspace-7.6.1 fixture.detectChanges(); })); diff --git a/src/app/shared/object-list/metadata-representation-list-element/browse-link/browse-link-metadata-list-element.component.ts b/src/app/shared/object-list/metadata-representation-list-element/browse-link/browse-link-metadata-list-element.component.ts index 0eb0ce05b02..0cddaa06708 100644 --- a/src/app/shared/object-list/metadata-representation-list-element/browse-link/browse-link-metadata-list-element.component.ts +++ b/src/app/shared/object-list/metadata-representation-list-element/browse-link/browse-link-metadata-list-element.component.ts @@ -2,6 +2,10 @@ import { MetadataRepresentationType } from '../../../../core/shared/metadata-rep import { Component } from '@angular/core'; import { MetadataRepresentationListElementComponent } from '../metadata-representation-list-element.component'; import { metadataRepresentationComponent } from '../../../metadata-representation/metadata-representation.decorator'; +<<<<<<< HEAD +======= +import { VALUE_LIST_BROWSE_DEFINITION } from '../../../../core/shared/value-list-browse-definition.resource-type'; +>>>>>>> dspace-7.6.1 //@metadataRepresentationComponent('Publication', MetadataRepresentationType.PlainText) // For now, authority controlled fields are rendered the same way as plain text fields //@metadataRepresentationComponent('Publication', MetadataRepresentationType.AuthorityControlled) @@ -20,9 +24,15 @@ export class BrowseLinkMetadataListElementComponent extends MetadataRepresentati * expects 'startsWith' (eg browse by date) or 'value' (eg browse by title) */ getQueryParams() { +<<<<<<< HEAD let queryParams = {startsWith: this.metadataRepresentation.getValue()}; if (this.metadataRepresentation.browseDefinition.metadataBrowse) { return {value: this.metadataRepresentation.getValue()}; +======= + let queryParams = {startsWith: this.mdRepresentation.getValue()}; + if (this.mdRepresentation.browseDefinition.getRenderType() === VALUE_LIST_BROWSE_DEFINITION.value) { + return {value: this.mdRepresentation.getValue()}; +>>>>>>> dspace-7.6.1 } return queryParams; } diff --git a/src/app/shared/object-list/metadata-representation-list-element/item/item-metadata-list-element.component.html b/src/app/shared/object-list/metadata-representation-list-element/item/item-metadata-list-element.component.html index 91219c7189a..904ea95c20a 100644 --- a/src/app/shared/object-list/metadata-representation-list-element/item/item-metadata-list-element.component.html +++ b/src/app/shared/object-list/metadata-representation-list-element/item/item-metadata-list-element.component.html @@ -1 +1 @@ - + diff --git a/src/app/shared/object-list/metadata-representation-list-element/item/item-metadata-list-element.component.spec.ts b/src/app/shared/object-list/metadata-representation-list-element/item/item-metadata-list-element.component.spec.ts index 6e48ba3a6f0..99052b6b143 100644 --- a/src/app/shared/object-list/metadata-representation-list-element/item/item-metadata-list-element.component.spec.ts +++ b/src/app/shared/object-list/metadata-representation-list-element/item/item-metadata-list-element.component.spec.ts @@ -23,7 +23,7 @@ describe('ItemMetadataListElementComponent', () => { beforeEach(waitForAsync(() => { fixture = TestBed.createComponent(ItemMetadataListElementComponent); comp = fixture.componentInstance; - comp.metadataRepresentation = mockItemMetadataRepresentation; + comp.mdRepresentation = mockItemMetadataRepresentation; fixture.detectChanges(); })); diff --git a/src/app/shared/object-list/metadata-representation-list-element/item/item-metadata-representation-list-element.component.ts b/src/app/shared/object-list/metadata-representation-list-element/item/item-metadata-representation-list-element.component.ts index 967b09986da..c4a6903129a 100644 --- a/src/app/shared/object-list/metadata-representation-list-element/item/item-metadata-representation-list-element.component.ts +++ b/src/app/shared/object-list/metadata-representation-list-element/item/item-metadata-representation-list-element.component.ts @@ -1,5 +1,5 @@ import { MetadataRepresentationListElementComponent } from '../metadata-representation-list-element.component'; -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, Input } from '@angular/core'; import { ItemMetadataRepresentation } from '../../../../core/shared/metadata-representation/item/item-metadata-representation.model'; import { getItemPageRoute } from '../../../../item-page/item-page-routing-paths'; @@ -11,7 +11,7 @@ import { getItemPageRoute } from '../../../../item-page/item-page-routing-paths' * An abstract class for displaying a single ItemMetadataRepresentation */ export class ItemMetadataRepresentationListElementComponent extends MetadataRepresentationListElementComponent implements OnInit { - metadataRepresentation: ItemMetadataRepresentation; + @Input() mdRepresentation: ItemMetadataRepresentation; /** * Route to the item's page @@ -19,6 +19,6 @@ export class ItemMetadataRepresentationListElementComponent extends MetadataRepr itemPageRoute: string; ngOnInit(): void { - this.itemPageRoute = getItemPageRoute(this.metadataRepresentation); + this.itemPageRoute = getItemPageRoute(this.mdRepresentation); } } diff --git a/src/app/shared/object-list/metadata-representation-list-element/metadata-representation-list-element.component.spec.ts b/src/app/shared/object-list/metadata-representation-list-element/metadata-representation-list-element.component.spec.ts index f0cc150b3ee..ba7844afb62 100644 --- a/src/app/shared/object-list/metadata-representation-list-element/metadata-representation-list-element.component.spec.ts +++ b/src/app/shared/object-list/metadata-representation-list-element/metadata-representation-list-element.component.spec.ts @@ -36,7 +36,11 @@ describe('MetadataRepresentationListElementComponent', () => { describe('when the value is not a URL', () => { beforeEach(() => { +<<<<<<< HEAD comp.metadataRepresentation = mockMetadataRepresentation; +======= + comp.mdRepresentation = mockMetadataRepresentation; +>>>>>>> dspace-7.6.1 }); it('isLink correctly detects a non-URL string as false', () => { waitForAsync(() => { @@ -47,7 +51,11 @@ describe('MetadataRepresentationListElementComponent', () => { describe('when the value is a URL', () => { beforeEach(() => { +<<<<<<< HEAD comp.metadataRepresentation = mockMetadataRepresentationUrl; +======= + comp.mdRepresentation = mockMetadataRepresentationUrl; +>>>>>>> dspace-7.6.1 }); it('isLink correctly detects a URL string as true', () => { waitForAsync(() => { diff --git a/src/app/shared/object-list/metadata-representation-list-element/metadata-representation-list-element.component.ts b/src/app/shared/object-list/metadata-representation-list-element/metadata-representation-list-element.component.ts index b69f6b37dcb..8b4f37c3f73 100644 --- a/src/app/shared/object-list/metadata-representation-list-element/metadata-representation-list-element.component.ts +++ b/src/app/shared/object-list/metadata-representation-list-element/metadata-representation-list-element.component.ts @@ -1,5 +1,6 @@ -import { Component } from '@angular/core'; +import { Component, Input } from '@angular/core'; import { MetadataRepresentation } from '../../../core/shared/metadata-representation/metadata-representation.model'; +import { Context } from '../../../core/shared/context.model'; @Component({ selector: 'ds-metadata-representation-list-element', @@ -9,10 +10,19 @@ import { MetadataRepresentation } from '../../../core/shared/metadata-representa * An abstract class for displaying a single MetadataRepresentation */ export class MetadataRepresentationListElementComponent { + /** + * The optional context + */ + @Input() context: Context; + /** * The metadata representation of this component */ +<<<<<<< HEAD metadataRepresentation: MetadataRepresentation; +======= + @Input() mdRepresentation: MetadataRepresentation; +>>>>>>> dspace-7.6.1 /** * Returns true if this component's value matches a basic regex "Is this an HTTP URL" test @@ -20,7 +30,11 @@ export class MetadataRepresentationListElementComponent { isLink(): boolean { // Match any string that begins with http:// or https:// const linkPattern = new RegExp(/^https?\/\/.*/); +<<<<<<< HEAD return linkPattern.test(this.metadataRepresentation.getValue()); +======= + return linkPattern.test(this.mdRepresentation.getValue()); +>>>>>>> dspace-7.6.1 } } diff --git a/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.html b/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.html index 7b611a7d1f0..6c92a3a5438 100644 --- a/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.html +++ b/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.html @@ -1,5 +1,6 @@
    +<<<<<<< HEAD {{metadataRepresentation.getValue()}} @@ -13,5 +14,20 @@ [routerLink]="['/browse/', metadataRepresentation.browseDefinition.id]" [queryParams]="getQueryParams()"> {{metadataRepresentation.getValue()}} +======= + + {{mdRepresentation.getValue()}} + + + {{mdRepresentation.getValue()}} + + {{mdRepresentation.getValue()}} + + {{mdRepresentation.getValue()}} +>>>>>>> dspace-7.6.1
    diff --git a/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.spec.ts b/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.spec.ts index cfb812a4752..91d7db35620 100644 --- a/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.spec.ts +++ b/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.spec.ts @@ -29,7 +29,7 @@ describe('PlainTextMetadataListElementComponent', () => { beforeEach(waitForAsync(() => { fixture = TestBed.createComponent(PlainTextMetadataListElementComponent); comp = fixture.componentInstance; - comp.metadataRepresentation = mockMetadataRepresentation; + comp.mdRepresentation = mockMetadataRepresentation; fixture.detectChanges(); })); diff --git a/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.ts b/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.ts index 2d21a7afe82..6103eab74b6 100644 --- a/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.ts +++ b/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.ts @@ -2,6 +2,7 @@ import { MetadataRepresentationType } from '../../../../core/shared/metadata-rep import { Component } from '@angular/core'; import { MetadataRepresentationListElementComponent } from '../metadata-representation-list-element.component'; import { metadataRepresentationComponent } from '../../../metadata-representation/metadata-representation.decorator'; +import { VALUE_LIST_BROWSE_DEFINITION } from '../../../../core/shared/value-list-browse-definition.resource-type'; @metadataRepresentationComponent('Publication', MetadataRepresentationType.PlainText) // For now, authority controlled fields are rendered the same way as plain text fields @@ -20,9 +21,15 @@ export class PlainTextMetadataListElementComponent extends MetadataRepresentatio * expects 'startsWith' (eg browse by date) or 'value' (eg browse by title) */ getQueryParams() { +<<<<<<< HEAD let queryParams = {startsWith: this.metadataRepresentation.getValue()}; if (this.metadataRepresentation.browseDefinition.metadataBrowse) { return {value: this.metadataRepresentation.getValue()}; +======= + let queryParams = {startsWith: this.mdRepresentation.getValue()}; + if (this.mdRepresentation.browseDefinition.getRenderType() === VALUE_LIST_BROWSE_DEFINITION.value) { + return {value: this.mdRepresentation.getValue()}; +>>>>>>> dspace-7.6.1 } return queryParams; } diff --git a/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-approved-search-result/claimed-approved-search-result-list-element.component.html b/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-approved-search-result/claimed-approved-search-result-list-element.component.html index 79e870e130c..4e0686f418f 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-approved-search-result/claimed-approved-search-result-list-element.component.html +++ b/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-approved-search-result/claimed-approved-search-result-list-element.component.html @@ -4,7 +4,11 @@

    {{ 'claimed-approved-search-result-list-element.t >>>>>> dspace-7.6.1 [showSubmitter]="showSubmitter">

    diff --git a/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-approved-search-result/claimed-approved-search-result-list-element.component.spec.ts b/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-approved-search-result/claimed-approved-search-result-list-element.component.spec.ts index c05a4fc390d..c27dfdc1f03 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-approved-search-result/claimed-approved-search-result-list-element.component.spec.ts +++ b/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-approved-search-result/claimed-approved-search-result-list-element.component.spec.ts @@ -12,13 +12,16 @@ import { getMockLinkService } from '../../../../mocks/link-service.mock'; import { VarDirective } from '../../../../utils/var.directive'; import { TruncatableService } from '../../../../truncatable/truncatable.service'; import { LinkService } from '../../../../../core/cache/builders/link.service'; -import { MyDspaceItemStatusType } from '../../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type'; import { ClaimedApprovedTaskSearchResult } from '../../../../object-collection/shared/claimed-approved-task-search-result.model'; import { ClaimedApprovedSearchResultListElementComponent } from './claimed-approved-search-result-list-element.component'; import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service'; import { DSONameServiceMock } from '../../../../mocks/dso-name.service.mock'; import { APP_CONFIG } from '../../../../../../config/app-config.interface'; import { environment } from '../../../../../../environments/environment'; +<<<<<<< HEAD +======= +import { Context } from '../../../../../core/shared/context.model'; +>>>>>>> dspace-7.6.1 import { TranslateModule } from '@ngx-translate/core'; let component: ClaimedApprovedSearchResultListElementComponent; @@ -104,8 +107,8 @@ describe('ClaimedApprovedSearchResultListElementComponent', () => { }); }); - it('should have properly status', () => { - expect(component.status).toEqual(MyDspaceItemStatusType.APPROVED); + it('should have the correct badge context', () => { + expect(component.badgeContext).toEqual(Context.MyDSpaceApproved); }); }); diff --git a/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-approved-search-result/claimed-approved-search-result-list-element.component.ts b/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-approved-search-result/claimed-approved-search-result-list-element.component.ts index 5dda0e44ea3..942d3e8fbf8 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-approved-search-result/claimed-approved-search-result-list-element.component.ts +++ b/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-approved-search-result/claimed-approved-search-result-list-element.component.ts @@ -4,7 +4,6 @@ import { ClaimedApprovedTaskSearchResult } from '../../../../object-collection/s import { listableObjectComponent } from '../../../../object-collection/shared/listable-object/listable-object.decorator'; import { LinkService } from '../../../../../core/cache/builders/link.service'; import { TruncatableService } from '../../../../truncatable/truncatable.service'; -import { MyDspaceItemStatusType } from '../../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type'; import { Observable } from 'rxjs'; import { RemoteData } from '../../../../../core/data/remote-data'; import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model'; @@ -14,6 +13,10 @@ import { ClaimedTaskSearchResult } from '../../../../object-collection/shared/cl import { ClaimedTask } from '../../../../../core/tasks/models/claimed-task-object.model'; import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service'; import { APP_CONFIG, AppConfig } from '../../../../../../config/app-config.interface'; +<<<<<<< HEAD +======= +import { Context } from 'src/app/core/shared/context.model'; +>>>>>>> dspace-7.6.1 /** * This component renders claimed task approved object for the search result in the list view. @@ -32,9 +35,9 @@ export class ClaimedApprovedSearchResultListElementComponent extends SearchResul public showSubmitter = true; /** - * Represent item's status + * Represents the badge context */ - public status = MyDspaceItemStatusType.APPROVED; + public badgeContext = Context.MyDSpaceApproved; /** * The workflowitem object that belonging to the result object @@ -44,7 +47,11 @@ export class ClaimedApprovedSearchResultListElementComponent extends SearchResul public constructor( protected linkService: LinkService, protected truncatableService: TruncatableService, +<<<<<<< HEAD protected dsoNameService: DSONameService, +======= + public dsoNameService: DSONameService, +>>>>>>> dspace-7.6.1 @Inject(APP_CONFIG) protected appConfig: AppConfig ) { super(truncatableService, dsoNameService, appConfig); diff --git a/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-declined-search-result/claimed-declined-search-result-list-element.component.html b/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-declined-search-result/claimed-declined-search-result-list-element.component.html index 83fb3646c62..c07c0a10cee 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-declined-search-result/claimed-declined-search-result-list-element.component.html +++ b/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-declined-search-result/claimed-declined-search-result-list-element.component.html @@ -4,7 +4,11 @@

    {{ 'claimed-declined-search-result-list-element.t >>>>>> dspace-7.6.1 [showSubmitter]="showSubmitter">

    diff --git a/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-declined-search-result/claimed-declined-search-result-list-element.component.spec.ts b/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-declined-search-result/claimed-declined-search-result-list-element.component.spec.ts index dc06446cbce..805e75b3590 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-declined-search-result/claimed-declined-search-result-list-element.component.spec.ts +++ b/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-declined-search-result/claimed-declined-search-result-list-element.component.spec.ts @@ -14,11 +14,14 @@ import { getMockLinkService } from '../../../../mocks/link-service.mock'; import { VarDirective } from '../../../../utils/var.directive'; import { TruncatableService } from '../../../../truncatable/truncatable.service'; import { LinkService } from '../../../../../core/cache/builders/link.service'; -import { MyDspaceItemStatusType } from '../../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type'; import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service'; import { DSONameServiceMock } from '../../../../mocks/dso-name.service.mock'; import { APP_CONFIG } from '../../../../../../config/app-config.interface'; import { environment } from '../../../../../../environments/environment'; +<<<<<<< HEAD:src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-declined-search-result/claimed-declined-search-result-list-element.component.spec.ts +======= +import { Context } from '../../../../../core/shared/context.model'; +>>>>>>> dspace-7.6.1:src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-declined-search-result/claimed-declided-search-result-list-element.component.spec.ts import { TranslateModule } from '@ngx-translate/core'; let component: ClaimedDeclinedSearchResultListElementComponent; @@ -104,8 +107,8 @@ describe('ClaimedDeclinedSearchResultListElementComponent', () => { }); }); - it('should have properly status', () => { - expect(component.status).toEqual(MyDspaceItemStatusType.DECLINED); + it('should have the correct badge context', () => { + expect(component.badgeContext).toEqual(Context.MyDSpaceDeclined); }); }); diff --git a/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-declined-search-result/claimed-declined-search-result-list-element.component.ts b/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-declined-search-result/claimed-declined-search-result-list-element.component.ts index 90f523a7fba..6c7941577b1 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-declined-search-result/claimed-declined-search-result-list-element.component.ts +++ b/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-declined-search-result/claimed-declined-search-result-list-element.component.ts @@ -5,7 +5,6 @@ import { ClaimedDeclinedTaskSearchResult } from '../../../../object-collection/s import { ViewMode } from '../../../../../core/shared/view-mode.model'; import { LinkService } from '../../../../../core/cache/builders/link.service'; import { TruncatableService } from '../../../../truncatable/truncatable.service'; -import { MyDspaceItemStatusType } from '../../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type'; import { Observable } from 'rxjs'; import { RemoteData } from '../../../../../core/data/remote-data'; import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model'; @@ -15,6 +14,10 @@ import { ClaimedTaskSearchResult } from '../../../../object-collection/shared/cl import { ClaimedTask } from '../../../../../core/tasks/models/claimed-task-object.model'; import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service'; import { APP_CONFIG, AppConfig } from '../../../../../../config/app-config.interface'; +<<<<<<< HEAD +======= +import { Context } from '../../../../../core/shared/context.model'; +>>>>>>> dspace-7.6.1 /** * This component renders claimed task declined object for the search result in the list view. @@ -33,9 +36,9 @@ export class ClaimedDeclinedSearchResultListElementComponent extends SearchResul public showSubmitter = true; /** - * Represent item's status + * Represents the badge context */ - public status = MyDspaceItemStatusType.DECLINED; + public badgeContext = Context.MyDSpaceDeclined; /** * The workflowitem object that belonging to the result object @@ -45,7 +48,11 @@ export class ClaimedDeclinedSearchResultListElementComponent extends SearchResul public constructor( protected linkService: LinkService, protected truncatableService: TruncatableService, +<<<<<<< HEAD protected dsoNameService: DSONameService, +======= + public dsoNameService: DSONameService, +>>>>>>> dspace-7.6.1 @Inject(APP_CONFIG) protected appConfig: AppConfig ) { super(truncatableService, dsoNameService, appConfig); diff --git a/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-declined-task-search-result/claimed-declined-task-search-result-list-element.component.html b/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-declined-task-search-result/claimed-declined-task-search-result-list-element.component.html index a8ec21fa6ca..b7f58fc86af 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-declined-task-search-result/claimed-declined-task-search-result-list-element.component.html +++ b/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-declined-task-search-result/claimed-declined-task-search-result-list-element.component.html @@ -4,7 +4,11 @@

    {{ 'claimed-declined-task-search-result-list-elem >>>>>> dspace-7.6.1 [showSubmitter]="showSubmitter">

    diff --git a/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-declined-task-search-result/claimed-declined-task-search-result-list-element.component.spec.ts b/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-declined-task-search-result/claimed-declined-task-search-result-list-element.component.spec.ts index 1b23def2e45..c5e02871868 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-declined-task-search-result/claimed-declined-task-search-result-list-element.component.spec.ts +++ b/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-declined-task-search-result/claimed-declined-task-search-result-list-element.component.spec.ts @@ -12,7 +12,10 @@ import { getMockLinkService } from '../../../../mocks/link-service.mock'; import { VarDirective } from '../../../../utils/var.directive'; import { TruncatableService } from '../../../../truncatable/truncatable.service'; import { LinkService } from '../../../../../core/cache/builders/link.service'; +<<<<<<< HEAD import { MyDspaceItemStatusType } from '../../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type'; +======= +>>>>>>> dspace-7.6.1 import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service'; import { DSONameServiceMock } from '../../../../mocks/dso-name.service.mock'; import { APP_CONFIG } from '../../../../../../config/app-config.interface'; @@ -102,8 +105,11 @@ describe('ClaimedDeclinedTaskSearchResultListElementComponent', () => { }); }); +<<<<<<< HEAD it('should have properly status', () => { expect(component.status).toEqual(MyDspaceItemStatusType.DECLINED_TASk); }); +======= +>>>>>>> dspace-7.6.1 }); diff --git a/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-declined-task-search-result/claimed-declined-task-search-result-list-element.component.ts b/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-declined-task-search-result/claimed-declined-task-search-result-list-element.component.ts index 131f4b67302..7a5f4f20e39 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-declined-task-search-result/claimed-declined-task-search-result-list-element.component.ts +++ b/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-declined-task-search-result/claimed-declined-task-search-result-list-element.component.ts @@ -4,7 +4,10 @@ import { ClaimedDeclinedTaskTaskSearchResult } from 'src/app/shared/object-colle import { ViewMode } from '../../../../../core/shared/view-mode.model'; import { LinkService } from '../../../../../core/cache/builders/link.service'; import { TruncatableService } from '../../../../truncatable/truncatable.service'; +<<<<<<< HEAD import { MyDspaceItemStatusType } from '../../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type'; +======= +>>>>>>> dspace-7.6.1 import { Observable } from 'rxjs'; import { RemoteData } from '../../../../../core/data/remote-data'; import { WorkflowItem } from '../../../../../core/submission/models/workflowitem.model'; @@ -14,6 +17,10 @@ import { ClaimedTaskSearchResult } from '../../../../object-collection/shared/cl import { ClaimedTask } from '../../../../../core/tasks/models/claimed-task-object.model'; import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service'; import { APP_CONFIG, AppConfig } from '../../../../../../config/app-config.interface'; +<<<<<<< HEAD +======= +import { Context } from '../../../../../core/shared/context.model'; +>>>>>>> dspace-7.6.1 /** * This component renders claimed task declined task object for the search result in the list view. @@ -32,9 +39,15 @@ export class ClaimedDeclinedTaskSearchResultListElementComponent extends SearchR public showSubmitter = true; /** +<<<<<<< HEAD * Represent item's status */ public status = MyDspaceItemStatusType.DECLINED_TASk; +======= + * Represents the badge context + */ + public badgeContext = Context.MyDSpaceDeclined; +>>>>>>> dspace-7.6.1 /** * The workflowitem object that belonging to the result object @@ -44,7 +57,11 @@ export class ClaimedDeclinedTaskSearchResultListElementComponent extends SearchR public constructor( protected linkService: LinkService, protected truncatableService: TruncatableService, +<<<<<<< HEAD protected dsoNameService: DSONameService, +======= + public dsoNameService: DSONameService, +>>>>>>> dspace-7.6.1 @Inject(APP_CONFIG) protected appConfig: AppConfig, ) { super(truncatableService, dsoNameService, appConfig); diff --git a/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.html b/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.html index 4584b12550c..f921aaccd70 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.html +++ b/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.html @@ -2,8 +2,13 @@ +======= + [badgeContext]="badgeContext" + [workflowItem]="workflowitem$.value"> +>>>>>>> dspace-7.6.1
    diff --git a/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.spec.ts b/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.spec.ts index 4ea716cc2aa..04c721356c1 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.spec.ts +++ b/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.spec.ts @@ -1,5 +1,16 @@ import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; +<<<<<<< HEAD import { ComponentFixture, fakeAsync, flush, TestBed, tick, waitForAsync } from '@angular/core/testing'; +======= +import { + ComponentFixture, + fakeAsync, + flush, + TestBed, + tick, + waitForAsync +} from '@angular/core/testing'; +>>>>>>> dspace-7.6.1 import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { of as observableOf } from 'rxjs'; @@ -7,9 +18,12 @@ import { of as observableOf } from 'rxjs'; import { Item } from '../../../../core/shared/item.model'; import { ClaimedSearchResultListElementComponent } from './claimed-search-result-list-element.component'; import { ClaimedTask } from '../../../../core/tasks/models/claimed-task-object.model'; +<<<<<<< HEAD import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type'; +======= +>>>>>>> dspace-7.6.1 import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model'; import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils'; import { ClaimedTaskSearchResult } from '../../../object-collection/shared/claimed-task-search-result.model'; @@ -23,6 +37,10 @@ import { DSONameServiceMock } from '../../../mocks/dso-name.service.mock'; import { APP_CONFIG } from '../../../../../config/app-config.interface'; import { environment } from '../../../../../environments/environment'; import { ObjectCacheService } from '../../../../core/cache/object-cache.service'; +<<<<<<< HEAD +======= +import { Context } from '../../../../core/shared/context.model'; +>>>>>>> dspace-7.6.1 let component: ClaimedSearchResultListElementComponent; let fixture: ComponentFixture; @@ -107,8 +125,8 @@ describe('ClaimedSearchResultListElementComponent', () => { expect(component.item$.value).toEqual(item); })); - it('should have properly status', () => { - expect(component.status).toEqual(MyDspaceItemStatusType.VALIDATION); + it('should have the correct badge context', () => { + expect(component.badgeContext).toEqual(Context.MyDSpaceValidation); }); it('should forward claimed-task-actions processComplete event to reloadObject event emitter', fakeAsync(() => { diff --git a/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.ts b/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.ts index 237a5f516e2..9a956962012 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.ts +++ b/src/app/shared/object-list/my-dspace-result-list-element/claimed-search-result/claimed-search-result-list-element.component.ts @@ -5,9 +5,12 @@ import { listableObjectComponent } from '../../../object-collection/shared/lista import { ClaimedTaskSearchResult } from '../../../object-collection/shared/claimed-task-search-result.model'; import { LinkService } from '../../../../core/cache/builders/link.service'; import { TruncatableService } from '../../../truncatable/truncatable.service'; +<<<<<<< HEAD import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type'; +======= +>>>>>>> dspace-7.6.1 import { BehaviorSubject, EMPTY, Observable } from 'rxjs'; import { RemoteData } from '../../../../core/data/remote-data'; import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model'; @@ -22,7 +25,12 @@ import { ObjectCacheService } from '../../../../core/cache/object-cache.service' import { getFirstCompletedRemoteData } from '../../../../core/shared/operators'; import { Item } from '../../../../core/shared/item.model'; import { mergeMap, tap } from 'rxjs/operators'; +<<<<<<< HEAD import { isNotEmpty } from '../../../empty.util'; +======= +import { isNotEmpty, hasValue } from '../../../empty.util'; +import { Context } from '../../../../core/shared/context.model'; +>>>>>>> dspace-7.6.1 @Component({ selector: 'ds-claimed-search-result-list-element', @@ -38,9 +46,14 @@ export class ClaimedSearchResultListElementComponent extends SearchResultListEle public showSubmitter = true; /** - * Represent item's status + * Represents the badge context */ - public status = MyDspaceItemStatusType.VALIDATION; + public badgeContext = Context.MyDSpaceValidation; + + /** + * The item object that belonging to the result object + */ + public item$: BehaviorSubject = new BehaviorSubject(null); /** * The item object that belonging to the result object @@ -60,7 +73,11 @@ export class ClaimedSearchResultListElementComponent extends SearchResultListEle public constructor( protected linkService: LinkService, protected truncatableService: TruncatableService, +<<<<<<< HEAD protected dsoNameService: DSONameService, +======= + public dsoNameService: DSONameService, +>>>>>>> dspace-7.6.1 protected objectCache: ObjectCacheService, @Inject(APP_CONFIG) protected appConfig: AppConfig ) { @@ -101,7 +118,13 @@ export class ClaimedSearchResultListElementComponent extends SearchResultListEle ngOnDestroy() { // This ensures the object is removed from cache, when action is performed on task +<<<<<<< HEAD this.objectCache.remove(this.dso._links.workflowitem.href); +======= + if (hasValue(this.dso)) { + this.objectCache.remove(this.dso._links.workflowitem.href); + } +>>>>>>> dspace-7.6.1 } } diff --git a/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.html b/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.html index 94426136b5d..1a11ef76a69 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.html +++ b/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.html @@ -5,12 +5,17 @@
    +<<<<<<< HEAD
    +======= +
    + +>>>>>>> dspace-7.6.1

    @@ -47,4 +52,8 @@

    -
    \ No newline at end of file +<<<<<<< HEAD +
    +======= +
    +>>>>>>> dspace-7.6.1 diff --git a/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.spec.ts b/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.spec.ts index aaddebd8eba..aa618add35a 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.spec.ts +++ b/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.spec.ts @@ -178,8 +178,8 @@ describe('ItemListPreviewComponent', () => { fixture.detectChanges(); }); - it('should show the entity type span', () => { - const entityField = fixture.debugElement.query(By.css('ds-type-badge')); + it('should show the badges', () => { + const entityField = fixture.debugElement.query(By.css('ds-themed-badges')); expect(entityField).not.toBeNull(); }); }); diff --git a/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.ts b/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.ts index 39f83bc3717..c101443ec64 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.ts +++ b/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.ts @@ -2,12 +2,19 @@ import { Component, Inject, Input, OnInit } from '@angular/core'; import { Item } from '../../../../core/shared/item.model'; import { fadeInOut } from '../../../animations/fade'; +<<<<<<< HEAD import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type'; import { SearchResult } from '../../../search/models/search-result.model'; import { APP_CONFIG, AppConfig } from '../../../../../config/app-config.interface'; import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; +======= +import { SearchResult } from '../../../search/models/search-result.model'; +import { APP_CONFIG, AppConfig } from '../../../../../config/app-config.interface'; +import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; +import { Context } from 'src/app/core/shared/context.model'; +>>>>>>> dspace-7.6.1 import { WorkflowItem } from 'src/app/core/submission/models/workflowitem.model'; /** @@ -32,9 +39,9 @@ export class ItemListPreviewComponent implements OnInit { @Input() object: SearchResult; /** - * Represent item's status + * Represents the badge context */ - @Input() status: MyDspaceItemStatusType; + @Input() badgeContext: Context; /** * A boolean representing if to show submitter information @@ -55,7 +62,11 @@ export class ItemListPreviewComponent implements OnInit { constructor( @Inject(APP_CONFIG) protected appConfig: AppConfig, +<<<<<<< HEAD private dsoNameService: DSONameService, +======= + public dsoNameService: DSONameService, +>>>>>>> dspace-7.6.1 ) { } diff --git a/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/themed-item-list-preview.component.ts b/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/themed-item-list-preview.component.ts index ea5a38e3cb6..ed76a09bd2d 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/themed-item-list-preview.component.ts +++ b/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/themed-item-list-preview.component.ts @@ -1,3 +1,4 @@ +<<<<<<< HEAD import { ChangeDetectorRef, Component, ComponentFactoryResolver, Input } from '@angular/core'; import { ThemedComponent } from '../../../theme-support/themed.component'; import { ItemListPreviewComponent } from './item-list-preview.component'; @@ -6,6 +7,15 @@ import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspa import { SearchResult } from '../../../search/models/search-result.model'; import { WorkflowItem } from 'src/app/core/submission/models/workflowitem.model'; import { ThemeService } from 'src/app/shared/theme-support/theme.service'; +======= +import { Component, Input } from '@angular/core'; +import { ThemedComponent } from '../../../theme-support/themed.component'; +import { ItemListPreviewComponent } from './item-list-preview.component'; +import { Item } from '../../../../core/shared/item.model'; +import { SearchResult } from '../../../search/models/search-result.model'; +import { Context } from 'src/app/core/shared/context.model'; +import { WorkflowItem } from 'src/app/core/submission/models/workflowitem.model'; +>>>>>>> dspace-7.6.1 /** * Themed wrapper for ItemListPreviewComponent @@ -16,12 +26,17 @@ import { ThemeService } from 'src/app/shared/theme-support/theme.service'; templateUrl: '../../../theme-support/themed.component.html' }) export class ThemedItemListPreviewComponent extends ThemedComponent { +<<<<<<< HEAD protected inAndOutputNames: (keyof ItemListPreviewComponent & keyof this)[] = ['item', 'object', 'status', 'showSubmitter', 'workflowItem']; +======= + protected inAndOutputNames: (keyof ItemListPreviewComponent & keyof this)[] = ['item', 'object', 'badgeContext', 'showSubmitter', 'workflowItem']; +>>>>>>> dspace-7.6.1 @Input() item: Item; @Input() object: SearchResult; +<<<<<<< HEAD @Input() status: MyDspaceItemStatusType; @Input() showSubmitter = false; @@ -40,6 +55,14 @@ export class ThemedItemListPreviewComponent extends ThemedComponent>>>>>> dspace-7.6.1 protected getComponentName(): string { return 'ItemListPreviewComponent'; } diff --git a/src/app/shared/object-list/my-dspace-result-list-element/item-search-result/item-search-result-list-element-submission.component.html b/src/app/shared/object-list/my-dspace-result-list-element/item-search-result/item-search-result-list-element-submission.component.html index b5e9155b813..a5718b59665 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/item-search-result/item-search-result-list-element-submission.component.html +++ b/src/app/shared/object-list/my-dspace-result-list-element/item-search-result/item-search-result-list-element-submission.component.html @@ -1,6 +1,10 @@ +======= + [badgeContext]="badgeContext"> +>>>>>>> dspace-7.6.1
    diff --git a/src/app/shared/object-list/my-dspace-result-list-element/item-search-result/item-search-result-list-element-submission.component.spec.ts b/src/app/shared/object-list/my-dspace-result-list-element/item-search-result/item-search-result-list-element-submission.component.spec.ts index 1dc221f2ed5..706b4bb73c8 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/item-search-result/item-search-result-list-element-submission.component.spec.ts +++ b/src/app/shared/object-list/my-dspace-result-list-element/item-search-result/item-search-result-list-element-submission.component.spec.ts @@ -1,11 +1,10 @@ import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; -import { waitForAsync, ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; +import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { of as observableOf } from 'rxjs'; import { Item } from '../../../../core/shared/item.model'; -import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type'; import { ItemSearchResult } from '../../../object-collection/shared/item-search-result.model'; import { ItemSearchResultListElementSubmissionComponent } from './item-search-result-list-element-submission.component'; import { TruncatableService } from '../../../truncatable/truncatable.service'; @@ -14,6 +13,10 @@ import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; import { DSONameServiceMock } from '../../../mocks/dso-name.service.mock'; import { APP_CONFIG } from '../../../../../config/app-config.interface'; import { environment } from '../../../../../environments/environment'; +<<<<<<< HEAD +======= +import { Context } from '../../../../core/shared/context.model'; +>>>>>>> dspace-7.6.1 let component: ItemSearchResultListElementSubmissionComponent; let fixture: ComponentFixture; @@ -77,8 +80,8 @@ describe('ItemMyDSpaceResultListElementComponent', () => { fixture.detectChanges(); }); - it('should have properly status', () => { - expect(component.status).toEqual(MyDspaceItemStatusType.ARCHIVED); + it('should have correct badge context', () => { + expect(component.badgeContext).toEqual(Context.MyDSpaceArchived); }); it('should forward item-actions processComplete event to reloadObject event emitter', fakeAsync(() => { diff --git a/src/app/shared/object-list/my-dspace-result-list-element/item-search-result/item-search-result-list-element-submission.component.ts b/src/app/shared/object-list/my-dspace-result-list-element/item-search-result/item-search-result-list-element-submission.component.ts index 26cbafc102d..fbc8fa4b668 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/item-search-result/item-search-result-list-element-submission.component.ts +++ b/src/app/shared/object-list/my-dspace-result-list-element/item-search-result/item-search-result-list-element-submission.component.ts @@ -1,7 +1,6 @@ import { Component, OnInit } from '@angular/core'; import { Item } from '../../../../core/shared/item.model'; -import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type'; import { ViewMode } from '../../../../core/shared/view-mode.model'; import { listableObjectComponent } from '../../../object-collection/shared/listable-object/listable-object.decorator'; import { Context } from '../../../../core/shared/context.model'; @@ -21,9 +20,15 @@ import { SearchResultListElementComponent } from '../../search-result-list-eleme @listableObjectComponent(ItemSearchResult, ViewMode.ListElement, Context.Workflow) export class ItemSearchResultListElementSubmissionComponent extends SearchResultListElementComponent implements OnInit { /** - * Represent item's status + * Represents the badge context */ - public status = MyDspaceItemStatusType.ARCHIVED; + public badgeContext = Context.MyDSpaceArchived; + + + /** + * Display thumbnails if required by configuration + */ + showThumbnails: boolean; /** * Display thumbnails if required by configuration diff --git a/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.html b/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.html index d8633714461..40b2e5d0357 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.html +++ b/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.html @@ -2,8 +2,13 @@ +======= + [badgeContext]="badgeContext" + [workflowItem]="workflowitem$.value"> +>>>>>>> dspace-7.6.1
    >>>>>> dspace-7.6.1 import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { of as observableOf } from 'rxjs'; @@ -7,9 +18,12 @@ import { of as observableOf } from 'rxjs'; import { Item } from '../../../../core/shared/item.model'; import { PoolSearchResultListElementComponent } from './pool-search-result-list-element.component'; import { PoolTask } from '../../../../core/tasks/models/pool-task-object.model'; +<<<<<<< HEAD import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type'; +======= +>>>>>>> dspace-7.6.1 import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model'; import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils'; import { PoolTaskSearchResult } from '../../../object-collection/shared/pool-task-search-result.model'; @@ -22,6 +36,10 @@ import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; import { DSONameServiceMock } from '../../../mocks/dso-name.service.mock'; import { APP_CONFIG } from '../../../../../config/app-config.interface'; import { ObjectCacheService } from '../../../../core/cache/object-cache.service'; +<<<<<<< HEAD +======= +import { Context } from '../../../../core/shared/context.model'; +>>>>>>> dspace-7.6.1 let component: PoolSearchResultListElementComponent; let fixture: ComponentFixture; @@ -113,8 +131,8 @@ describe('PoolSearchResultListElementComponent', () => { expect(component.item$.value).toEqual(item); })); - it('should have properly status', () => { - expect(component.status).toEqual(MyDspaceItemStatusType.WAITING_CONTROLLER); + it('should have correct badge context', () => { + expect(component.badgeContext).toEqual(Context.MyDSpaceWaitingController); }); it('should forward pool-task-actions processCompleted event to the reloadedObject event emitter', fakeAsync(() => { diff --git a/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.ts b/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.ts index cb924af40f7..f33c37e4547 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.ts +++ b/src/app/shared/object-list/my-dspace-result-list-element/pool-search-result/pool-search-result-list-element.component.ts @@ -7,9 +7,12 @@ import { ViewMode } from '../../../../core/shared/view-mode.model'; import { RemoteData } from '../../../../core/data/remote-data'; import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model'; import { PoolTask } from '../../../../core/tasks/models/pool-task-object.model'; +<<<<<<< HEAD import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type'; +======= +>>>>>>> dspace-7.6.1 import { listableObjectComponent } from '../../../object-collection/shared/listable-object/listable-object.decorator'; import { PoolTaskSearchResult } from '../../../object-collection/shared/pool-task-search-result.model'; import { @@ -23,7 +26,12 @@ import { APP_CONFIG, AppConfig } from '../../../../../config/app-config.interfac import { ObjectCacheService } from '../../../../core/cache/object-cache.service'; import { getFirstCompletedRemoteData } from '../../../../core/shared/operators'; import { Item } from '../../../../core/shared/item.model'; +<<<<<<< HEAD import { isNotEmpty } from '../../../empty.util'; +======= +import { isNotEmpty, hasValue } from '../../../empty.util'; +import { Context } from '../../../../core/shared/context.model'; +>>>>>>> dspace-7.6.1 /** * This component renders pool task object for the search result in the list view. @@ -43,9 +51,14 @@ export class PoolSearchResultListElementComponent extends SearchResultListElemen public showSubmitter = true; /** - * Represent item's status + * Represents the badge Context */ - public status = MyDspaceItemStatusType.WAITING_CONTROLLER; + public badgeContext = Context.MyDSpaceWaitingController; + + /** + * The item object that belonging to the result object + */ + public item$: BehaviorSubject = new BehaviorSubject(null); /** * The item object that belonging to the result object @@ -70,7 +83,11 @@ export class PoolSearchResultListElementComponent extends SearchResultListElemen constructor( protected linkService: LinkService, protected truncatableService: TruncatableService, +<<<<<<< HEAD protected dsoNameService: DSONameService, +======= + public dsoNameService: DSONameService, +>>>>>>> dspace-7.6.1 protected objectCache: ObjectCacheService, @Inject(APP_CONFIG) protected appConfig: AppConfig ) { @@ -111,6 +128,12 @@ export class PoolSearchResultListElementComponent extends SearchResultListElemen ngOnDestroy() { // This ensures the object is removed from cache, when action is performed on task +<<<<<<< HEAD this.objectCache.remove(this.dso._links.workflowitem.href); +======= + if (hasValue(this.dso)) { + this.objectCache.remove(this.dso._links.workflowitem.href); + } +>>>>>>> dspace-7.6.1 } } diff --git a/src/app/shared/object-list/my-dspace-result-list-element/workflow-item-search-result/workflow-item-search-result-list-element.component.html b/src/app/shared/object-list/my-dspace-result-list-element/workflow-item-search-result/workflow-item-search-result-list-element.component.html index c3db054f8c8..5eca4e3cfac 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/workflow-item-search-result/workflow-item-search-result-list-element.component.html +++ b/src/app/shared/object-list/my-dspace-result-list-element/workflow-item-search-result/workflow-item-search-result-list-element.component.html @@ -1,4 +1,5 @@ +<<<<<<< HEAD
    @@ -7,6 +8,12 @@ +======= + +>>>>>>> dspace-7.6.1
    diff --git a/src/app/shared/object-list/my-dspace-result-list-element/workflow-item-search-result/workflow-item-search-result-list-element.component.spec.ts b/src/app/shared/object-list/my-dspace-result-list-element/workflow-item-search-result/workflow-item-search-result-list-element.component.spec.ts index 576b103ea75..4d3d70244a8 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/workflow-item-search-result/workflow-item-search-result-list-element.component.spec.ts +++ b/src/app/shared/object-list/my-dspace-result-list-element/workflow-item-search-result/workflow-item-search-result-list-element.component.spec.ts @@ -10,7 +10,6 @@ import { ItemDataService } from '../../../../core/data/item-data.service'; import { Item } from '../../../../core/shared/item.model'; import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model'; import { getMockLinkService } from '../../../mocks/link-service.mock'; -import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type'; import { WorkflowItemSearchResult } from '../../../object-collection/shared/workflow-item-search-result.model'; import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils'; import { TruncatableService } from '../../../truncatable/truncatable.service'; @@ -19,6 +18,10 @@ import { By } from '@angular/platform-browser'; import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; import { DSONameServiceMock } from '../../../mocks/dso-name.service.mock'; import { APP_CONFIG } from '../../../../../config/app-config.interface'; +<<<<<<< HEAD +======= +import { Context } from '../../../../core/shared/context.model'; +>>>>>>> dspace-7.6.1 let component: WorkflowItemSearchResultListElementComponent; let fixture: ComponentFixture; @@ -105,8 +108,8 @@ describe('WorkflowItemSearchResultListElementComponent', () => { }); }); - it('should have properly status', () => { - expect(component.status).toEqual(MyDspaceItemStatusType.WORKFLOW); + it('should have the correct badge context', () => { + expect(component.badgeContext).toEqual(Context.MyDSpaceWorkflow); }); it('should forward workflowitem-actions processCompleted event to the reloadedObject event emitter', fakeAsync(() => { diff --git a/src/app/shared/object-list/my-dspace-result-list-element/workflow-item-search-result/workflow-item-search-result-list-element.component.ts b/src/app/shared/object-list/my-dspace-result-list-element/workflow-item-search-result/workflow-item-search-result-list-element.component.ts index 11d4e181370..67beec512e0 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/workflow-item-search-result/workflow-item-search-result-list-element.component.ts +++ b/src/app/shared/object-list/my-dspace-result-list-element/workflow-item-search-result/workflow-item-search-result-list-element.component.ts @@ -8,7 +8,6 @@ import { Item } from '../../../../core/shared/item.model'; import { ViewMode } from '../../../../core/shared/view-mode.model'; import { WorkflowItem } from '../../../../core/submission/models/workflowitem.model'; import { listableObjectComponent } from '../../../object-collection/shared/listable-object/listable-object.decorator'; -import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type'; import { WorkflowItemSearchResult } from '../../../object-collection/shared/workflow-item-search-result.model'; import { TruncatableService } from '../../../truncatable/truncatable.service'; import { followLink } from '../../../utils/follow-link-config.model'; @@ -18,6 +17,10 @@ import { APP_CONFIG, AppConfig } from '../../../../../config/app-config.interfac import { getFirstSucceededRemoteDataPayload } from '../../../../core/shared/operators'; import { ItemSearchResult } from '../../../object-collection/shared/item-search-result.model'; import { CollectionElementLinkType } from '../../../object-collection/collection-element-link.type'; +<<<<<<< HEAD +======= +import { Context } from '../../../../core/shared/context.model'; +>>>>>>> dspace-7.6.1 /** * This component renders workflowitem object for the search result in the list view. @@ -33,16 +36,29 @@ export class WorkflowItemSearchResultListElementComponent extends SearchResultLi LinkTypes = CollectionElementLinkType; ViewModes = ViewMode; +<<<<<<< HEAD /** * The item search result derived from the WorkspaceItemSearchResult */ derivedSearchResult$: Observable; +======= /** - * Represent item's status + * The item search result derived from the WorkspaceItemSearchResult + */ + derivedSearchResult$: Observable; + + /** + * Represents the badge context */ - public status = MyDspaceItemStatusType.WORKFLOW; + public badgeContext = Context.MyDSpaceWorkflow; +>>>>>>> dspace-7.6.1 + + /** + * Display thumbnails if required by configuration + */ + showThumbnails: boolean; /** * Display thumbnails if required by configuration @@ -52,7 +68,11 @@ export class WorkflowItemSearchResultListElementComponent extends SearchResultLi constructor( protected truncatableService: TruncatableService, protected linkService: LinkService, +<<<<<<< HEAD protected dsoNameService: DSONameService, +======= + public dsoNameService: DSONameService, +>>>>>>> dspace-7.6.1 @Inject(APP_CONFIG) protected appConfig: AppConfig ) { super(truncatableService, dsoNameService, appConfig); diff --git a/src/app/shared/object-list/my-dspace-result-list-element/workspace-item-search-result/workspace-item-search-result-list-element.component.html b/src/app/shared/object-list/my-dspace-result-list-element/workspace-item-search-result/workspace-item-search-result-list-element.component.html index 4e86132df60..63f27d688b6 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/workspace-item-search-result/workspace-item-search-result-list-element.component.html +++ b/src/app/shared/object-list/my-dspace-result-list-element/workspace-item-search-result/workspace-item-search-result-list-element.component.html @@ -1,4 +1,5 @@ +<<<<<<< HEAD
    @@ -7,6 +8,12 @@ +======= + +>>>>>>> dspace-7.6.1
    diff --git a/src/app/shared/object-list/my-dspace-result-list-element/workspace-item-search-result/workspace-item-search-result-list-element.component.spec.ts b/src/app/shared/object-list/my-dspace-result-list-element/workspace-item-search-result/workspace-item-search-result-list-element.component.spec.ts index 2d64b1b581f..d0f5fddf6a5 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/workspace-item-search-result/workspace-item-search-result-list-element.component.spec.ts +++ b/src/app/shared/object-list/my-dspace-result-list-element/workspace-item-search-result/workspace-item-search-result-list-element.component.spec.ts @@ -1,5 +1,5 @@ import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; -import { waitForAsync, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; +import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { of as observableOf } from 'rxjs'; @@ -10,7 +10,6 @@ import { ItemDataService } from '../../../../core/data/item-data.service'; import { Item } from '../../../../core/shared/item.model'; import { WorkspaceItem } from '../../../../core/submission/models/workspaceitem.model'; import { getMockLinkService } from '../../../mocks/link-service.mock'; -import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type'; import { WorkflowItemSearchResult } from '../../../object-collection/shared/workflow-item-search-result.model'; import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils'; import { TruncatableService } from '../../../truncatable/truncatable.service'; @@ -19,6 +18,10 @@ import { By } from '@angular/platform-browser'; import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; import { DSONameServiceMock } from '../../../mocks/dso-name.service.mock'; import { APP_CONFIG } from '../../../../../config/app-config.interface'; +<<<<<<< HEAD +======= +import { Context } from '../../../../core/shared/context.model'; +>>>>>>> dspace-7.6.1 let component: WorkspaceItemSearchResultListElementComponent; let fixture: ComponentFixture; @@ -104,8 +107,8 @@ describe('WorkspaceItemSearchResultListElementComponent', () => { }); }); - it('should have properly status', () => { - expect(component.status).toEqual(MyDspaceItemStatusType.WORKSPACE); + it('should have correct badge context', () => { + expect(component.badgeContext).toEqual(Context.MyDSpaceWorkspace); }); it('should forward workspaceitem-actions processCompleted event to the reloadedObject event emitter', fakeAsync(() => { diff --git a/src/app/shared/object-list/my-dspace-result-list-element/workspace-item-search-result/workspace-item-search-result-list-element.component.ts b/src/app/shared/object-list/my-dspace-result-list-element/workspace-item-search-result/workspace-item-search-result-list-element.component.ts index 08ac896035b..44eb76c27d5 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/workspace-item-search-result/workspace-item-search-result-list-element.component.ts +++ b/src/app/shared/object-list/my-dspace-result-list-element/workspace-item-search-result/workspace-item-search-result-list-element.component.ts @@ -7,7 +7,6 @@ import { Item } from '../../../../core/shared/item.model'; import { ViewMode } from '../../../../core/shared/view-mode.model'; import { WorkspaceItem } from '../../../../core/submission/models/workspaceitem.model'; import { listableObjectComponent } from '../../../object-collection/shared/listable-object/listable-object.decorator'; -import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type'; import { WorkspaceItemSearchResult } from '../../../object-collection/shared/workspace-item-search-result.model'; import { TruncatableService } from '../../../truncatable/truncatable.service'; import { SearchResultListElementComponent } from '../../search-result-list-element/search-result-list-element.component'; @@ -18,6 +17,10 @@ import { map } from 'rxjs/operators'; import { getFirstSucceededRemoteDataPayload } from '../../../../core/shared/operators'; import { CollectionElementLinkType } from '../../../object-collection/collection-element-link.type'; import { followLink } from '../../../utils/follow-link-config.model'; +<<<<<<< HEAD +======= +import { Context } from '../../../../core/shared/context.model'; +>>>>>>> dspace-7.6.1 /** * This component renders workspaceitem object for the search result in the list view. @@ -33,16 +36,29 @@ export class WorkspaceItemSearchResultListElementComponent extends SearchResult LinkTypes = CollectionElementLinkType; ViewModes = ViewMode; +<<<<<<< HEAD /** * The item search result derived from the WorkspaceItemSearchResult */ derivedSearchResult$: Observable; +======= /** - * Represent item's status + * The item search result derived from the WorkspaceItemSearchResult + */ + derivedSearchResult$: Observable; + + /** + * Represents the badge context */ - status = MyDspaceItemStatusType.WORKSPACE; + public badgeContext = Context.MyDSpaceWorkspace; +>>>>>>> dspace-7.6.1 + + /** + * Display thumbnails if required by configuration + */ + showThumbnails: boolean; /** * Display thumbnails if required by configuration @@ -52,7 +68,11 @@ export class WorkspaceItemSearchResultListElementComponent extends SearchResult constructor( protected truncatableService: TruncatableService, protected linkService: LinkService, +<<<<<<< HEAD protected dsoNameService: DSONameService, +======= + public dsoNameService: DSONameService, +>>>>>>> dspace-7.6.1 @Inject(APP_CONFIG) protected appConfig: AppConfig ) { super(truncatableService, dsoNameService, appConfig); diff --git a/src/app/shared/object-list/object-list.component.html b/src/app/shared/object-list/object-list.component.html index 38d2175ff6c..6c683d46548 100644 --- a/src/app/shared/object-list/object-list.component.html +++ b/src/app/shared/object-list/object-list.component.html @@ -16,6 +16,7 @@ (prev)="goPrev()" (next)="goNext()">
      +<<<<<<< HEAD
    • +======= +
    • +>>>>>>> dspace-7.6.1 >>>>>> dspace-7.6.1 /** * Emit when one of the listed object has changed. diff --git a/src/app/shared/object-list/search-result-list-element/collection-search-result/collection-search-result-list-element.component.html b/src/app/shared/object-list/search-result-list-element/collection-search-result/collection-search-result-list-element.component.html index 83a8f4fdfa6..af1299094e0 100644 --- a/src/app/shared/object-list/search-result-list-element/collection-search-result/collection-search-result-list-element.component.html +++ b/src/app/shared/object-list/search-result-list-element/collection-search-result/collection-search-result-list-element.component.html @@ -1,8 +1,13 @@
      +<<<<<<< HEAD +======= + + +>>>>>>> dspace-7.6.1
      diff --git a/src/app/shared/object-list/search-result-list-element/collection-search-result/collection-search-result-list-element.component.ts b/src/app/shared/object-list/search-result-list-element/collection-search-result/collection-search-result-list-element.component.ts index 60415f649ed..3cebac99136 100644 --- a/src/app/shared/object-list/search-result-list-element/collection-search-result/collection-search-result-list-element.component.ts +++ b/src/app/shared/object-list/search-result-list-element/collection-search-result/collection-search-result-list-element.component.ts @@ -23,7 +23,11 @@ export class CollectionSearchResultListElementComponent extends SearchResultList ngOnInit(): void { super.ngOnInit(); +<<<<<<< HEAD this.showThumbnails = this.appConfig.browseBy.showThumbnails; +======= + this.showThumbnails = this.showThumbnails ?? this.appConfig.browseBy.showThumbnails; +>>>>>>> dspace-7.6.1 } } diff --git a/src/app/shared/object-list/search-result-list-element/community-search-result/community-search-result-list-element.component.html b/src/app/shared/object-list/search-result-list-element/community-search-result/community-search-result-list-element.component.html index 22be357a537..998863b031f 100644 --- a/src/app/shared/object-list/search-result-list-element/community-search-result/community-search-result-list-element.component.html +++ b/src/app/shared/object-list/search-result-list-element/community-search-result/community-search-result-list-element.component.html @@ -1,8 +1,13 @@
      +<<<<<<< HEAD +======= + + +>>>>>>> dspace-7.6.1
      diff --git a/src/app/shared/object-list/search-result-list-element/community-search-result/community-search-result-list-element.component.ts b/src/app/shared/object-list/search-result-list-element/community-search-result/community-search-result-list-element.component.ts index 4cc25b8b763..15c3b10c80d 100644 --- a/src/app/shared/object-list/search-result-list-element/community-search-result/community-search-result-list-element.component.ts +++ b/src/app/shared/object-list/search-result-list-element/community-search-result/community-search-result-list-element.component.ts @@ -23,6 +23,10 @@ export class CommunitySearchResultListElementComponent extends SearchResultListE ngOnInit(): void { super.ngOnInit(); +<<<<<<< HEAD this.showThumbnails = this.appConfig.browseBy.showThumbnails; +======= + this.showThumbnails = this.showThumbnails ?? this.appConfig.browseBy.showThumbnails; +>>>>>>> dspace-7.6.1 } } diff --git a/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.html b/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.html index bf1d51bfc26..1b1b8a53dbb 100644 --- a/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.html +++ b/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.html @@ -1,6 +1,10 @@
      + + + >>>>>> dspace-7.6.1 [routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out" [innerHTML]="dsoTitle"> +<<<<<<< HEAD ( ) +======= + ( + , + ) +>>>>>>> dspace-7.6.1 diff --git a/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.ts b/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.ts index f84ae642ad2..2d493e6d44e 100644 --- a/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.ts +++ b/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.ts @@ -29,7 +29,11 @@ export class ItemSearchResultListElementComponent extends SearchResultListElemen ngOnInit(): void { super.ngOnInit(); +<<<<<<< HEAD this.showThumbnails = this.appConfig.browseBy.showThumbnails; +======= + this.showThumbnails = this.showThumbnails ?? this.appConfig.browseBy.showThumbnails; +>>>>>>> dspace-7.6.1 this.itemPageRoute = getItemPageRoute(this.dso); } } diff --git a/src/app/shared/object-list/search-result-list-element/search-result-list-element.component.ts b/src/app/shared/object-list/search-result-list-element/search-result-list-element.component.ts index e56b7e970a9..e3b015ec16c 100644 --- a/src/app/shared/object-list/search-result-list-element/search-result-list-element.component.ts +++ b/src/app/shared/object-list/search-result-list-element/search-result-list-element.component.ts @@ -22,9 +22,15 @@ export class SearchResultListElementComponent, K exten dsoTitle: string; public constructor(protected truncatableService: TruncatableService, +<<<<<<< HEAD protected dsoNameService: DSONameService, @Inject(APP_CONFIG) protected appConfig?: AppConfig) { super(); +======= + public dsoNameService: DSONameService, + @Inject(APP_CONFIG) protected appConfig?: AppConfig) { + super(dsoNameService); +>>>>>>> dspace-7.6.1 } /** diff --git a/src/app/shared/object-list/selectable-list/selectable-list.service.spec.ts b/src/app/shared/object-list/selectable-list/selectable-list.service.spec.ts index 1535671f797..496ef287616 100644 --- a/src/app/shared/object-list/selectable-list/selectable-list.service.spec.ts +++ b/src/app/shared/object-list/selectable-list/selectable-list.service.spec.ts @@ -11,7 +11,7 @@ import { } from './selectable-list.actions'; import { AppState } from '../../../app.reducer'; -class SelectableObject extends ListableObject { +export class SelectableObject extends ListableObject { constructor(private value: string) { super(); } diff --git a/src/app/shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component.ts b/src/app/shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component.ts index c87559e11d3..0ffe2d58b44 100644 --- a/src/app/shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component.ts +++ b/src/app/shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component.ts @@ -35,7 +35,7 @@ export class SidebarSearchListElementComponent, K exte public constructor(protected truncatableService: TruncatableService, protected linkService: LinkService, - protected dsoNameService: DSONameService + public dsoNameService: DSONameService, ) { super(truncatableService, dsoNameService, null); } diff --git a/src/app/shared/object-list/themed-object-list.component.ts b/src/app/shared/object-list/themed-object-list.component.ts index 8d47f9e1086..6402856777b 100644 --- a/src/app/shared/object-list/themed-object-list.component.ts +++ b/src/app/shared/object-list/themed-object-list.component.ts @@ -1,3 +1,4 @@ +<<<<<<< HEAD import {Component, EventEmitter, Input, Output} from '@angular/core'; import { ObjectListComponent } from './object-list.component'; import { ThemedComponent } from '../theme-support/themed.component'; @@ -9,6 +10,18 @@ import {Context} from '../../core/shared/context.model'; import {RemoteData} from '../../core/data/remote-data'; import {PaginatedList} from '../../core/data/paginated-list.model'; import {ListableObject} from '../object-collection/shared/listable-object.model'; +======= +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { ObjectListComponent } from './object-list.component'; +import { ThemedComponent } from '../theme-support/themed.component'; +import { PaginationComponentOptions } from '../pagination/pagination-component-options.model'; +import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; +import { CollectionElementLinkType } from '../object-collection/collection-element-link.type'; +import { Context } from '../../core/shared/context.model'; +import { RemoteData } from '../../core/data/remote-data'; +import { PaginatedList } from '../../core/data/paginated-list.model'; +import { ListableObject } from '../object-collection/shared/listable-object.model'; +>>>>>>> dspace-7.6.1 /** * Themed wrapper for ObjectListComponent @@ -19,10 +32,13 @@ import {ListableObject} from '../object-collection/shared/listable-object.model' templateUrl: '../theme-support/themed.component.html', }) export class ThemedObjectListComponent extends ThemedComponent { +<<<<<<< HEAD /** * The view mode of the this component */ viewMode = ViewMode.ListElement; +======= +>>>>>>> dspace-7.6.1 /** * The current pagination configuration @@ -37,18 +53,33 @@ export class ThemedObjectListComponent extends ThemedComponent>>>>>> dspace-7.6.1 /** * The whether or not the gear is hidden */ +<<<<<<< HEAD @Input() hideGear = false; +======= + @Input() hideGear: boolean; +>>>>>>> dspace-7.6.1 /** * Whether or not the pager is visible when there is only a single page of results */ +<<<<<<< HEAD @Input() hidePagerWhenSinglePage = true; @Input() selectable = false; +======= + @Input() hidePagerWhenSinglePage: boolean; + + @Input() selectable: boolean; + +>>>>>>> dspace-7.6.1 @Input() selectionConfig: { repeatable: boolean, listId: string }; /** @@ -64,12 +95,20 @@ export class ThemedObjectListComponent extends ThemedComponent>>>>>> dspace-7.6.1 /** * Whether or not to add an import button to the object */ +<<<<<<< HEAD @Input() importable = false; +======= + @Input() importable: boolean; +>>>>>>> dspace-7.6.1 /** * Config used for the import button @@ -79,6 +118,7 @@ export class ThemedObjectListComponent extends ThemedComponent = new EventEmitter(); + + /** + * If showPaginator is set to true, emit when the previous button is clicked + */ + @Output() prev: EventEmitter = new EventEmitter(); + + /** + * If showPaginator is set to true, emit when the next button is clicked + */ + @Output() next: EventEmitter = new EventEmitter(); + + @Input() objects: RemoteData>; +>>>>>>> dspace-7.6.1 /** * An event fired when the page is changed. @@ -123,48 +188,80 @@ export class ThemedObjectListComponent extends ThemedComponent = new EventEmitter<{ pagination: PaginationComponentOptions, sort: SortOptions }>(); +======= + }> = new EventEmitter(); +>>>>>>> dspace-7.6.1 /** * An event fired when the page is changed. * Event's payload equals to the newly selected page. */ +<<<<<<< HEAD @Output() pageChange: EventEmitter = new EventEmitter(); +======= + @Output() pageChange: EventEmitter = new EventEmitter(); +>>>>>>> dspace-7.6.1 /** * An event fired when the page wsize is changed. * Event's payload equals to the newly selected page size. */ +<<<<<<< HEAD @Output() pageSizeChange: EventEmitter = new EventEmitter(); +======= + @Output() pageSizeChange: EventEmitter = new EventEmitter(); +>>>>>>> dspace-7.6.1 /** * An event fired when the sort direction is changed. * Event's payload equals to the newly selected sort direction. */ +<<<<<<< HEAD @Output() sortDirectionChange: EventEmitter = new EventEmitter(); +======= + @Output() sortDirectionChange: EventEmitter = new EventEmitter(); +>>>>>>> dspace-7.6.1 /** * An event fired when on of the pagination parameters changes */ +<<<<<<< HEAD @Output() paginationChange: EventEmitter = new EventEmitter(); @Output() deselectObject: EventEmitter = new EventEmitter(); @Output() selectObject: EventEmitter = new EventEmitter(); +======= + @Output() paginationChange: EventEmitter = new EventEmitter(); + + @Output() deselectObject: EventEmitter = new EventEmitter(); + + @Output() selectObject: EventEmitter = new EventEmitter(); +>>>>>>> dspace-7.6.1 /** * Send an import event to the parent component */ +<<<<<<< HEAD @Output() importObject: EventEmitter = new EventEmitter(); +======= + @Output() importObject: EventEmitter = new EventEmitter(); +>>>>>>> dspace-7.6.1 /** * An event fired when the sort field is changed. * Event's payload equals to the newly selected sort field. */ +<<<<<<< HEAD @Output() sortFieldChange: EventEmitter = new EventEmitter(); +======= + @Output() sortFieldChange: EventEmitter = new EventEmitter(); +>>>>>>> dspace-7.6.1 inAndOutputNames: (keyof ObjectListComponent & keyof this)[] = [ 'config', @@ -180,6 +277,10 @@ export class ThemedObjectListComponent extends ThemedComponent>>>>>> dspace-7.6.1 'contentChange', 'prev', 'next', diff --git a/src/app/shared/object-select/collection-select/collection-select.component.html b/src/app/shared/object-select/collection-select/collection-select.component.html index 84577f645e4..9b87f69c040 100644 --- a/src/app/shared/object-select/collection-select/collection-select.component.html +++ b/src/app/shared/object-select/collection-select/collection-select.component.html @@ -18,7 +18,7 @@ - {{collection.name}} + {{ dsoNameService.getName(collection) }} diff --git a/src/app/shared/object-select/collection-select/collection-select.component.scss b/src/app/shared/object-select/collection-select/collection-select.component.scss new file mode 100644 index 00000000000..b505f2895d4 --- /dev/null +++ b/src/app/shared/object-select/collection-select/collection-select.component.scss @@ -0,0 +1,3 @@ +table tr th:first-of-type, table tr td:first-of-type, { + width: 1rem !important; +} diff --git a/src/app/shared/object-select/collection-select/collection-select.component.ts b/src/app/shared/object-select/collection-select/collection-select.component.ts index a02305f1160..2d36f80274a 100644 --- a/src/app/shared/object-select/collection-select/collection-select.component.ts +++ b/src/app/shared/object-select/collection-select/collection-select.component.ts @@ -4,10 +4,12 @@ import { ObjectSelectComponent } from '../object-select/object-select.component' import { isNotEmpty } from '../../empty.util'; import { ObjectSelectService } from '../object-select.service'; import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; +import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; @Component({ selector: 'ds-collection-select', - templateUrl: './collection-select.component.html' + templateUrl: './collection-select.component.html', + styleUrls: ['./collection-select.component.scss'], }) /** @@ -15,8 +17,11 @@ import { AuthorizationDataService } from '../../../core/data/feature-authorizati */ export class CollectionSelectComponent extends ObjectSelectComponent { - constructor(protected objectSelectService: ObjectSelectService, - protected authorizationService: AuthorizationDataService) { + constructor( + protected objectSelectService: ObjectSelectService, + protected authorizationService: AuthorizationDataService, + public dsoNameService: DSONameService, + ) { super(objectSelectService, authorizationService); } diff --git a/src/app/shared/object-select/item-select/item-select.component.html b/src/app/shared/object-select/item-select/item-select.component.html index b7fce78289f..7f8ff943a37 100644 --- a/src/app/shared/object-select/item-select/item-select.component.html +++ b/src/app/shared/object-select/item-select/item-select.component.html @@ -22,11 +22,13 @@ - {{collection?.name}} + + {{ dsoNameService.getName(collection) }} + {{item.firstMetadataValue(['dc.contributor.author', 'dc.creator', 'dc.contributor.*'])}} - {{item.firstMetadataValue("dc.title")}} + {{ dsoNameService.getName(item) }} diff --git a/src/app/shared/object-select/item-select/item-select.component.ts b/src/app/shared/object-select/item-select/item-select.component.ts index 5cf32c2953e..dd0266ff834 100644 --- a/src/app/shared/object-select/item-select/item-select.component.ts +++ b/src/app/shared/object-select/item-select/item-select.component.ts @@ -8,6 +8,7 @@ import { getAllSucceededRemoteDataPayload } from '../../../core/shared/operators import { map } from 'rxjs/operators'; import { getItemPageRoute } from '../../../item-page/item-page-routing-paths'; import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; +import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; @Component({ selector: 'ds-item-select', @@ -34,8 +35,11 @@ export class ItemSelectComponent extends ObjectSelectComponent { [itemId: string]: string }>; - constructor(protected objectSelectService: ObjectSelectService, - protected authorizationService: AuthorizationDataService ) { + constructor( + protected objectSelectService: ObjectSelectService, + protected authorizationService: AuthorizationDataService, + public dsoNameService: DSONameService, + ) { super(objectSelectService, authorizationService); } diff --git a/src/app/shared/page-size-selector/page-size-selector.component.spec.ts b/src/app/shared/page-size-selector/page-size-selector.component.spec.ts index 67947c34259..6b0e9d265ba 100644 --- a/src/app/shared/page-size-selector/page-size-selector.component.spec.ts +++ b/src/app/shared/page-size-selector/page-size-selector.component.spec.ts @@ -72,9 +72,9 @@ describe('PageSizeSelectorComponent', () => { }); it('it should show the size settings with the respective selectable options', (done) => { - (comp as any).paginationOptions$.pipe(first()).subscribe((options) => { + comp.paginationOptions$.pipe(first()).subscribe((options: PaginationComponentOptions) => { const pageSizeSetting = fixture.debugElement.query(By.css('div.page-size-settings')); - expect(pageSizeSetting).toBeDefined(); + expect(pageSizeSetting).not.toBeNull(); const childElements = pageSizeSetting.queryAll(By.css('option')); expect(childElements.length).toEqual(options.pageSizeOptions.length); done(); @@ -83,10 +83,11 @@ describe('PageSizeSelectorComponent', () => { }); it('should have the proper rpp value selected by default', (done) => { - (comp as any).paginationOptions$.pipe(take(1)).subscribe((options) => { + comp.paginationOptions$.pipe(take(1)).subscribe(() => { const pageSizeSetting = fixture.debugElement.query(By.css('div.page-size-settings')); - const childElementToBeSelected = pageSizeSetting.query(By.css('option[value="10"][selected="selected"]')); - expect(childElementToBeSelected).toBeDefined(); + const childElementToBeSelected = pageSizeSetting.query(By.css('option[value="10"]')); + expect(childElementToBeSelected).not.toBeNull(); + expect(childElementToBeSelected.nativeElement.selected).toBeTrue(); done(); }); }); diff --git a/src/app/shared/pagination/pagination.component.html b/src/app/shared/pagination/pagination.component.html index f0a88660494..ca3a2f0f7e0 100644 --- a/src/app/shared/pagination/pagination.component.html +++ b/src/app/shared/pagination/pagination.component.html @@ -39,12 +39,20 @@
      diff --git a/src/app/shared/search-form/scope-selector-modal/scope-selector-modal.component.ts b/src/app/shared/search-form/scope-selector-modal/scope-selector-modal.component.ts index 86c30102875..3301435956e 100644 --- a/src/app/shared/search-form/scope-selector-modal/scope-selector-modal.component.ts +++ b/src/app/shared/search-form/scope-selector-modal/scope-selector-modal.component.ts @@ -4,6 +4,8 @@ import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model'; import { DSOSelectorModalWrapperComponent, SelectorActionType } from '../../dso-selector/modal-wrappers/dso-selector-modal-wrapper.component'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; +import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model'; +import { environment } from '../../../../environments/environment'; /** * Component to wrap a button - to select the entire repository - @@ -33,6 +35,11 @@ export class ScopeSelectorModalComponent extends DSOSelectorModalWrapperComponen */ scopeChange = new EventEmitter(); + /** + * Default DSO ordering + */ + defaultSort = new SortOptions(environment.comcolSelectionSort.sortField, environment.comcolSelectionSort.sortDirection as SortDirection); + constructor(protected activeModal: NgbActiveModal, protected route: ActivatedRoute) { super(activeModal, route); } diff --git a/src/app/shared/search-form/search-form.component.html b/src/app/shared/search-form/search-form.component.html index 226962cc617..3e8a38e8d8e 100644 --- a/src/app/shared/search-form/search-form.component.html +++ b/src/app/shared/search-form/search-form.component.html @@ -2,9 +2,20 @@
      +<<<<<<< HEAD
      + {{dsoNameService.getName(selectedScope | async) || ('search.form.scope.all' | translate)}} + +
      + >>>>>> dspace-7.6.1 [placeholder]="searchPlaceholder"> diff --git a/src/app/shared/search-form/search-form.component.spec.ts b/src/app/shared/search-form/search-form.component.spec.ts index 4b5844f6609..c7f4921ed76 100644 --- a/src/app/shared/search-form/search-form.component.spec.ts +++ b/src/app/shared/search-form/search-form.component.spec.ts @@ -28,12 +28,16 @@ describe('SearchFormComponent', () => { const searchService = new SearchServiceStub(); const paginationService = new PaginationServiceStub(); const searchConfigService = { paginationID: 'test-id' }; +<<<<<<< HEAD +======= + const firstPage = { 'spc.page': 1 }; +>>>>>>> dspace-7.6.1 const dspaceObjectService = { findById: () => createSuccessfulRemoteDataObject$(undefined), }; beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ + return TestBed.configureTestingModule({ imports: [FormsModule, RouterTestingModule, TranslateModule.forRoot()], providers: [ { provide: Router, useValue: router }, @@ -96,7 +100,7 @@ describe('SearchFormComponent', () => { tick(); const scopeSelect = de.query(By.css('.scope-button')).nativeElement; - expect(scopeSelect.textContent).toBe(testCommunity.name); + expect(scopeSelect.textContent).toContain('Sample Community'); })); describe('updateSearch', () => { @@ -104,16 +108,28 @@ describe('SearchFormComponent', () => { const scope = 'MCU'; let searchQuery = {}; +<<<<<<< HEAD it('should navigate to the search page even when no parameters are provided', () => { comp.updateSearch(searchQuery); expect(router.navigate).toHaveBeenCalledWith(comp.getSearchLinkParts(), { queryParams: searchQuery, +======= + it('should navigate to the search first page even when no parameters are provided', () => { + comp.updateSearch(searchQuery); + + expect(router.navigate).toHaveBeenCalledWith(comp.getSearchLinkParts(), { + queryParams: { ...searchQuery, ...firstPage }, +>>>>>>> dspace-7.6.1 queryParamsHandling: 'merge' }); }); +<<<<<<< HEAD it('should navigate to the search page with parameters only query if only query is provided', () => { +======= + it('should navigate to the search first page with parameters only query if only query is provided', () => { +>>>>>>> dspace-7.6.1 searchQuery = { query: query }; @@ -121,12 +137,20 @@ describe('SearchFormComponent', () => { comp.updateSearch(searchQuery); expect(router.navigate).toHaveBeenCalledWith(comp.getSearchLinkParts(), { +<<<<<<< HEAD queryParams: searchQuery, +======= + queryParams: { ...searchQuery, ...firstPage }, +>>>>>>> dspace-7.6.1 queryParamsHandling: 'merge' }); }); +<<<<<<< HEAD it('should navigate to the search page with parameters only query if only scope is provided', () => { +======= + it('should navigate to the search first page with parameters only query if only scope is provided', () => { +>>>>>>> dspace-7.6.1 searchQuery = { scope: scope }; @@ -134,7 +158,11 @@ describe('SearchFormComponent', () => { comp.updateSearch(searchQuery); expect(router.navigate).toHaveBeenCalledWith(comp.getSearchLinkParts(), { +<<<<<<< HEAD queryParams: searchQuery, +======= + queryParams: {...searchQuery, ...firstPage}, +>>>>>>> dspace-7.6.1 queryParamsHandling: 'merge' }); }); @@ -172,6 +200,7 @@ describe('SearchFormComponent', () => { expect(comp.updateSearch).toHaveBeenCalledWith(searchQuery); }); }); +<<<<<<< HEAD // it('should call updateSearch when clicking the submit button with correct parameters', fakeAsync(() => { // comp.query = 'Test String' @@ -198,6 +227,11 @@ describe('SearchFormComponent', () => { }); export const objects: DSpaceObject[] = [ +======= +}); + +const objects: DSpaceObject[] = [ +>>>>>>> dspace-7.6.1 Object.assign(new Community(), { logo: { self: { diff --git a/src/app/shared/search-form/search-form.component.ts b/src/app/shared/search-form/search-form.component.ts index 7ea51e4c1e0..95a063bdd67 100644 --- a/src/app/shared/search-form/search-form.component.ts +++ b/src/app/shared/search-form/search-form.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { Component, EventEmitter, Input, Output, OnChanges } from '@angular/core'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { Router } from '@angular/router'; import { isNotEmpty } from '../empty.util'; @@ -12,23 +12,17 @@ import { take } from 'rxjs/operators'; import { BehaviorSubject } from 'rxjs'; import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; import { getFirstSucceededRemoteDataPayload } from '../../core/shared/operators'; - -/** - * This component renders a simple item page. - * The route parameter 'id' is used to request the item it represents. - * All fields of the item that should be displayed, are defined in its template. - */ +import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; @Component({ selector: 'ds-search-form', styleUrls: ['./search-form.component.scss'], templateUrl: './search-form.component.html' }) - /** * Component that represents the search form */ -export class SearchFormComponent implements OnInit { +export class SearchFormComponent implements OnChanges { /** * The search query */ @@ -37,7 +31,7 @@ export class SearchFormComponent implements OnInit { /** * True when the search component should show results on the current page */ - @Input() inPlaceSearch; + @Input() inPlaceSearch: boolean; /** * The currently selected scope object's UUID @@ -74,19 +68,21 @@ export class SearchFormComponent implements OnInit { */ @Output() submitSearch = new EventEmitter(); - constructor(private router: Router, - private searchService: SearchService, - private paginationService: PaginationService, - private searchConfig: SearchConfigurationService, - private modalService: NgbModal, - private dsoService: DSpaceObjectDataService + constructor( + protected router: Router, + protected searchService: SearchService, + protected paginationService: PaginationService, + protected searchConfig: SearchConfigurationService, + protected modalService: NgbModal, + protected dsoService: DSpaceObjectDataService, + public dsoNameService: DSONameService, ) { } /** * Retrieve the scope object from the URL so we can show its name */ - ngOnInit(): void { + ngOnChanges(): void { if (isNotEmpty(this.scope)) { this.dsoService.findById(this.scope).pipe(getFirstSucceededRemoteDataPayload()) .subscribe((scope: DSpaceObject) => this.selectedScope.next(scope)); @@ -118,21 +114,21 @@ export class SearchFormComponent implements OnInit { * @param data Updated parameters */ updateSearch(data: any) { - const queryParams = Object.assign({}, data); + const goToFirstPage = { 'spc.page': 1 }; + + const queryParams = Object.assign( + { + ...goToFirstPage + }, + data + ); - this.router.navigate(this.getSearchLinkParts(), { + void this.router.navigate(this.getSearchLinkParts(), { queryParams: queryParams, queryParamsHandling: 'merge' }); } - /** - * For usage of the isNotEmpty function in the template - */ - isNotEmpty(object: any) { - return isNotEmpty(object); - } - /** * @returns {string} The base path to the search page, or the current page when inPlaceSearch is true */ diff --git a/src/app/shared/search-form/themed-search-form.component.ts b/src/app/shared/search-form/themed-search-form.component.ts new file mode 100644 index 00000000000..50b3751b06d --- /dev/null +++ b/src/app/shared/search-form/themed-search-form.component.ts @@ -0,0 +1,50 @@ +import { Component, Input, Output, EventEmitter } from '@angular/core'; +import { ThemedComponent } from '../theme-support/themed.component'; +import { SearchFormComponent } from './search-form.component'; + +/** + * Themed wrapper for {@link SearchFormComponent} + */ +@Component({ + selector: 'ds-themed-search-form', + styleUrls: [], + templateUrl: '../../shared/theme-support/themed.component.html', +}) +export class ThemedSearchFormComponent extends ThemedComponent { + + @Input() query: string; + + @Input() inPlaceSearch: boolean; + + @Input() scope: string; + + @Input() currentUrl: string; + + @Input() large: boolean; + + @Input() brandColor: string; + + @Input() searchPlaceholder: string; + + @Input() showScopeSelector: boolean; + + @Output() submitSearch: EventEmitter = new EventEmitter(); + + protected inAndOutputNames: (keyof SearchFormComponent & keyof this)[] = [ + 'query', 'inPlaceSearch', 'scope', 'currentUrl', 'large', 'brandColor', 'searchPlaceholder', 'showScopeSelector', + 'submitSearch', + ]; + + protected getComponentName(): string { + return 'SearchFormComponent'; + } + + protected importThemedComponent(themeName: string): Promise { + return import(`../../../themes/${themeName}/app/shared/search-form/search-form.component`); + } + + protected importUnthemedComponent(): Promise { + return import('./search-form.component'); + } + +} diff --git a/src/app/shared/search/search-export-csv/search-export-csv.component.ts b/src/app/shared/search/search-export-csv/search-export-csv.component.ts index 6ad105342f6..1be9a600d4e 100644 --- a/src/app/shared/search/search-export-csv/search-export-csv.component.ts +++ b/src/app/shared/search/search-export-csv/search-export-csv.component.ts @@ -94,6 +94,22 @@ export class SearchExportCsvComponent implements OnInit { } }); } +<<<<<<< HEAD +======= + if (isNotEmpty(this.searchConfig.fixedFilter)) { + const fixedFilter = this.searchConfig.fixedFilter.substring(2); + const keyAndValue = fixedFilter.split('='); + if (keyAndValue.length > 1) { + const key = keyAndValue[0]; + const valueAndOperator = keyAndValue[1].split(','); + if (valueAndOperator.length > 1) { + const value = valueAndOperator[0]; + const operator = valueAndOperator[1]; + parameters.push({name: '-f', value: `${key},${operator}=${value}`}); + } + } + } +>>>>>>> dspace-7.6.1 } this.scriptDataService.invoke('metadata-export-search', parameters, []).pipe( diff --git a/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-selected-option/search-facet-selected-option.component.html b/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-selected-option/search-facet-selected-option.component.html index d5f88c53332..e5c0a8e4a21 100644 --- a/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-selected-option/search-facet-selected-option.component.html +++ b/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-selected-option/search-facet-selected-option.component.html @@ -4,7 +4,11 @@ [queryParams]="removeQueryParams" queryParamsHandling="merge"> diff --git a/src/app/shared/search/search-filters/search-filter/search-filter.component.html b/src/app/shared/search/search-filters/search-filter/search-filter.component.html index 13457cc0086..921e5acc80f 100644 --- a/src/app/shared/search/search-filters/search-filter/search-filter.component.html +++ b/src/app/shared/search/search-filters/search-filter/search-filter.component.html @@ -3,12 +3,16 @@
      @@ -21,8 +21,8 @@
      @@ -33,7 +33,7 @@ - diff --git a/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.ts b/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.ts index 938f67412e4..e3565a7e0d1 100644 --- a/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.ts +++ b/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.ts @@ -2,6 +2,7 @@ import { BehaviorSubject, combineLatest as observableCombineLatest, Subscription import { map, startWith } from 'rxjs/operators'; import { isPlatformBrowser } from '@angular/common'; import { Component, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; import { RemoteDataBuildService } from '../../../../../core/cache/builders/remote-data-build.service'; import { FilterType } from '../../../models/filter-type.model'; import { renderFacetFor } from '../search-filter-type-decorator'; @@ -53,11 +54,27 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple */ min = 1950; + /** + * i18n Label to use for minimum field + */ + minLabel: string; + /** * Fallback maximum for the range */ max = new Date().getUTCFullYear(); + /** + * i18n Label to use for maximum field + */ + maxLabel: string; + + /** + * Base configuration for nouislider + * https://refreshless.com/nouislider/slider-options/ + */ + config = {}; + /** * The current range of the filter */ @@ -78,6 +95,7 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple protected filterService: SearchFilterService, protected router: Router, protected rdbs: RemoteDataBuildService, + private translateService: TranslateService, @Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService, @Inject(IN_PLACE_SEARCH) public inPlaceSearch: boolean, @Inject(FILTER_CONFIG) public filterConfig: SearchFilterConfig, @@ -96,6 +114,11 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple super.ngOnInit(); this.min = yearFromString(this.filterConfig.minValue) || this.min; this.max = yearFromString(this.filterConfig.maxValue) || this.max; +<<<<<<< HEAD +======= + this.minLabel = this.translateService.instant('search.filters.filter.' + this.filterConfig.name + '.min.placeholder'); + this.maxLabel = this.translateService.instant('search.filters.filter.' + this.filterConfig.name + '.max.placeholder'); +>>>>>>> dspace-7.6.1 const iniMin = this.route.getQueryParameterValue(this.filterConfig.paramName + RANGE_FILTER_MIN_SUFFIX).pipe(startWith(undefined)); const iniMax = this.route.getQueryParameterValue(this.filterConfig.paramName + RANGE_FILTER_MAX_SUFFIX).pipe(startWith(undefined)); this.sub = observableCombineLatest(iniMin, iniMax).pipe( @@ -105,6 +128,15 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple return [minimum, maximum]; }) ).subscribe((minmax) => this.range = minmax); + + // Default/base config for nouislider + this.config = { + // Ensure draggable handles have labels + handleAttributes: [ + { 'aria-label': this.minLabel }, + { 'aria-label': this.maxLabel }, + ], + }; } /** diff --git a/src/app/shared/search/search-filters/themed-search-filters.component.ts b/src/app/shared/search/search-filters/themed-search-filters.component.ts new file mode 100644 index 00000000000..a9a9c10e519 --- /dev/null +++ b/src/app/shared/search/search-filters/themed-search-filters.component.ts @@ -0,0 +1,38 @@ +import { Component, Input } from '@angular/core'; +import { ThemedComponent } from '../../theme-support/themed.component'; +import { SearchFiltersComponent } from './search-filters.component'; +import { Observable } from 'rxjs/internal/Observable'; +import { RemoteData } from '../../../core/data/remote-data'; +import { SearchFilterConfig } from '../models/search-filter-config.model'; + +/** + * Themed wrapper for SearchFiltersComponent + */ +@Component({ + selector: 'ds-themed-search-filters', + styleUrls: [], + templateUrl: '../../theme-support/themed.component.html', +}) +export class ThemedSearchFiltersComponent extends ThemedComponent { + + @Input() currentConfiguration; + @Input() currentScope: string; + @Input() inPlaceSearch; + @Input() refreshFilters: Observable; + @Input() filters: Observable>; + + protected inAndOutputNames: (keyof SearchFiltersComponent & keyof this)[] = [ + 'filters', 'currentConfiguration', 'currentScope', 'inPlaceSearch', 'refreshFilters']; + + protected getComponentName(): string { + return 'SearchFiltersComponent'; + } + + protected importThemedComponent(themeName: string): Promise { + return import(`../../../../themes/${themeName}/app/shared/search/search-filters/search-filters.component`); + } + + protected importUnthemedComponent(): Promise { + return import('./search-filters.component'); + } +} diff --git a/src/app/shared/search/search-labels/search-label/search-label.component.html b/src/app/shared/search/search-labels/search-label/search-label.component.html index bffb7f9329b..17c5a487181 100644 --- a/src/app/shared/search/search-labels/search-label/search-label.component.html +++ b/src/app/shared/search/search-labels/search-label/search-label.component.html @@ -1,4 +1,4 @@ - {{('search.filters.applied.' + key) | translate}}: {{'search.filters.' + filterName + '.' + value | translate: {default: normalizeFilterValue(value)} }} diff --git a/src/app/shared/search/search-labels/search-label/search-label.component.ts b/src/app/shared/search/search-labels/search-label/search-label.component.ts index 74526ad2ad4..ab4c57d9f53 100644 --- a/src/app/shared/search/search-labels/search-label/search-label.component.ts +++ b/src/app/shared/search/search-labels/search-label/search-label.component.ts @@ -7,6 +7,7 @@ import { SearchService } from '../../../../core/shared/search/search.service'; import { currentPath } from '../../../utils/route.utils'; import { PaginationService } from '../../../../core/pagination/pagination.service'; import { SearchConfigurationService } from '../../../../core/shared/search/search-configuration.service'; +import { stripOperatorFromFilterValue } from '../../search.utils'; @Component({ selector: 'ds-search-label', @@ -83,7 +84,8 @@ export class SearchLabelComponent implements OnInit { normalizeFilterValue(value: string) { // const pattern = /,[^,]*$/g; const pattern = /,authority*$/g; - return value.replace(pattern, ''); + value = value.replace(pattern, ''); + return stripOperatorFromFilterValue(value); } private getFilterName(): string { diff --git a/src/app/shared/search/search-labels/search-labels.component.ts b/src/app/shared/search/search-labels/search-labels.component.ts index 8f77d73b217..2cc1919f500 100644 --- a/src/app/shared/search/search-labels/search-labels.component.ts +++ b/src/app/shared/search/search-labels/search-labels.component.ts @@ -4,7 +4,6 @@ import { Observable } from 'rxjs'; import { Params, Router } from '@angular/router'; import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service'; import { map } from 'rxjs/operators'; -import { stripOperatorFromFilterValue } from '../search.utils'; @Component({ selector: 'ds-search-labels', @@ -37,7 +36,7 @@ export class SearchLabelsComponent { const labels = {}; Object.keys(params) .forEach((key) => { - labels[key] = [...params[key].map((value) => stripOperatorFromFilterValue(value))]; + labels[key] = [...params[key].map((value) => value)]; }); return labels; }) diff --git a/src/app/shared/search/search-results/search-results.component.html b/src/app/shared/search/search-results/search-results.component.html index 1e8c81e4542..fa951ca730a 100644 --- a/src/app/shared/search/search-results/search-results.component.html +++ b/src/app/shared/search/search-results/search-results.component.html @@ -13,6 +13,10 @@

      {{ (configuration ? configuration + '.search.results. [linkType]="linkType" [context]="context" [hidePaginationDetail]="hidePaginationDetail" +<<<<<<< HEAD +======= + [showThumbnails]="showThumbnails" +>>>>>>> dspace-7.6.1 (contentChange)="contentChange.emit($event)" (deselectObject)="deselectObject.emit($event)" (selectObject)="selectObject.emit($event)"> diff --git a/src/app/shared/search/search-results/search-results.component.ts b/src/app/shared/search/search-results/search-results.component.ts index 0a83d3e5c6e..a31e7fa4587 100644 --- a/src/app/shared/search/search-results/search-results.component.ts +++ b/src/app/shared/search/search-results/search-results.component.ts @@ -53,6 +53,14 @@ export class SearchResultsComponent { @Input() showCsvExport = false; /** +<<<<<<< HEAD +======= + * Whether to show the thumbnail preview + */ + @Input() showThumbnails; + + /** +>>>>>>> dspace-7.6.1 * The current sorting configuration of the search */ @Input() sortConfig: SortOptions; diff --git a/src/app/shared/search/search-results/themed-search-results.component.ts b/src/app/shared/search/search-results/themed-search-results.component.ts index 655b68d22c7..c1aa11da0e8 100644 --- a/src/app/shared/search/search-results/themed-search-results.component.ts +++ b/src/app/shared/search/search-results/themed-search-results.component.ts @@ -21,7 +21,11 @@ import { ListableObject } from '../../object-collection/shared/listable-object.m templateUrl: '../../theme-support/themed.component.html', }) export class ThemedSearchResultsComponent extends ThemedComponent { +<<<<<<< HEAD protected inAndOutputNames: (keyof SearchResultsComponent & keyof this)[] = ['linkType', 'searchResults', 'searchConfig', 'showCsvExport', 'sortConfig', 'viewMode', 'configuration', 'disableHeader', 'selectable', 'context', 'hidePaginationDetail', 'selectionConfig', 'contentChange', 'deselectObject', 'selectObject']; +======= + protected inAndOutputNames: (keyof SearchResultsComponent & keyof this)[] = ['linkType', 'searchResults', 'searchConfig', 'showCsvExport', 'showThumbnails', 'sortConfig', 'viewMode', 'configuration', 'disableHeader', 'selectable', 'context', 'hidePaginationDetail', 'selectionConfig', 'contentChange', 'deselectObject', 'selectObject']; +>>>>>>> dspace-7.6.1 @Input() linkType: CollectionElementLinkType; @@ -29,7 +33,13 @@ export class ThemedSearchResultsComponent extends ThemedComponent>>>>>> dspace-7.6.1 @Input() sortConfig: SortOptions; @@ -37,6 +47,7 @@ export class ThemedSearchResultsComponent extends ThemedComponent = new EventEmitter(); @Output() selectObject: EventEmitter = new EventEmitter(); +======= + @Input() disableHeader: boolean; + + @Input() selectable: boolean; + + @Input() context: Context; + + @Input() hidePaginationDetail: boolean; + + @Input() selectionConfig: SelectionConfig; + + @Output() contentChange: EventEmitter = new EventEmitter(); + + @Output() deselectObject: EventEmitter = new EventEmitter(); + + @Output() selectObject: EventEmitter = new EventEmitter(); +>>>>>>> dspace-7.6.1 protected getComponentName(): string { return 'SearchResultsComponent'; diff --git a/src/app/shared/search/search-settings/search-settings.component.spec.ts b/src/app/shared/search/search-settings/search-settings.component.spec.ts index 06e506ddb05..d0b51f04b1f 100644 --- a/src/app/shared/search/search-settings/search-settings.component.spec.ts +++ b/src/app/shared/search/search-settings/search-settings.component.spec.ts @@ -107,6 +107,7 @@ describe('SearchSettingsComponent', () => { new SortOptions('dc.title', SortDirection.ASC), new SortOptions('dc.title', SortDirection.DESC) ]; + comp.currentSortOption = new SortOptions('score', SortDirection.DESC); // SearchPageComponent test instance fixture.detectChanges(); @@ -133,7 +134,8 @@ describe('SearchSettingsComponent', () => { it('should have the proper order value selected by default', () => { fixture.detectChanges(); const orderSetting = fixture.debugElement.query(By.css('div.result-order-settings')); - const childElementToBeSelected = orderSetting.query(By.css('option[value="score,DESC"][selected="selected"]')); - expect(childElementToBeSelected).toBeDefined(); + const childElementToBeSelected = orderSetting.query(By.css('option[value="score,DESC"]')); + expect(childElementToBeSelected).not.toBeNull(); + expect(childElementToBeSelected.nativeElement.selected).toBeTrue(); }); }); diff --git a/src/app/shared/search/search-sidebar/search-sidebar.component.html b/src/app/shared/search/search-sidebar/search-sidebar.component.html index 59d84ee3830..79094f6d4db 100644 --- a/src/app/shared/search/search-sidebar/search-sidebar.component.html +++ b/src/app/shared/search/search-sidebar/search-sidebar.component.html @@ -17,11 +17,15 @@ [defaultConfiguration]="configuration" [inPlaceSearch]="inPlaceSearch" (changeConfiguration)="changeConfiguration.emit($event)"> - +======= + [inPlaceSearch]="inPlaceSearch"> +>>>>>>> dspace-7.6.1

      diff --git a/src/app/shared/search/search-sidebar/themed-search-sidebar.component.ts b/src/app/shared/search/search-sidebar/themed-search-sidebar.component.ts new file mode 100644 index 00000000000..935f7975328 --- /dev/null +++ b/src/app/shared/search/search-sidebar/themed-search-sidebar.component.ts @@ -0,0 +1,54 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { ThemedComponent } from '../../theme-support/themed.component'; +import { SearchSidebarComponent } from './search-sidebar.component'; +import { SearchConfigurationOption } from '../search-switch-configuration/search-configuration-option.model'; +import { SortOptions } from '../../../core/cache/models/sort-options.model'; +import { ViewMode } from '../../../core/shared/view-mode.model'; +import { PaginatedSearchOptions } from '../models/paginated-search-options.model'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { RemoteData } from '../../../core/data/remote-data'; +import { SearchFilterConfig } from '../models/search-filter-config.model'; + +/** + * Themed wrapper for SearchSidebarComponent + */ +@Component({ + selector: 'ds-themed-search-sidebar', + styleUrls: [], + templateUrl: '../../theme-support/themed.component.html', +}) +export class ThemedSearchSidebarComponent extends ThemedComponent { + + @Input() configuration; + @Input() configurationList: SearchConfigurationOption[]; + @Input() currentScope: string; + @Input() currentSortOption: SortOptions; + @Input() filters: Observable>; + @Input() resultCount; + @Input() viewModeList: ViewMode[]; + @Input() showViewModes = true; + @Input() inPlaceSearch; + @Input() searchOptions: PaginatedSearchOptions; + @Input() sortOptionsList: SortOptions[]; + @Input() refreshFilters: BehaviorSubject; + @Output() toggleSidebar = new EventEmitter(); + @Output() changeConfiguration: EventEmitter = new EventEmitter(); + @Output() changeViewMode: EventEmitter = new EventEmitter(); + + protected inAndOutputNames: (keyof SearchSidebarComponent & keyof this)[] = [ + 'configuration', 'configurationList', 'currentScope', 'currentSortOption', + 'resultCount', 'filters', 'viewModeList', 'showViewModes', 'inPlaceSearch', + 'searchOptions', 'sortOptionsList', 'refreshFilters', 'toggleSidebar', 'changeConfiguration', 'changeViewMode']; + + protected getComponentName(): string { + return 'SearchSidebarComponent'; + } + + protected importThemedComponent(themeName: string): Promise { + return import(`../../../../themes/${themeName}/app/shared/search/search-sidebar/search-sidebar.component`); + } + + protected importUnthemedComponent(): Promise { + return import('./search-sidebar.component'); + } +} diff --git a/src/app/shared/search/search-switch-configuration/search-switch-configuration.component.spec.ts b/src/app/shared/search/search-switch-configuration/search-switch-configuration.component.spec.ts index fadde46e53d..8fc178c67a4 100644 --- a/src/app/shared/search/search-switch-configuration/search-switch-configuration.component.spec.ts +++ b/src/app/shared/search/search-switch-configuration/search-switch-configuration.component.spec.ts @@ -78,7 +78,7 @@ describe('SearchSwitchConfigurationComponent', () => { it('should display select field properly', () => { const selectField = fixture.debugElement.query(By.css('.form-control')); - expect(selectField).toBeDefined(); + expect(selectField).not.toBeNull(); const childElements = selectField.children; expect(childElements.length).toEqual(comp.configurationList.length); diff --git a/src/app/shared/search/search.component.html b/src/app/shared/search/search.component.html index 298ee310190..46fe0ccc00b 100644 --- a/src/app/shared/search/search.component.html +++ b/src/app/shared/search/search.component.html @@ -38,6 +38,10 @@ [selectable]="selectable" [selectionConfig]="selectionConfig" [showCsvExport]="showCsvExport" +<<<<<<< HEAD +======= + [showThumbnails]="showThumbnails" +>>>>>>> dspace-7.6.1 (contentChange)="onContentChange($event)" (deselectObject)="deselectObject.emit($event)" (selectObject)="selectObject.emit($event)"> @@ -46,7 +50,7 @@ - - + - + - - +
      diff --git a/src/app/shared/search/search.component.spec.ts b/src/app/shared/search/search.component.spec.ts index 3f00cf354ff..e6193007ef7 100644 --- a/src/app/shared/search/search.component.spec.ts +++ b/src/app/shared/search/search.component.spec.ts @@ -31,6 +31,11 @@ import { SearchObjects } from './models/search-objects.model'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { SearchFilterConfig } from './models/search-filter-config.model'; import { FilterType } from './models/filter-type.model'; +<<<<<<< HEAD +======= +import { getCommunityPageRoute } from '../../community-page/community-page-routing-paths'; +import { getCollectionPageRoute } from '../../collection-page/collection-page-routing-paths'; +>>>>>>> dspace-7.6.1 let comp: SearchComponent; let fixture: ComponentFixture; @@ -101,8 +106,9 @@ const searchServiceStub = jasmine.createSpyObj('SearchService', { search: mockResultsRD$, getSearchLink: '/search', getScopes: observableOf(['test-scope']), - getSearchConfigurationFor: createSuccessfulRemoteDataObject$(searchConfig) -}); + getSearchConfigurationFor: createSuccessfulRemoteDataObject$(searchConfig), + trackSearch: {}, +}) as SearchService; const configurationParam = 'default'; const queryParam = 'test query'; const scopeParam = '7669c72a-3f2a-451f-a3b9-9210e7a4c02f'; @@ -327,4 +333,64 @@ describe('SearchComponent', () => { })); }); + + describe('getDsoUUIDFromUrl', () => { + let url: string; + let result: string; + + describe('when the navigated URL is an entity route', () => { + beforeEach(() => { + url = '/entities/publication/9a364471-3f19-4e7b-916a-a24a44ff48e3'; + result = (comp as any).getDsoUUIDFromUrl(url); + }); + + it('should return the UUID', () => { + expect(result).toEqual('9a364471-3f19-4e7b-916a-a24a44ff48e3'); + }); + }); + + describe('when the navigated URL is a community route', () => { + beforeEach(() => { + url = `${getCommunityPageRoute('9a364471-3f19-4e7b-916a-a24a44ff48e3')}`; + result = (comp as any).getDsoUUIDFromUrl(url); + }); + + it('should return the UUID', () => { + expect(result).toEqual('9a364471-3f19-4e7b-916a-a24a44ff48e3'); + }); + }); + + describe('when the navigated URL is a collection route', () => { + beforeEach(() => { + url = `${getCollectionPageRoute('9a364471-3f19-4e7b-916a-a24a44ff48e3')}`; + result = (comp as any).getDsoUUIDFromUrl(url); + }); + + it('should return the UUID', () => { + expect(result).toEqual('9a364471-3f19-4e7b-916a-a24a44ff48e3'); + }); + }); + + describe('when the navigated URL is an item route', () => { + beforeEach(() => { + url = '/items/9a364471-3f19-4e7b-916a-a24a44ff48e3'; + result = (comp as any).getDsoUUIDFromUrl(url); + }); + + it('should return the UUID', () => { + expect(result).toEqual('9a364471-3f19-4e7b-916a-a24a44ff48e3'); + }); + }); + + describe('when the navigated URL is an invalid route', () => { + beforeEach(() => { + url = '/invalid/object/route/9a364471-3f19-4e7b-916a-a24a44ff48e3'; + result = (comp as any).getDsoUUIDFromUrl(url); + }); + + it('should return null', () => { + expect(result).toBeNull(); + }); + }); + }); }); diff --git a/src/app/shared/search/search.component.ts b/src/app/shared/search/search.component.ts index 3a37d8a3b4e..45ec40592dc 100644 --- a/src/app/shared/search/search.component.ts +++ b/src/app/shared/search/search.component.ts @@ -1,5 +1,5 @@ import { ChangeDetectionStrategy, Component, EventEmitter, Inject, Input, OnInit, Output } from '@angular/core'; -import { Router } from '@angular/router'; +import { NavigationStart, Router } from '@angular/router'; import { BehaviorSubject, combineLatest, Observable, Subscription } from 'rxjs'; import { debounceTime, distinctUntilChanged, filter, map, switchMap } from 'rxjs/operators'; @@ -11,7 +11,7 @@ import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { pushInOut } from '../animations/push'; import { HostWindowService } from '../host-window.service'; import { SidebarService } from '../sidebar/sidebar.service'; -import { hasValue } from '../empty.util'; +import { hasValue, hasValueOperator, isNotEmpty } from '../empty.util'; import { RouteService } from '../../core/services/route.service'; import { SEARCH_CONFIG_SERVICE } from '../../my-dspace-page/my-dspace-page.component'; import { PaginatedSearchOptions } from './models/paginated-search-options.model'; @@ -35,6 +35,12 @@ import { environment } from 'src/environments/environment'; import { SubmissionObject } from '../../core/submission/models/submission-object.model'; import { SearchFilterConfig } from './models/search-filter-config.model'; import { WorkspaceItem } from '../..//core/submission/models/workspaceitem.model'; +<<<<<<< HEAD +======= +import { ITEM_MODULE_PATH } from '../../item-page/item-page-routing-paths'; +import { COLLECTION_MODULE_PATH } from '../../collection-page/collection-page-routing-paths'; +import { COMMUNITY_MODULE_PATH } from '../../community-page/community-page-routing-paths'; +>>>>>>> dspace-7.6.1 @Component({ selector: 'ds-search', @@ -64,7 +70,7 @@ export class SearchComponent implements OnInit { * The configuration to use for the search options * If empty, 'default' is used */ - @Input() configuration = 'default'; + @Input() configuration; /** * The actual query for the fixed filter. @@ -128,6 +134,11 @@ export class SearchComponent implements OnInit { */ @Input() showSidebar = true; + /** + * Whether to show the thumbnail preview + */ + @Input() showThumbnails; + /** * Whether to show the view mode switch */ @@ -136,7 +147,7 @@ export class SearchComponent implements OnInit { /** * List of available view mode */ - @Input() useUniquePageId: false; + @Input() useUniquePageId: boolean; /** * List of available view mode @@ -154,6 +165,14 @@ export class SearchComponent implements OnInit { @Input() trackStatistics = false; /** +<<<<<<< HEAD +======= + * The default value for the search query when none is already defined in the {@link SearchConfigurationService} + */ + @Input() query: string; + + /** +>>>>>>> dspace-7.6.1 * The current configuration used during the search */ currentConfiguration$: BehaviorSubject = new BehaviorSubject(''); @@ -224,9 +243,21 @@ export class SearchComponent implements OnInit { searchLink: string; /** - * Subscription to unsubscribe from + * Regex to match UUIDs */ - sub: Subscription; + uuidRegex = /\b[0-9a-f]{8}\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\b[0-9a-f]{12}\b/g; + + /** + * List of paths that are considered to be the start of a route to an object page (excluding "/", e.g. "items") + * These are expected to end on an object UUID + * If they match the route we're navigating to, an object property will be added to the search event sent + */ + allowedObjectPaths: string[] = ['entities', ITEM_MODULE_PATH, COLLECTION_MODULE_PATH, COMMUNITY_MODULE_PATH]; + + /** + * Subscriptions to unsubscribe from + */ + subs: Subscription[] = []; /** * Emits an event with the current search result entries @@ -296,7 +327,7 @@ export class SearchComponent implements OnInit { ); const searchOptions$: Observable = this.getSearchOptions().pipe(distinctUntilChanged()); - this.sub = combineLatest([configuration$, searchSortOptions$, searchOptions$, sortOption$]).pipe( + this.subs.push(combineLatest([configuration$, searchSortOptions$, searchOptions$, sortOption$]).pipe( filter(([configuration, searchSortOptions, searchOptions, sortOption]: [string, SortOptions[], PaginatedSearchOptions, SortOptions]) => { // filter for search options related to instanced paginated id return searchOptions.pagination.id === this.paginationId; @@ -309,6 +340,9 @@ export class SearchComponent implements OnInit { configuration: searchOptions.configuration || configuration, sort: sortOption || searchOptions.sort }); + if (combinedOptions.query === '') { + combinedOptions.query = this.query; + } const newSearchOptions = new PaginatedSearchOptions(combinedOptions); // check if search options are changed // if so retrieve new related results otherwise skip it @@ -324,7 +358,9 @@ export class SearchComponent implements OnInit { this.retrieveSearchResults(newSearchOptions); this.retrieveFilters(searchOptions); } - }); + })); + + this.subscribeToRoutingEvents(); } /** @@ -366,12 +402,14 @@ export class SearchComponent implements OnInit { } /** +<<<<<<< HEAD * Unsubscribe from the subscription +======= + * Unsubscribe from the subscriptions +>>>>>>> dspace-7.6.1 */ ngOnDestroy(): void { - if (hasValue(this.sub)) { - this.sub.unsubscribe(); - } + this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe()); } /** @@ -385,9 +423,15 @@ export class SearchComponent implements OnInit { /** * Retrieve search filters by the given search options * @param searchOptions +<<<<<<< HEAD * @protected */ protected retrieveFilters(searchOptions: PaginatedSearchOptions) { +======= + * @private + */ + private retrieveFilters(searchOptions: PaginatedSearchOptions) { +>>>>>>> dspace-7.6.1 this.filtersRD$.next(null); this.searchConfigService.getConfig(searchOptions.scope, searchOptions.configuration).pipe( getFirstCompletedRemoteData(), @@ -432,6 +476,43 @@ export class SearchComponent implements OnInit { }); } + /** + * Subscribe to routing events to detect when a user moves away from the search page + * When the user is routing to an object page, it needs to send out a separate search event containing that object's UUID + * This method should only be called once and is essentially what SearchTrackingComponent used to do (now removed) + * @private + */ + private subscribeToRoutingEvents() { + this.subs.push( + this.router.events.pipe( + filter((event) => event instanceof NavigationStart), + map((event: NavigationStart) => this.getDsoUUIDFromUrl(event.url)), + hasValueOperator(), + ).subscribe((uuid) => { + if (this.resultsRD$.value.hasSucceeded) { + this.service.trackSearch(this.searchOptions$.value, this.resultsRD$.value.payload as SearchObjects, uuid); + } + }), + ); + } + + /** + * Get the UUID from a DSO url + * Return null if the url isn't an object page (allowedObjectPaths) or the UUID couldn't be found + * @param url + */ + private getDsoUUIDFromUrl(url: string): string { + if (isNotEmpty(url)) { + if (this.allowedObjectPaths.some((path) => url.startsWith(`/${path}`))) { + const uuid = url.substring(url.lastIndexOf('/') + 1); + if (uuid.match(this.uuidRegex)) { + return uuid; + } + } + } + return null; + } + /** * Check if the sidebar is collapsed * @returns {Observable} emits true if the sidebar is currently collapsed, false if it is expanded diff --git a/src/app/shared/search/search.module.ts b/src/app/shared/search/search.module.ts index ae980e4a178..49e54ceb2f4 100644 --- a/src/app/shared/search/search.module.ts +++ b/src/app/shared/search/search.module.ts @@ -33,6 +33,11 @@ import { ThemedSearchComponent } from './themed-search.component'; import { ThemedSearchResultsComponent } from './search-results/themed-search-results.component'; import { ThemedSearchSettingsComponent } from './search-settings/themed-search-settings.component'; import { NouisliderModule } from 'ng2-nouislider'; +<<<<<<< HEAD +======= +import { ThemedSearchFiltersComponent } from './search-filters/themed-search-filters.component'; +import { ThemedSearchSidebarComponent } from './search-sidebar/themed-search-sidebar.component'; +>>>>>>> dspace-7.6.1 const COMPONENTS = [ SearchComponent, @@ -59,6 +64,11 @@ const COMPONENTS = [ ThemedConfigurationSearchPageComponent, ThemedSearchResultsComponent, ThemedSearchSettingsComponent, +<<<<<<< HEAD +======= + ThemedSearchFiltersComponent, + ThemedSearchSidebarComponent, +>>>>>>> dspace-7.6.1 ]; const ENTRY_COMPONENTS = [ @@ -95,7 +105,10 @@ export const MODELS = [ useDefaultLang: true }), SharedModule.withEntryComponents(), +<<<<<<< HEAD SharedModule.withEntryComponents(), +======= +>>>>>>> dspace-7.6.1 NouisliderModule, ], exports: [ diff --git a/src/app/shared/search/themed-search.component.ts b/src/app/shared/search/themed-search.component.ts index bdeb7247792..3e88ade9552 100644 --- a/src/app/shared/search/themed-search.component.ts +++ b/src/app/shared/search/themed-search.component.ts @@ -11,7 +11,11 @@ import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { ListableObject } from '../object-collection/shared/listable-object.model'; /** +<<<<<<< HEAD * Themed wrapper for SearchComponent +======= + * Themed wrapper for {@link SearchComponent} +>>>>>>> dspace-7.6.1 */ @Component({ selector: 'ds-themed-search', @@ -19,6 +23,7 @@ import { ListableObject } from '../object-collection/shared/listable-object.mode templateUrl: '../theme-support/themed.component.html', }) export class ThemedSearchComponent extends ThemedComponent { +<<<<<<< HEAD protected inAndOutputNames: (keyof SearchComponent & keyof this)[] = ['configurationList', 'context', 'configuration', 'fixedFilterQuery', 'useCachedVersionIfAvailable', 'inPlaceSearch', 'linkType', 'paginationId', 'searchEnabled', 'sideBarWidth', 'searchFormPlaceholder', 'selectable', 'selectionConfig', 'showCsvExport', 'showSidebar', 'showViewModes', 'useUniquePageId', 'viewModeList', 'showScopeSelector', 'resultFound', 'deselectObject', 'selectObject', 'trackStatistics']; @Input() configurationList: SearchConfigurationOption[] = []; @@ -66,6 +71,59 @@ export class ThemedSearchComponent extends ThemedComponent { @Output() deselectObject: EventEmitter = new EventEmitter(); @Output() selectObject: EventEmitter = new EventEmitter(); +======= + protected inAndOutputNames: (keyof SearchComponent & keyof this)[] = ['configurationList', 'context', 'configuration', 'fixedFilterQuery', 'useCachedVersionIfAvailable', 'inPlaceSearch', 'linkType', 'paginationId', 'searchEnabled', 'sideBarWidth', 'searchFormPlaceholder', 'selectable', 'selectionConfig', 'showCsvExport', 'showSidebar', 'showThumbnails', 'showViewModes', 'useUniquePageId', 'viewModeList', 'showScopeSelector', 'resultFound', 'deselectObject', 'selectObject', 'trackStatistics', 'query']; + + @Input() configurationList: SearchConfigurationOption[]; + + @Input() context: Context; + + @Input() configuration: string; + + @Input() fixedFilterQuery: string; + + @Input() useCachedVersionIfAvailable: boolean; + + @Input() inPlaceSearch: boolean; + + @Input() linkType: CollectionElementLinkType; + + @Input() paginationId: string; + + @Input() searchEnabled: boolean; + + @Input() sideBarWidth: number; + + @Input() searchFormPlaceholder: string; + + @Input() selectable: boolean; + + @Input() selectionConfig: SelectionConfig; + + @Input() showCsvExport: boolean; + + @Input() showSidebar: boolean; + + @Input() showThumbnails; + + @Input() showViewModes: boolean; + + @Input() useUniquePageId: boolean; + + @Input() viewModeList: ViewMode[]; + + @Input() showScopeSelector: boolean; + + @Input() trackStatistics: boolean; + + @Input() query: string; + + @Output() resultFound: EventEmitter> = new EventEmitter(); + + @Output() deselectObject: EventEmitter = new EventEmitter(); + + @Output() selectObject: EventEmitter = new EventEmitter(); +>>>>>>> dspace-7.6.1 protected getComponentName(): string { return 'SearchComponent'; diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index acea16c0d4e..d9a7ff533d5 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -50,7 +50,12 @@ import { ErrorComponent } from './error/error.component'; import { LoadingComponent } from './loading/loading.component'; import { PaginationComponent } from './pagination/pagination.component'; import { ThumbnailComponent } from '../thumbnail/thumbnail.component'; +import { ThemedThumbnailComponent } from '../thumbnail/themed-thumbnail.component'; import { SearchFormComponent } from './search-form/search-form.component'; +<<<<<<< HEAD +======= +import { ThemedSearchFormComponent } from './search-form/themed-search-form.component'; +>>>>>>> dspace-7.6.1 import { SearchResultGridElementComponent } from './object-grid/search-result-grid-element/search-result-grid-element.component'; @@ -163,8 +168,13 @@ import { DsoInputSuggestionsComponent } from './input-suggestions/dso-input-suggestions/dso-input-suggestions.component'; import { ItemGridElementComponent } from './object-grid/item-grid-element/item-types/item/item-grid-element.component'; +<<<<<<< HEAD import { TypeBadgeComponent } from './object-list/type-badge/type-badge.component'; import { AccessStatusBadgeComponent } from './object-list/access-status-badge/access-status-badge.component'; +======= +import { TypeBadgeComponent } from './object-collection/shared/badges/type-badge/type-badge.component'; +import { AccessStatusBadgeComponent } from './object-collection/shared/badges/access-status-badge/access-status-badge.component'; +>>>>>>> dspace-7.6.1 import { MetadataRepresentationLoaderComponent } from './metadata-representation/metadata-representation-loader.component'; @@ -190,6 +200,10 @@ import { import { LogInContainerComponent } from './log-in/container/log-in-container.component'; import { LogInPasswordComponent } from './log-in/methods/password/log-in-password.component'; import { LogInComponent } from './log-in/log-in.component'; +<<<<<<< HEAD +======= +import { ThemedLogInComponent } from './log-in/themed-log-in.component'; +>>>>>>> dspace-7.6.1 import { MissingTranslationHelper } from './translate/missing-translation.helper'; import { FileValidator } from './utils/require-file.validator'; import { FileValueAccessorDirective } from './utils/file-value-accessor.directive'; @@ -233,6 +247,7 @@ import { SearchNavbarComponent } from '../search-navbar/search-navbar.component' import { ThemedSearchNavbarComponent } from '../search-navbar/themed-search-navbar.component'; import { ScopeSelectorModalComponent } from './search-form/scope-selector-modal/scope-selector-modal.component'; import { DsSelectComponent } from './ds-select/ds-select.component'; +<<<<<<< HEAD import { ClarinExtendedLicensePipe } from './utils/clarin-extended-license.pipe'; import { ClarinLicenseCheckedPipe } from './utils/clarin-license-checked.pipe'; import { ClarinLicenseLabelRadioValuePipe } from './utils/clarin-license-label-radio-value.pipe'; @@ -240,6 +255,8 @@ import { CharToEndPipe } from './utils/char-to-end.pipe'; import { ClarinLicenseRequiredInfoPipe } from './utils/clarin-license-required-info.pipe'; import { ClarinItemBoxViewComponent } from './clarin-item-box-view/clarin-item-box-view.component'; import { ClarinItemAuthorPreviewComponent } from './clarin-item-author-preview/clarin-item-author-preview.component'; +======= +>>>>>>> dspace-7.6.1 import { ContextHelpDirective } from './context-help.directive'; import { ContextHelpWrapperComponent } from './context-help-wrapper/context-help-wrapper.component'; import { RSSComponent } from './rss-feed/rss.component'; @@ -257,6 +274,19 @@ import { } from './object-list/listable-notification-object/listable-notification-object.component'; import { ThemedCollectionDropdownComponent } from './collection-dropdown/themed-collection-dropdown.component'; import { MetadataFieldWrapperComponent } from './metadata-field-wrapper/metadata-field-wrapper.component'; +<<<<<<< HEAD +======= + +import { StatusBadgeComponent } from './object-collection/shared/badges/status-badge/status-badge.component'; +import { BadgesComponent } from './object-collection/shared/badges/badges.component'; +import { ThemedBadgesComponent } from './object-collection/shared/badges/themed-badges.component'; +import { ThemedStatusBadgeComponent } from './object-collection/shared/badges/status-badge/themed-status-badge.component'; +import { ThemedTypeBadgeComponent } from './object-collection/shared/badges/type-badge/themed-type-badge.component'; +import { ThemedMyDSpaceStatusBadgeComponent } from './object-collection/shared/badges/my-dspace-status-badge/themed-my-dspace-status-badge.component'; +import { ThemedAccessStatusBadgeComponent } from './object-collection/shared/badges/access-status-badge/themed-access-status-badge.component'; +import { MyDSpaceStatusBadgeComponent } from './object-collection/shared/badges/my-dspace-status-badge/my-dspace-status-badge.component'; + +>>>>>>> dspace-7.6.1 import { ShortNumberPipe } from './utils/short-number.pipe'; import { LogInExternalProviderComponent @@ -268,6 +298,7 @@ import { AdvancedClaimedTaskActionRatingComponent } from './mydspace-actions/claimed-task/rating/advanced-claimed-task-action-rating.component'; import { ClaimedTaskActionsDeclineTaskComponent } from './mydspace-actions/claimed-task/decline-task/claimed-task-actions-decline-task.component'; +<<<<<<< HEAD import { DsoPageSubscriptionButtonComponent } from './dso-page/dso-page-subscription-button/dso-page-subscription-button.component'; @@ -276,6 +307,18 @@ import { EpersonSearchBoxComponent } from './eperson-group-list/eperson-search-b import { GroupSearchBoxComponent } from './eperson-group-list/group-search-box/group-search-box.component'; import { HtmlContentService } from './html-content.service'; import { ClarinSafeHtmlPipe } from './utils/clarin-safehtml.pipe'; +======= +import { EpersonGroupListComponent } from './eperson-group-list/eperson-group-list.component'; +import { EpersonSearchBoxComponent } from './eperson-group-list/eperson-search-box/eperson-search-box.component'; +import { GroupSearchBoxComponent } from './eperson-group-list/group-search-box/group-search-box.component'; +import { + ThemedItemPageTitleFieldComponent +} from '../item-page/simple/field-components/specific-field/title/themed-item-page-field.component'; +import { BitstreamListItemComponent } from './object-list/bitstream-list-item/bitstream-list-item.component'; +import { NgxPaginationModule } from 'ngx-pagination'; +import { ThemedLangSwitchComponent } from './lang-switch/themed-lang-switch.component'; +import {ThemedUserMenuComponent} from './auth-nav-menu/user-menu/themed-user-menu.component'; +>>>>>>> dspace-7.6.1 const MODULES = [ CommonModule, @@ -290,7 +333,12 @@ const MODULES = [ RouterModule, DragDropModule, GoogleRecaptchaModule, +<<<<<<< HEAD MenuModule +======= + MenuModule, + NgxPaginationModule +>>>>>>> dspace-7.6.1 ]; const ROOT_MODULES = [ @@ -314,6 +362,7 @@ const PIPES = [ ObjNgFor, BrowserOnlyPipe, MarkdownPipe, +<<<<<<< HEAD ShortNumberPipe, ObjNgFor, ClarinExtendedLicensePipe, @@ -322,6 +371,9 @@ const PIPES = [ ClarinLicenseRequiredInfoPipe, CharToEndPipe, ClarinSafeHtmlPipe +======= + ShortNumberPipe +>>>>>>> dspace-7.6.1 ]; const COMPONENTS = [ @@ -330,12 +382,18 @@ const COMPONENTS = [ AuthNavMenuComponent, ThemedAuthNavMenuComponent, UserMenuComponent, +<<<<<<< HEAD +======= + ThemedUserMenuComponent, +>>>>>>> dspace-7.6.1 DsSelectComponent, ErrorComponent, LangSwitchComponent, + ThemedLangSwitchComponent, LoadingComponent, ThemedLoadingComponent, LogInComponent, + ThemedLogInComponent, LogOutComponent, ObjectListComponent, ThemedObjectListComponent, @@ -346,9 +404,16 @@ const COMPONENTS = [ PaginationComponent, RSSComponent, SearchFormComponent, + ThemedSearchFormComponent, PageWithSidebarComponent, SidebarDropdownComponent, ThumbnailComponent, +<<<<<<< HEAD +======= + ThemedThumbnailComponent, + MyDSpaceStatusBadgeComponent, + ThemedMyDSpaceStatusBadgeComponent, +>>>>>>> dspace-7.6.1 ViewModeSwitchComponent, TruncatableComponent, TruncatablePartComponent, @@ -364,6 +429,16 @@ const COMPONENTS = [ ComcolMetadataComponent, TypeBadgeComponent, AccessStatusBadgeComponent, +<<<<<<< HEAD +======= + ThemedAccessStatusBadgeComponent, + ThemedTypeBadgeComponent, + StatusBadgeComponent, + ThemedStatusBadgeComponent, + BadgesComponent, + ThemedBadgesComponent, + +>>>>>>> dspace-7.6.1 ItemSelectComponent, CollectionSelectComponent, MetadataRepresentationLoaderComponent, @@ -382,15 +457,22 @@ const COMPONENTS = [ ItemPageTitleFieldComponent, ThemedSearchNavbarComponent, ListableNotificationObjectComponent, +<<<<<<< HEAD DsoPageSubscriptionButtonComponent, +======= +>>>>>>> dspace-7.6.1 MetadataFieldWrapperComponent, ContextHelpWrapperComponent, EpersonGroupListComponent, EpersonSearchBoxComponent, GroupSearchBoxComponent, +<<<<<<< HEAD ScopeSelectorModalComponent, ClarinItemBoxViewComponent, ClarinItemAuthorPreviewComponent +======= + ThemedItemPageTitleFieldComponent, +>>>>>>> dspace-7.6.1 ]; const ENTRY_COMPONENTS = [ @@ -408,6 +490,7 @@ const ENTRY_COMPONENTS = [ SearchResultGridElementComponent, ItemListElementComponent, ItemGridElementComponent, + BitstreamListItemComponent, ItemSearchResultListElementComponent, ItemSearchResultGridElementComponent, BrowseEntryListElementComponent, diff --git a/src/app/shared/sidebar/sidebar-dropdown.component.html b/src/app/shared/sidebar/sidebar-dropdown.component.html index 0c2a1c05d25..2eadac09f75 100644 --- a/src/app/shared/sidebar/sidebar-dropdown.component.html +++ b/src/app/shared/sidebar/sidebar-dropdown.component.html @@ -1,5 +1,5 @@
      -
      +

      diff --git a/src/app/shared/starts-with/date/starts-with-date.component.ts b/src/app/shared/starts-with/date/starts-with-date.component.ts index d65d12b413e..c881f5b1c76 100644 --- a/src/app/shared/starts-with/date/starts-with-date.component.ts +++ b/src/app/shared/starts-with/date/starts-with-date.component.ts @@ -131,7 +131,10 @@ export class StartsWithDateComponent extends StartsWithAbstractComponent { } else { this.startsWithYear = +startsWith; } +<<<<<<< HEAD this.setStartsWithParam(false); +======= +>>>>>>> dspace-7.6.1 } /** diff --git a/src/app/shared/starts-with/starts-with-abstract.component.ts b/src/app/shared/starts-with/starts-with-abstract.component.ts index 3b35a11269b..a4b84ccf780 100644 --- a/src/app/shared/starts-with/starts-with-abstract.component.ts +++ b/src/app/shared/starts-with/starts-with-abstract.component.ts @@ -1,7 +1,7 @@ import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { Subscription } from 'rxjs'; -import { FormControl, FormGroup } from '@angular/forms'; +import { UntypedFormControl, UntypedFormGroup } from '@angular/forms'; import { hasValue } from '../empty.util'; import { PaginationService } from '../../core/pagination/pagination.service'; @@ -21,7 +21,7 @@ export abstract class StartsWithAbstractComponent implements OnInit, OnDestroy { /** * The formdata controlling the StartsWith input */ - formData: FormGroup; + formData: UntypedFormGroup; /** * List of subscriptions @@ -43,8 +43,8 @@ export abstract class StartsWithAbstractComponent implements OnInit, OnDestroy { } }) ); - this.formData = new FormGroup({ - startsWith: new FormControl() + this.formData = new UntypedFormGroup({ + startsWith: new UntypedFormControl() }); } @@ -70,7 +70,10 @@ export abstract class StartsWithAbstractComponent implements OnInit, OnDestroy { */ setStartsWith(startsWith: string) { this.startsWith = startsWith; +<<<<<<< HEAD this.setStartsWithParam(false); +======= +>>>>>>> dspace-7.6.1 } /** diff --git a/src/app/shared/starts-with/text/starts-with-text.component.html b/src/app/shared/starts-with/text/starts-with-text.component.html index 3314d9cc4a8..926a2f932d9 100644 --- a/src/app/shared/starts-with/text/starts-with-text.component.html +++ b/src/app/shared/starts-with/text/starts-with-text.component.html @@ -1,7 +1,7 @@
      - + diff --git a/src/app/shared/subscriptions/subscription-modal/subscription-modal.component.html b/src/app/shared/subscriptions/subscription-modal/subscription-modal.component.html index a71498f0021..bd2c659a59f 100644 --- a/src/app/shared/subscriptions/subscription-modal/subscription-modal.component.html +++ b/src/app/shared/subscriptions/subscription-modal/subscription-modal.component.html @@ -6,8 +6,13 @@