diff --git a/.editorconfig b/.editorconfig index 15d4c87b14..590d1dea08 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 b95b54b979..af1b97849b 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -7,7 +7,8 @@ "eslint-plugin-jsdoc", "eslint-plugin-deprecation", "unused-imports", - "eslint-plugin-lodash" + "eslint-plugin-lodash", + "eslint-plugin-jsonc" ], "overrides": [ { @@ -224,6 +225,42 @@ "@angular-eslint/template/no-negated-async": "off", "@angular-eslint/template/eqeqeq": "off" } + }, + { + "files": [ + "*.json5" + ], + "extends": [ + "plugin:jsonc/recommended-with-jsonc" + ], + "rules": { + "no-irregular-whitespace": "error", + "no-trailing-spaces": "error", + "jsonc/comma-dangle": [ + "error", + "always-multiline" + ], + "jsonc/indent": [ + "error", + 2 + ], + "jsonc/key-spacing": [ + "error", + { + "beforeColon": false, + "afterColon": true, + "mode": "strict" + } + ], + "jsonc/no-dupe-keys": "off", + "jsonc/quotes": [ + "error", + "double", + { + "avoidEscape": false + } + ] + } } ] } diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5470b3cc2a..6fae54f1f9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -68,7 +68,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: @@ -93,12 +93,16 @@ jobs: - name: Run specs (unit tests) run: yarn run test:headless + # Upload code coverage report to artifact (for one version of Node only), + # so that it can be shared with the 'codecov' job (see below) # NOTE: Angular CLI only supports code coverage for specs. See https://github.com/angular/angular-cli/issues/6286 - # Upload coverage reports to Codecov (for one version of Node only) - # https://github.com/codecov/codecov-action - - name: Upload coverage to Codecov.io - uses: codecov/codecov-action@v3 - if: matrix.node-version == '16.x' + - name: Upload code coverage report to Artifact + uses: actions/upload-artifact@v3 + if: matrix.node-version == '18.x' + with: + name: dspace-angular coverage report + path: 'coverage/dspace-angular/lcov.info' + retention-days: 14 # Using docker-compose start backend using CI configuration # and load assetstore from a cached copy @@ -112,11 +116,10 @@ jobs: # https://github.com/cypress-io/github-action # (NOTE: to run these e2e tests locally, just use 'ng e2e') - name: Run e2e tests (integration tests) - uses: cypress-io/github-action@v4 + uses: cypress-io/github-action@v5 with: - # Run tests in Chrome, headless mode + # Run tests in Chrome, headless mode (default) browser: chrome - headless: true # Start app before running tests (will be stopped automatically after tests finish) start: yarn run serve:ssr # Wait for backend & frontend to be available @@ -176,3 +179,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/docker.yml b/.github/workflows/docker.yml index 908c5c34fd..9a2c838d83 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -88,3 +88,33 @@ jobs: # Use tags / labels provided by 'docker/metadata-action' above tags: ${{ steps.meta_build.outputs.tags }} labels: ${{ steps.meta_build.outputs.labels }} + + ##################################################### + # Build/Push the 'dspace/dspace-angular' image ('-dist' tag) + ##################################################### + # 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@v3 + with: + context: . + file: ./Dockerfile.dist + platforms: ${{ env.PLATFORMS }} + # For pull requests, we run the Docker build (to ensure no PR changes break the build), + # but we ONLY do an image push to DockerHub if it's NOT a PR + push: ${{ github.event_name != 'pull_request' }} + # Use tags / labels provided by 'docker/metadata-action' above + tags: ${{ steps.meta_build_dist.outputs.tags }} + labels: ${{ steps.meta_build_dist.outputs.labels }} diff --git a/.github/workflows/issue_opened.yml b/.github/workflows/issue_opened.yml index 5d7c1c30f7..b4436dca3a 100644 --- a/.github/workflows/issue_opened.yml +++ b/.github/workflows/issue_opened.yml @@ -16,7 +16,7 @@ jobs: # Only add to project board if issue is flagged as "needs triage" or has no labels # NOTE: By default we flag new issues as "needs triage" in our issue template if: (contains(github.event.issue.labels.*.name, 'needs triage') || join(github.event.issue.labels.*.name) == '') - uses: actions/add-to-project@v0.3.0 + uses: actions/add-to-project@v0.5.0 # Note, the authentication token below is an ORG level Secret. # It must be created/recreated manually via a personal access token with admin:org, project, public_repo permissions # See: https://docs.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token#permissions-for-the-github_token diff --git a/.github/workflows/label_merge_conflicts.yml b/.github/workflows/label_merge_conflicts.yml index a840a4fd17..c1396b6f45 100644 --- a/.github/workflows/label_merge_conflicts.yml +++ b/.github/workflows/label_merge_conflicts.yml @@ -23,7 +23,7 @@ jobs: steps: # See: https://github.com/prince-chrismc/label-merge-conflicts-action - name: Auto-label PRs with merge conflicts - uses: prince-chrismc/label-merge-conflicts-action@v2 + uses: prince-chrismc/label-merge-conflicts-action@v3 # Add "merge conflict" label if a merge conflict is detected. Remove it when resolved. # Note, the authentication token is created automatically # See: https://docs.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token diff --git a/Dockerfile b/Dockerfile index 61d960e7d3..8fac7495e1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,20 +2,27 @@ # See https://github.com/DSpace/dspace-angular/tree/main/docker for usage details FROM node:18-alpine -WORKDIR /app -ADD . /app/ -EXPOSE 4000 # Ensure Python and other build tools are available # These are needed to install some node modules, especially on linux/arm64 RUN apk add --update python3 make g++ && rm -rf /var/cache/apk/* +WORKDIR /app +ADD . /app/ +EXPOSE 4000 + # We run yarn install with an increased network timeout (5min) to avoid "ESOCKETTIMEDOUT" errors from hub.docker.com # See, for example https://github.com/yarnpkg/yarn/issues/5540 RUN yarn install --network-timeout 300000 +# When running in dev mode, 4GB of memory is required to build & launch the app. +# This default setting can be overridden as needed in your shell, via an env file or in docker-compose. +# See Docker environment var precedence: https://docs.docker.com/compose/environment-variables/envvars-precedence/ +ENV NODE_OPTIONS="--max_old_space_size=4096" + # On startup, run in DEVELOPMENT mode (this defaults to live reloading enabled, etc). # Listen / accept connections from all IP addresses. # NOTE: At this time it is only possible to run Docker container in Production mode -# if you have a public IP. See https://github.com/DSpace/dspace-angular/issues/1485 +# if you have a public URL. See https://github.com/DSpace/dspace-angular/issues/1485 +ENV NODE_ENV development CMD yarn serve --host 0.0.0.0 diff --git a/Dockerfile.dist b/Dockerfile.dist new file mode 100644 index 0000000000..2a6a66fc06 --- /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/angular.json b/angular.json index b32670ad77..56c75d7b2d 100644 --- a/angular.json +++ b/angular.json @@ -266,7 +266,8 @@ "options": { "lintFilePatterns": [ "src/**/*.ts", - "src/**/*.html" + "src/**/*.html", + "src/**/*.json5" ] } } diff --git a/cypress/e2e/submission.cy.ts b/cypress/e2e/submission.cy.ts index 8262bc841e..ed10b2d13a 100644 --- a/cypress/e2e/submission.cy.ts +++ b/cypress/e2e/submission.cy.ts @@ -122,8 +122,6 @@ describe('New Submission page', () => { // Wait for upload to complete before proceeding cy.wait('@upload'); - // Close the upload success notice - cy.get('[data-dismiss="alert"]').click({multiple: true}); // Wait for deposit button to not be disabled & click it. cy.get('button#deposit').should('not.be.disabled').click(); diff --git a/docker/README.md b/docker/README.md index 1a9fee0a81..42deb793f9 100644 --- a/docker/README.md +++ b/docker/README.md @@ -6,7 +6,20 @@ If you wish to run DSpace on Docker in production, we recommend building your own Docker images. You are welcome to borrow ideas/concepts from the below images in doing so. But, the below images should not be used "as is" in any production scenario. *** -## 'Dockerfile' in root directory +## Overview +The scripts in this directory can be used to start the DSpace User Interface (frontend) in Docker. +Optionally, the backend (REST API) might also be started in Docker. + +For additional options/settings in starting the backend (REST API) in Docker, see the Docker Compose +documentation for the backend: https://github.com/DSpace/DSpace/blob/main/dspace/src/main/docker-compose/README.md + +## Root directory + +The root directory of this project contains all the Dockerfiles which may be referenced by +the Docker compose scripts in this 'docker' folder. + +### Dockerfile + This Dockerfile is used to build a *development* DSpace 7 Angular UI image, published as 'dspace/dspace-angular' ``` @@ -20,7 +33,18 @@ Admins to our DockerHub repo can manually publish with the following command. docker push dspace/dspace-angular:dspace-7_x ``` -## docker directory +### Dockerfile.dist + +The `Dockerfile.dist` is used to generate a *production* build and runtime environment. + +```bash +# build the latest image +docker build -f Dockerfile.dist -t dspace/dspace-angular:dspace-7_x-dist . +``` + +A default/demo version of this image is built *automatically*. + +## 'docker' directory - docker-compose.yml - Starts DSpace Angular with Docker Compose from the current branch. This file assumes that a DSpace 7 REST instance will also be started in Docker. - docker-compose-rest.yml @@ -45,23 +69,47 @@ docker-compose -f docker/docker-compose.yml build ## To start DSpace (REST and Angular) from your branch +This command provides a quick way to start both the frontend & backend from this single codebase ``` docker-compose -p d7 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml up -d ``` +Keep in mind, you may also start the backend by cloning the 'DSpace/DSpace' GitHub repository separately. See the next section. + + ## Run DSpace REST and DSpace Angular from local branches. + +This section assumes that you have clones *both* the 'DSpace/DSpace' and 'DSpace/dspace-angular' GitHub +repositories. When both are available locally, you can spin up both in Docker and have them work together. + _The system will be started in 2 steps. Each step shares the same docker network._ -From DSpace/DSpace (build as needed) +From 'DSpace/DSpace' clone (build first as needed): ``` docker-compose -p d7 up -d ``` -From DSpace/DSpace-angular +NOTE: More detailed instructions on starting the backend via Docker can be found in the [Docker Compose instructions for the Backend](https://github.com/DSpace/DSpace/blob/main/dspace/src/main/docker-compose/README.md). + +From 'DSpace/dspace-angular' clone (build first as needed) ``` docker-compose -p d7 -f docker/docker-compose.yml up -d ``` +At this point, you should be able to access the UI from http://localhost:4000, +and the backend at http://localhost:8080/server/ + +## Run DSpace Angular dist build with DSpace Demo site backend + +This allows you to run the Angular UI in *production* mode, pointing it at the demo backend +(https://api7.dspace.org/server/). + +``` +docker-compose -f docker/docker-compose-dist.yml pull +docker-compose -f docker/docker-compose-dist.yml build +docker-compose -p d7 -f docker/docker-compose-dist.yml up -d +``` + ## Ingest test data from AIPDIR Create an administrator @@ -87,9 +135,10 @@ Load assetstore content and trigger a re-index of the repository docker-compose -p d7 -f docker/cli.yml -f docker/cli.assetstore.yml run --rm dspace-cli ``` -## End to end testing of the rest api (runs in travis). -_In this instance, only the REST api runs in Docker using the Entities dataset. Travis will perform CI testing of Angular using Node to drive the tests._ +## End to end testing of the REST API (runs in GitHub Actions CI). +_In this instance, only the REST api runs in Docker using the Entities dataset. GitHub Actions will perform CI testing of Angular using Node to drive the tests. See `.github/workflows/build.yml` for more details._ +This command is only really useful for testing our Continuous Integration process. ``` -docker-compose -p d7ci -f docker/docker-compose-travis.yml up -d +docker-compose -p d7ci -f docker/docker-compose-ci.yml up -d ``` diff --git a/docker/docker-compose-dist.yml b/docker/docker-compose-dist.yml new file mode 100644 index 0000000000..1c75539da9 --- /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: api7.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 b73f1b7a39..e5f62600e7 100644 --- a/docker/docker-compose-rest.yml +++ b/docker/docker-compose-rest.yml @@ -39,7 +39,7 @@ services: # proxies.trusted.ipranges: This setting is required for a REST API running in Docker to trust requests # from the host machine. This IP range MUST correspond to the 'dspacenet' subnet defined above. proxies__P__trusted__P__ipranges: '172.23.0' - image: dspace/dspace:dspace-7_x-test + image: "${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-dspace-7_x-test}" depends_on: - dspacedb networks: @@ -82,8 +82,7 @@ services: # DSpace Solr container dspacesolr: container_name: dspacesolr - # Uses official Solr image at https://hub.docker.com/_/solr/ - image: solr:8.11-slim + image: "${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-dspace-7_x}" # Needs main 'dspace' container to start first to guarantee access to solr_configs depends_on: - dspace @@ -96,28 +95,26 @@ services: tty: true working_dir: /var/solr/data volumes: - # Mount our "solr_configs" volume available under the Solr's configsets folder (in a 'dspace' subfolder) - # This copies the Solr configs from main 'dspace' container into 'dspacesolr' via that volume - - solr_configs:/opt/solr/server/solr/configsets/dspace # Keep Solr data directory between reboots - solr_data:/var/solr/data # Initialize all DSpace Solr cores using the mounted local configsets (see above), then start Solr # * First, run precreate-core to create the core (if it doesn't yet exist). If exists already, this is a no-op - # * Second, copy updated configs from mounted configsets to this core. If it already existed, this updates core - # to the latest configs. If it's a newly created core, this is a no-op. + # * Second, copy configsets to this core: + # Updates to Solr configs require the container to be rebuilt/restarted: + # `docker-compose -p d7 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml up -d --build dspacesolr` entrypoint: - /bin/bash - '-c' - | init-var-solr - precreate-core authority /opt/solr/server/solr/configsets/dspace/authority - cp -r -u /opt/solr/server/solr/configsets/dspace/authority/* authority - precreate-core oai /opt/solr/server/solr/configsets/dspace/oai - cp -r -u /opt/solr/server/solr/configsets/dspace/oai/* oai - precreate-core search /opt/solr/server/solr/configsets/dspace/search - cp -r -u /opt/solr/server/solr/configsets/dspace/search/* search - precreate-core statistics /opt/solr/server/solr/configsets/dspace/statistics - cp -r -u /opt/solr/server/solr/configsets/dspace/statistics/* statistics + precreate-core authority /opt/solr/server/solr/configsets/authority + cp -r /opt/solr/server/solr/configsets/authority/* authority + precreate-core oai /opt/solr/server/solr/configsets/oai + cp -r /opt/solr/server/solr/configsets/oai/* oai + precreate-core search /opt/solr/server/solr/configsets/search + cp -r /opt/solr/server/solr/configsets/search/* search + precreate-core statistics /opt/solr/server/solr/configsets/statistics + cp -r /opt/solr/server/solr/configsets/statistics/* statistics exec solr -f volumes: assetstore: diff --git a/docker/dspace-ui.json b/docker/dspace-ui.json new file mode 100644 index 0000000000..0758679ab8 --- /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/package.json b/package.json index b994672548..e1bedeb02b 100644 --- a/package.json +++ b/package.json @@ -163,13 +163,14 @@ "compression-webpack-plugin": "^9.2.0", "copy-webpack-plugin": "^6.4.1", "cross-env": "^7.0.3", - "cypress": "^12.0.1", + "cypress": "12.9.0", "cypress-axe": "^1.1.0", "deep-freeze": "0.0.1", "eslint": "^8.2.0", "eslint-plugin-deprecation": "^1.3.2", "eslint-plugin-import": "^2.25.4", "eslint-plugin-jsdoc": "^39.6.4", + "eslint-plugin-jsonc": "^2.6.0", "eslint-plugin-lodash": "^7.4.0", "eslint-plugin-unused-imports": "^2.0.0", "express-static-gzip": "^2.1.5", 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 7171c67585..95e69277a8 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 @@ -97,9 +97,15 @@ export class BatchImportPageComponent { this.router.navigateByUrl(getProcessDetailRoute(rd.payload.processId)); } } else { - const title = this.translate.get('process.new.notification.error.title'); - const content = this.translate.get('process.new.notification.error.content'); - this.notificationsService.error(title, content); + if (rd.statusCode === 413) { + const title = this.translate.get('process.new.notification.error.title'); + const content = this.translate.get('process.new.notification.error.max-upload.content'); + this.notificationsService.error(title, content); + } else { + const title = this.translate.get('process.new.notification.error.title'); + const content = this.translate.get('process.new.notification.error.content'); + this.notificationsService.error(title, content); + } } }); } diff --git a/src/app/core/auth/auth.actions.ts b/src/app/core/auth/auth.actions.ts index 60440d371e..6bc4565682 100644 --- a/src/app/core/auth/auth.actions.ts +++ b/src/app/core/auth/auth.actions.ts @@ -17,6 +17,7 @@ export const AuthActionTypes = { AUTHENTICATED_SUCCESS: type('dspace/auth/AUTHENTICATED_SUCCESS'), CHECK_AUTHENTICATION_TOKEN: type('dspace/auth/CHECK_AUTHENTICATION_TOKEN'), CHECK_AUTHENTICATION_TOKEN_COOKIE: type('dspace/auth/CHECK_AUTHENTICATION_TOKEN_COOKIE'), + SET_AUTH_COOKIE_STATUS: type('dspace/auth/SET_AUTH_COOKIE_STATUS'), RETRIEVE_AUTH_METHODS: type('dspace/auth/RETRIEVE_AUTH_METHODS'), RETRIEVE_AUTH_METHODS_SUCCESS: type('dspace/auth/RETRIEVE_AUTH_METHODS_SUCCESS'), RETRIEVE_AUTH_METHODS_ERROR: type('dspace/auth/RETRIEVE_AUTH_METHODS_ERROR'), @@ -150,6 +151,19 @@ export class CheckAuthenticationTokenCookieAction implements Action { public type: string = AuthActionTypes.CHECK_AUTHENTICATION_TOKEN_COOKIE; } +/** + * Sets the authentication cookie status to flag an external authentication response. + */ +export class SetAuthCookieStatus implements Action { + public type: string = AuthActionTypes.SET_AUTH_COOKIE_STATUS; + + payload = false; + + constructor(exists: boolean) { + this.payload = exists; + } +} + /** * Sign out. * @class LogOutAction @@ -425,6 +439,7 @@ export type AuthActions | AuthenticationSuccessAction | CheckAuthenticationTokenAction | CheckAuthenticationTokenCookieAction + | SetAuthCookieStatus | RedirectWhenAuthenticationIsRequiredAction | RedirectWhenTokenExpiredAction | AddAuthenticationMessageAction diff --git a/src/app/core/auth/auth.effects.spec.ts b/src/app/core/auth/auth.effects.spec.ts index f09db04d99..2e6ba917aa 100644 --- a/src/app/core/auth/auth.effects.spec.ts +++ b/src/app/core/auth/auth.effects.spec.ts @@ -214,12 +214,15 @@ describe('AuthEffects', () => { authenticated: true }) ); + spyOn((authEffects as any).authService, 'setExternalAuthStatus'); actions = hot('--a-', { a: { type: AuthActionTypes.CHECK_AUTHENTICATION_TOKEN_COOKIE } }); const expected = cold('--b-', { b: new RetrieveTokenAction() }); expect(authEffects.checkTokenCookie$).toBeObservable(expected); authEffects.checkTokenCookie$.subscribe(() => { + expect(authServiceStub.setExternalAuthStatus).toHaveBeenCalled(); + expect(authServiceStub.isExternalAuthentication).toBeTrue(); expect((authEffects as any).authorizationsService.invalidateAuthorizationsRequestCache).toHaveBeenCalled(); }); }); diff --git a/src/app/core/auth/auth.effects.ts b/src/app/core/auth/auth.effects.ts index 22d1bf35e7..281355b769 100644 --- a/src/app/core/auth/auth.effects.ts +++ b/src/app/core/auth/auth.effects.ts @@ -153,6 +153,7 @@ export class AuthEffects { return this.authService.checkAuthenticationCookie().pipe( map((response: AuthStatus) => { if (response.authenticated) { + this.authService.setExternalAuthStatus(true); this.authorizationsService.invalidateAuthorizationsRequestCache(); return new RetrieveTokenAction(); } else { diff --git a/src/app/core/auth/auth.reducer.spec.ts b/src/app/core/auth/auth.reducer.spec.ts index 8ebc9f6cb0..c0619adf79 100644 --- a/src/app/core/auth/auth.reducer.spec.ts +++ b/src/app/core/auth/auth.reducer.spec.ts @@ -8,6 +8,7 @@ import { AuthenticationErrorAction, AuthenticationSuccessAction, CheckAuthenticationTokenAction, + SetAuthCookieStatus, CheckAuthenticationTokenCookieAction, LogOutAction, LogOutErrorAction, @@ -219,6 +220,28 @@ describe('authReducer', () => { expect(newState).toEqual(state); }); + it('should set the authentication cookie status in response to a SET_AUTH_COOKIE_STATUS action', () => { + initialState = { + authenticated: true, + loaded: false, + blocking: false, + loading: true, + externalAuth: false, + idle: false + }; + const action = new SetAuthCookieStatus(true); + const newState = authReducer(initialState, action); + state = { + authenticated: true, + loaded: false, + blocking: false, + loading: true, + externalAuth: true, + idle: false + }; + expect(newState).toEqual(state); + }); + it('should properly set the state, in response to a LOG_OUT action', () => { initialState = { authenticated: true, diff --git a/src/app/core/auth/auth.reducer.ts b/src/app/core/auth/auth.reducer.ts index acdb8ef812..ba9c41326a 100644 --- a/src/app/core/auth/auth.reducer.ts +++ b/src/app/core/auth/auth.reducer.ts @@ -10,7 +10,7 @@ import { RedirectWhenTokenExpiredAction, RefreshTokenSuccessAction, RetrieveAuthenticatedEpersonSuccessAction, - RetrieveAuthMethodsSuccessAction, + RetrieveAuthMethodsSuccessAction, SetAuthCookieStatus, SetRedirectUrlAction } from './auth.actions'; // import models @@ -59,6 +59,8 @@ export interface AuthState { // all authentication Methods enabled at the backend authMethods?: AuthMethod[]; + externalAuth?: boolean, + // true when the current user is idle idle: boolean; @@ -73,6 +75,7 @@ const initialState: AuthState = { blocking: true, loading: false, authMethods: [], + externalAuth: false, idle: false }; @@ -104,6 +107,11 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut loading: true, }); + case AuthActionTypes.SET_AUTH_COOKIE_STATUS: + return Object.assign({}, state, { + externalAuth: (action as SetAuthCookieStatus).payload + }); + case AuthActionTypes.AUTHENTICATED_ERROR: case AuthActionTypes.RETRIEVE_AUTHENTICATED_EPERSON_ERROR: return Object.assign({}, state, { diff --git a/src/app/core/auth/auth.service.ts b/src/app/core/auth/auth.service.ts index 3034c00197..6604936cde 100644 --- a/src/app/core/auth/auth.service.ts +++ b/src/app/core/auth/auth.service.ts @@ -25,7 +25,7 @@ import { import { CookieService } from '../services/cookie.service'; import { getAuthenticatedUserId, - getAuthenticationToken, + getAuthenticationToken, getExternalAuthCookieStatus, getRedirectUrl, isAuthenticated, isAuthenticatedLoaded, @@ -36,7 +36,7 @@ import { AppState } from '../../app.reducer'; import { CheckAuthenticationTokenAction, RefreshTokenAction, - ResetAuthenticationMessagesAction, + ResetAuthenticationMessagesAction, SetAuthCookieStatus, SetRedirectUrlAction, SetUserAsIdleAction, UnsetUserAsIdleAction @@ -156,6 +156,24 @@ export class AuthService { return this.store.pipe(select(isAuthenticatedLoaded)); } + /** + * Used to set the external authentication status when authenticating via an + * external authentication system (e.g. Shibboleth). + * @param external + */ + public setExternalAuthStatus(external: boolean) { + this.store.dispatch(new SetAuthCookieStatus(external)); + } + + /** + * Returns true if an external authentication system (e.g. Shibboleth) is being used + * for authentication. Returns false otherwise. + */ + public isExternalAuthentication(): Observable { + return this.store.pipe( + select(getExternalAuthCookieStatus)); + } + /** * Returns the href link to authenticated user * @returns {string} diff --git a/src/app/core/auth/selectors.ts b/src/app/core/auth/selectors.ts index ce8d38d6ba..aba739edf6 100644 --- a/src/app/core/auth/selectors.ts +++ b/src/app/core/auth/selectors.ts @@ -116,6 +116,8 @@ const _getRedirectUrl = (state: AuthState) => state.redirectUrl; const _getAuthenticationMethods = (state: AuthState) => state.authMethods; +const _getExternalAuthCookieStatus = (state: AuthState) => state.externalAuth; + /** * Returns true if the user is idle. * @function _isIdle @@ -178,6 +180,16 @@ export const isAuthenticated = createSelector(getAuthState, _isAuthenticated); */ export const isAuthenticatedLoaded = createSelector(getAuthState, _isAuthenticatedLoaded); +/** + * Returns the authentication cookie status. Expect to be true when external authentication + * is used. + * @function getExternalAuthCookieStatus + * @param {AuthState} state + * @param {any} props + * @return {boolean} + */ +export const getExternalAuthCookieStatus = createSelector(getAuthState, _getExternalAuthCookieStatus); + /** * Returns true if the authentication request is loading. * @function isAuthenticationLoading diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 0237a9eb53..319b42d58b 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -176,6 +176,7 @@ import { VocabularyEntryDetailsDataService } from './submission/vocabularies/voc import { IdentifierData } from '../shared/object-list/identifier-data/identifier-data.model'; import { Subscription } from '../shared/subscriptions/models/subscription.model'; import { SupervisionOrderDataService } from './supervision-order/supervision-order-data.service'; +import { ItemRequest } from './shared/item-request.model'; /** * When not in production, endpoint responses can be mocked for testing purposes @@ -369,6 +370,7 @@ export const models = AccessStatusObject, IdentifierData, Subscription, + ItemRequest, ]; @NgModule({ diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index 80da91acb3..c3fa84dd6c 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -246,10 +246,10 @@ export abstract class BaseItemDataService extends IdentifiableDataService * Get the endpoint to move the item * @param itemId */ - public getMoveItemEndpoint(itemId: string): Observable { + public getMoveItemEndpoint(itemId: string, inheritPolicies: boolean): Observable { return this.halService.getEndpoint(this.linkPath).pipe( map((endpoint: string) => this.getIDHref(endpoint, itemId)), - map((endpoint: string) => `${endpoint}/owningCollection`), + map((endpoint: string) => `${endpoint}/owningCollection?inheritPolicies=${inheritPolicies}`) ); } @@ -258,14 +258,14 @@ export abstract class BaseItemDataService extends IdentifiableDataService * @param itemId * @param collection */ - public moveToCollection(itemId: string, collection: Collection): Observable> { + public moveToCollection(itemId: string, collection: Collection, inheritPolicies: boolean): Observable> { const options: HttpOptions = Object.create({}); let headers = new HttpHeaders(); headers = headers.append('Content-Type', 'text/uri-list'); options.headers = headers; const requestId = this.requestService.generateRequestId(); - const hrefObs = this.getMoveItemEndpoint(itemId); + const hrefObs = this.getMoveItemEndpoint(itemId, inheritPolicies); hrefObs.pipe( find((href: string) => hasValue(href)), diff --git a/src/app/footer/themed-footer.component.ts b/src/app/footer/themed-footer.component.ts index c52a0af29f..e8f64f3434 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/header-nav-wrapper/themed-header-navbar-wrapper.component.ts b/src/app/header-nav-wrapper/themed-header-navbar-wrapper.component.ts index 7f9c181fe2..02d09c44ef 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/home-page/themed-home-page.component.ts b/src/app/home-page/themed-home-page.component.ts index e50f955cb1..c0ef723b38 100644 --- a/src/app/home-page/themed-home-page.component.ts +++ b/src/app/home-page/themed-home-page.component.ts @@ -8,8 +8,6 @@ import { Component } from '@angular/core'; templateUrl: '../shared/theme-support/themed.component.html', }) export class ThemedHomePageComponent extends ThemedComponent { - protected inAndOutputNames: (keyof HomePageComponent & keyof this)[]; - protected getComponentName(): string { return 'HomePageComponent'; diff --git a/src/app/item-page/edit-item-page/item-move/item-move.component.spec.ts b/src/app/item-page/edit-item-page/item-move/item-move.component.spec.ts index d200891629..43ac47e37b 100644 --- a/src/app/item-page/edit-item-page/item-move/item-move.component.spec.ts +++ b/src/app/item-page/edit-item-page/item-move/item-move.component.spec.ts @@ -134,9 +134,10 @@ describe('ItemMoveComponent', () => { }); comp.selectedCollectionName = 'selected-collection-id'; comp.selectedCollection = collection1; + comp.inheritPolicies = false; comp.moveToCollection(); - expect(itemDataService.moveToCollection).toHaveBeenCalledWith('item-id', collection1); + expect(itemDataService.moveToCollection).toHaveBeenCalledWith('item-id', collection1, false); }); it('should call notificationsService success message on success', () => { comp.moveToCollection(); diff --git a/src/app/item-page/edit-item-page/item-move/item-move.component.ts b/src/app/item-page/edit-item-page/item-move/item-move.component.ts index df2b24ea7c..0f9df588a1 100644 --- a/src/app/item-page/edit-item-page/item-move/item-move.component.ts +++ b/src/app/item-page/edit-item-page/item-move/item-move.component.ts @@ -106,7 +106,7 @@ export class ItemMoveComponent implements OnInit { */ moveToCollection() { this.processing = true; - const move$ = this.itemDataService.moveToCollection(this.item.id, this.selectedCollection) + const move$ = this.itemDataService.moveToCollection(this.item.id, this.selectedCollection, this.inheritPolicies) .pipe(getFirstCompletedRemoteData()); move$.subscribe((response: RemoteData) => { 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 4c32ba5376..2ad43f6883 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,15 +13,16 @@ 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; thumbnailPlaceholder = './assets/images/replacement_image.svg'; - galleryOptions: NgxGalleryOptions[]; - galleryImages: NgxGalleryImage[]; + galleryOptions: NgxGalleryOptions[] = []; + + galleryImages: NgxGalleryImage[] = []; /** * Whether or not the current user is authenticated @@ -33,11 +34,7 @@ export class MediaViewerImageComponent implements OnInit { ) { } - /** - * 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, @@ -53,7 +50,6 @@ export class MediaViewerImageComponent implements OnInit { previewFullscreen: true, }, ]; - if (this.image) { this.galleryImages = [ { @@ -67,6 +63,11 @@ 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 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 c133f6f63b..3217680815 100644 --- a/src/app/item-page/media-viewer/media-viewer-video/media-viewer-video.component.html +++ b/src/app/item-page/media-viewer/media-viewer-video/media-viewer-video.component.html @@ -1,23 +1,22 @@ -
+
- + -
- +
+
diff --git a/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.html b/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.html index 9c0039d263..ff98057334 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 @@ -16,13 +16,13 @@
- + -
- +
+
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 index e7a526bb05..a290b82dd9 100644 --- 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 @@ -19,7 +19,7 @@ export class ThemedMetadataRepresentationListComponent extends ThemedComponent { - protected inAndOutputNames: (keyof MyDSpacePageComponent & keyof this)[]; protected getComponentName(): string { return 'MyDSpacePageComponent'; diff --git a/src/app/search-page/themed-configuration-search-page.component.ts b/src/app/search-page/themed-configuration-search-page.component.ts index e4d6e93402..e367ee5238 100644 --- a/src/app/search-page/themed-configuration-search-page.component.ts +++ b/src/app/search-page/themed-configuration-search-page.component.ts @@ -28,19 +28,18 @@ export class ThemedConfigurationSearchPageComponent extends ThemedComponent { - return import(`./configuration-search-page.component`); + return import('./configuration-search-page.component'); } } diff --git a/src/app/shared/collection-dropdown/themed-collection-dropdown.component.ts b/src/app/shared/collection-dropdown/themed-collection-dropdown.component.ts index 27c883099d..3fb338186f 100644 --- a/src/app/shared/collection-dropdown/themed-collection-dropdown.component.ts +++ b/src/app/shared/collection-dropdown/themed-collection-dropdown.component.ts @@ -11,11 +11,11 @@ export class ThemedCollectionDropdownComponent extends ThemedComponent(); + @Output() searchComplete: EventEmitter = new EventEmitter(); - @Output() theOnlySelectable = new EventEmitter(); + @Output() theOnlySelectable: EventEmitter = new EventEmitter(); - @Output() selectionChange = new EventEmitter(); + @Output() selectionChange = new EventEmitter(); protected inAndOutputNames: (keyof CollectionDropdownComponent & keyof this)[] = ['entityType', 'searchComplete', 'theOnlySelectable', 'selectionChange']; diff --git a/src/app/shared/dso-page/dso-page.module.ts b/src/app/shared/dso-page/dso-page.module.ts index 6820e8eb53..d700b34e77 100644 --- a/src/app/shared/dso-page/dso-page.module.ts +++ b/src/app/shared/dso-page/dso-page.module.ts @@ -9,7 +9,7 @@ import { import { DsoEditMenuExpandableSectionComponent } from '../dso-page/dso-edit-menu/dso-edit-expandable-menu-section/dso-edit-menu-expandable-section.component'; -import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; +import { NgbDropdownModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; const COMPONENTS = [ DsoEditMenuComponent, @@ -25,6 +25,7 @@ const MODULES = [ RouterModule, CommonModule, NgbTooltipModule, + NgbDropdownModule, ]; const PROVIDERS = [ diff --git a/src/app/shared/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 2381ada66d..9e1f1d48aa 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 @@
-
0) { const listGroup = this.group.controls[this.model.id] as FormGroup; + if (this.model.repeatable && this.model.required) { + listGroup.addValidators(this.hasAtLeastOneVocabularyEntry()); + } const pageInfo: PageInfo = new PageInfo({ elementsPerPage: 9999, currentPage: 1 } as PageInfo); @@ -121,7 +123,7 @@ export class DsDynamicListComponent extends DynamicFormControlComponent implemen let tempList: ListItem[] = []; this.optionsList = entries.page; // Make a list of available options (checkbox/radio) and split in groups of 'model.groupLength' - entries.page.forEach((option, key) => { + entries.page.forEach((option: VocabularyEntry, key: number) => { const value = option.authority || option.value; const checked: boolean = isNotEmpty(findKey( this.model.value, @@ -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/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 index f3d8421365..637941ce5b 100644 --- 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 @@ -4,27 +4,14 @@ 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 { SEARCH_CONFIG_SERVICE } from '../../../../../../my-dspace-page/my-dspace-page.component'; -import { SearchConfigurationService } from '../../../../../../core/shared/search/search-configuration.service'; 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'; -import { fadeIn, fadeInOut } from '../../../../../animations/fade'; @Component({ selector: 'ds-themed-dynamic-lookup-relation-external-source-tab', styleUrls: [], templateUrl: '../../../../../theme-support/themed.component.html', - providers: [ - { - provide: SEARCH_CONFIG_SERVICE, - useClass: SearchConfigurationService - } - ], - animations: [ - fadeIn, - fadeInOut - ] }) export class ThemedDynamicLookupRelationExternalSourceTabComponent extends ThemedComponent { protected inAndOutputNames: (keyof DsDynamicLookupRelationExternalSourceTabComponent & keyof this)[] = ['label', 'listId', @@ -44,7 +31,7 @@ export class ThemedDynamicLookupRelationExternalSourceTabComponent extends Theme @Input() repeatable: boolean; - @Output() importedObject: EventEmitter = new EventEmitter(); + @Output() importedObject: EventEmitter = new EventEmitter(); @Input() externalSource: ExternalSource; 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 index da998ed5a6..d44f8f84a0 100644 --- 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 @@ -10,19 +10,11 @@ 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'; -import { SEARCH_CONFIG_SERVICE } from '../../../../../../my-dspace-page/my-dspace-page.component'; -import { SearchConfigurationService } from '../../../../../../core/shared/search/search-configuration.service'; @Component({ selector: 'ds-themed-dynamic-lookup-relation-search-tab', styleUrls: [], templateUrl: '../../../../../theme-support/themed.component.html', - providers: [ - { - provide: SEARCH_CONFIG_SERVICE, - useClass: SearchConfigurationService - } - ] }) export class ThemedDynamicLookupRelationSearchTabComponent extends ThemedComponent { protected inAndOutputNames: (keyof DsDynamicLookupRelationSearchTabComponent & keyof this)[] = ['relationship', 'listId', @@ -51,11 +43,11 @@ export class ThemedDynamicLookupRelationSearchTabComponent extends ThemedCompone @Input() isEditRelationship: boolean; - @Output() deselectObject: EventEmitter = new EventEmitter(); + @Output() deselectObject: EventEmitter = new EventEmitter(); - @Output() selectObject: EventEmitter = new EventEmitter(); + @Output() selectObject: EventEmitter = new EventEmitter(); - @Output() resultFound: EventEmitter> = new EventEmitter>(); + @Output() resultFound: EventEmitter> = new EventEmitter(); protected getComponentName(): string { return 'DsDynamicLookupRelationSearchTabComponent'; 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 b68963e5ad..7d1a9a3986 100644 --- a/src/app/shared/form/builder/form-builder.service.spec.ts +++ b/src/app/shared/form/builder/form-builder.service.spec.ts @@ -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, diff --git a/src/app/shared/loading/themed-loading.component.ts b/src/app/shared/loading/themed-loading.component.ts index ffdf9d3cbe..48773d75c8 100644 --- a/src/app/shared/loading/themed-loading.component.ts +++ b/src/app/shared/loading/themed-loading.component.ts @@ -14,8 +14,8 @@ import { ThemeService } from '../theme-support/theme.service'; export class ThemedLoadingComponent extends ThemedComponent { @Input() message: string; - @Input() showMessage = true; - @Input() spinner = false; + @Input() showMessage: boolean; + @Input() spinner: boolean; protected inAndOutputNames: (keyof LoadingComponent & keyof this)[] = ['message', 'showMessage', 'spinner']; 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 ea5a38e3cb..9166e42040 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,11 +1,10 @@ -import { ChangeDetectorRef, Component, ComponentFactoryResolver, Input } from '@angular/core'; +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 { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type'; 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'; /** * Themed wrapper for ItemListPreviewComponent @@ -24,22 +23,10 @@ export class ThemedItemListPreviewComponent extends ThemedComponent { - /** - * The view mode of the this component - */ - viewMode = ViewMode.ListElement; /** * The current pagination configuration @@ -37,18 +32,20 @@ export class ThemedObjectListComponent extends ThemedComponent